diff --git a/.ci.yaml b/.ci.yaml index 9526059749637..12abdde9297e9 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -177,6 +177,9 @@ targets: - name: Linux Framework Smoke Tests recipe: engine/framework_smoke + enabled_branches: + - main + - master timeout: 60 scheduler: luci @@ -208,6 +211,10 @@ targets: properties: add_recipes_cq: "true" gcs_goldens_bucket: flutter_logs + dependencies: >- + [ + {"dependency": "goldctl"} + ] timeout: 60 scheduler: luci runIf: @@ -220,6 +227,9 @@ targets: - name: Linux Web Framework tests recipe: engine/web_engine_framework + enabled_branches: + - main + - master properties: add_recipes_cq: "true" framework: "true" @@ -274,6 +284,10 @@ targets: recipe: web_engine properties: gcs_goldens_bucket: flutter_logs + dependencies: >- + [ + {"dependency": "goldctl"} + ] timeout: 60 scheduler: luci runIf: @@ -284,6 +298,33 @@ targets: - ci/** - flutter_frontend_server/** + - name: Mac mac_ios_engine + recipe: engine_v2/engine_v2 + bringup: true + timeout: 60 + properties: + config_name: mac_ios_engine + environment: Staging + scheduler: luci + + - name: Mac mac_ios_engine_profile + recipe: engine_v2/engine_v2 + bringup: true + timeout: 60 + properties: + config_name: mac_ios_engine_profile + environment: Staging + scheduler: luci + + - name: Mac mac_ios_engine_release + recipe: engine_v2/engine_v2 + bringup: true + timeout: 60 + properties: + config_name: mac_ios_engine_release + environment: Staging + scheduler: luci + - name: Windows Android AOT Engine recipe: engine properties: @@ -301,6 +342,24 @@ targets: build_host: "true" scheduler: luci + - name: Windows windows_android_aot_engine + recipe: engine_v2/engine_v2 + bringup: true + timeout: 60 + properties: + config_name: windows_android_aot_engine + environment: Staging + scheduler: luci + + - name: Windows windows_host_engine + recipe: engine_v2/engine_v2 + bringup: true + timeout: 60 + properties: + config_name: windows_host_engine + environment: Staging + scheduler: luci + - name: Windows Unopt recipe: engine_unopt properties: diff --git a/.clang-tidy b/.clang-tidy index fe6d3fcfd359f..a8ad73bfd8abc 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1 +1,13 @@ -Checks: 'google-*' +# Prefix check with "-" to ignore. +Checks: "google-*,\ +-google-objc-global-variable-declaration,\ +-google-objc-avoid-throwing-exception" + +# Only warnings treated as errors are reported +# in the "ci/lint.sh" script and pre-push git hook. +# Add checks when all warnings are fixed +# to prevent new warnings being introduced. +# https://github.com/flutter/flutter/issues/93279 +WarningsAsErrors: "clang-analyzer-osx.*,\ +google-objc-*,\ +google-explicit-constructor" diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 26f5225b9fa53..f5872c2b8a7dd 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,7 +22,7 @@ If you need help, consider asking for advice on the #hackers-new channel on [Dis [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo -[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/master/CONTRIBUTING.md#style +[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style [testing the engine]: https://github.com/flutter/flutter/wiki/Testing-the-engine [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 4c86ce357bc6f..dc2baf82fc88c 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -6,13 +6,14 @@ on: push: branches: - - 'master' + - 'main' jobs: mirror_job: permissions: pull-requests: write runs-on: ubuntu-latest + if: ${{ github.repository == 'flutter/engine' }} name: Mirror main branch to master branch steps: - name: Mirror action step @@ -20,5 +21,5 @@ jobs: uses: google/mirror-branch-action@c6b07e441a7ffc5ae15860c1d0a8107a3a151db8 with: github-token: ${{ secrets.FLUTTERGITHUBBOT_TOKEN }} - source: 'master' - dest: 'main' + source: 'main' + dest: 'master' diff --git a/.gitignore b/.gitignore index 74bb88e9104da..ed1194e1d9e5b 100644 --- a/.gitignore +++ b/.gitignore @@ -65,7 +65,7 @@ third_party/gn/ .packages .pub-cache/ .pub/ -build/ +*/**/build/ flutter_*.png linked_*.ds unlinked.ds diff --git a/AUTHORS b/AUTHORS index 455f2c6699c8f..0cab6e7e7b937 100644 --- a/AUTHORS +++ b/AUTHORS @@ -20,4 +20,5 @@ Hidenori Matsubayashi Sarbagya Dhaubanjar Callum Moffat Koutaro Mori -TheOneWithTheBraid \ No newline at end of file +TheOneWithTheBraid +Twin Sun, LLC diff --git a/BUILD.gn b/BUILD.gn index 5a9848368121c..87b102774b68d 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -21,12 +21,6 @@ config("config") { cflags = [ "/WX" ] # Treat warnings as errors. } } - - defines = [] - - if (is_debug) { - defines += [ "FLUTTER_ENABLE_DIFF_CONTEXT" ] - } } config("export_dynamic_symbols") { @@ -38,11 +32,19 @@ config("export_dynamic_symbols") { if (flutter_prebuilt_dart_sdk) { copy_trees("_copy_trees") { + # There is no prebuilt Dart SDK targeting Fuchsia, but we also don't need + # one, so even when the build is targeting Fuchsia, use the prebuilt + # Dart SDK for the host. + if (current_toolchain == host_toolchain || is_fuchsia) { + prebuilt_dart_sdk = host_prebuilt_dart_sdk + } else { + prebuilt_dart_sdk = target_prebuilt_dart_sdk + } sources = [ { target = "copy_dart_sdk" visibility = [ ":dart_sdk" ] - source = target_prebuilt_dart_sdk + source = prebuilt_dart_sdk dest = "$root_out_dir/dart-sdk" ignore_patterns = "{}" }, @@ -52,8 +54,11 @@ if (flutter_prebuilt_dart_sdk) { # Flutter SDK artifacts should only be built when either doing host builds, or # for cross-compiled desktop targets. +# TODO: We can't build the engine artifacts for arm (32-bit) right now; +# see https://github.com/flutter/flutter/issues/74322 _build_engine_artifacts = - current_toolchain == host_toolchain || (is_linux && !is_chromeos) || is_mac + current_toolchain == host_toolchain || + (is_linux && !is_chromeos && current_cpu != "arm") || is_mac group("dart_sdk") { if (_build_engine_artifacts) { @@ -227,7 +232,7 @@ group("dist") { deps = [ "//flutter/sky/dist" ] } -if (is_fuchsia) { +if (is_fuchsia && enable_unittests) { group("fuchsia_tests") { testonly = true diff --git a/DEPS b/DEPS index 7616a1422577b..c1a64ef1f92cd 100644 --- a/DEPS +++ b/DEPS @@ -27,19 +27,19 @@ 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': '5420cbcf657ddb40bb158e3a299b3cba65108b32', + 'skia_revision': 'a5261995416e0af98403978ccdf18b910adf933b', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. - 'canvaskit_cipd_instance': '1e8cK_8LOs0dz4lqd20LwTUYNqfu_4YL-dFG5yK1xXQC', + 'canvaskit_cipd_instance': 'NcwvqeeKK7urddCbEdDvHytdaCiCA_8-4oS_T_ouGfgC', # When updating the Dart revision, ensure that all entries that are # dependencies of Dart are also updated to match the entries in the # Dart SDK's DEPS file for that revision of Dart. The DEPS file for - # Dart is: https://github.com/dart-lang/sdk/blob/master/DEPS. + # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS. # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '5703908f6d10d2baa0ff28d88973f5a7f0898642', + 'dart_revision': '91e3fa1604328542b8b81c77753af04b90d3a788', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py @@ -48,11 +48,11 @@ vars = { 'dart_browser_launcher_rev': 'c6cc1025d6901926cf022e144ba109677e3548f1', 'dart_clock_rev': 'a494269254ba978e7ef8f192c5f7fec3fc05b9d3', 'dart_collection_rev': 'a4c941ab94044d118b2086a3f261c30377604127', - 'dart_devtools_rev': '8881a7caa9067471008a8e00750b161f53cdb843', + 'dart_devtools_rev': '3a2f570813200e6dee141f3e7a9edcae5f31c2e8', 'dart_intl_tag': '0.17.0-nullsafety', - 'dart_linter_tag': '1.12.0', + 'dart_linter_tag': '1.14.0', 'dart_protobuf_rev': 'c1eb6cb51af39ccbaa1a8e19349546586a5c8e31', - 'dart_pub_rev': '0764437088fd58eb7af779ecef66bab40dfcf2e9', + 'dart_pub_rev': '96404e0749864c9fbf8b12e1d424e8078809e00a', 'dart_root_certificates_rev': '692f6d6488af68e0121317a9c2c9eb393eb0ee50', 'dart_shelf_proxy_tag': 'v1.0.0', 'dart_shelf_static_rev': '202ec1a53c9a830c17cf3b718d089cf7eba568ad', @@ -99,7 +99,7 @@ allowed_hosts = [ ] deps = { - 'src': 'https://github.com/flutter/buildroot.git' + '@' + '2610767168166f98e7e0656893fe1a550d5f6fab', + 'src': 'https://github.com/flutter/buildroot.git' + '@' + '3dc3ec56b3dc76ba60c8348053a9f6ef3446b0f2', # Fuchsia compatibility # @@ -107,14 +107,11 @@ deps = { # build. Eventually, we'll manage these dependencies together with Fuchsia # and not have to specific specific hashes. - 'src/third_party/benchmark': - Var('fuchsia_git') + '/third_party/benchmark' + '@' + 'a779ffce872b4c811beef482e18bd0b63626aa42', - 'src/third_party/rapidjson': Var('fuchsia_git') + '/third_party/rapidjson' + '@' + 'ef3564c5c8824989393b87df25355baf35ff544b', 'src/third_party/harfbuzz': - Var('fuchsia_git') + '/third_party/harfbuzz' + '@' + '9a15acd28cf9c62a5820b6ed1013c4a7f8717d8c', + Var('fuchsia_git') + '/third_party/harfbuzz' + '@' + '047b946d1d0a159d7ef9691a44a91bd1253aec6b', 'src/third_party/libcxx': Var('fuchsia_git') + '/third_party/libcxx' + '@' + '7524ef50093a376f334a62a7e5cebf5d238d4c99', @@ -155,6 +152,9 @@ deps = { 'src/third_party/khronos': Var('chromium_git') + '/chromium/src/third_party/khronos.git' + '@' + '7122230e90547962e0f0c627f62eeed3c701f275', + 'src/third_party/benchmark': + Var('github_git') + '/google/benchmark' + '@' + '431abd149fd76a072f821913c0340137cc755f36', + 'src/third_party/googletest': Var('github_git') + '/google/googletest' + '@' + 'f5e592d8ee5ffb1d9af5be7f715ce3576b8bf9c4', @@ -170,7 +170,7 @@ deps = { # WARNING: Unused Dart dependencies in the list below till "WARNING:" marker are removed automatically - see create_updated_flutter_deps.py. 'src/third_party/dart/third_party/devtools': - {'packages': [{'version': 'git_revision:8881a7caa9067471008a8e00750b161f53cdb843', 'package': 'dart/third_party/flutter/devtools'}], 'dep_type': 'cipd'}, + {'packages': [{'version': 'git_revision:3a2f570813200e6dee141f3e7a9edcae5f31c2e8', 'package': 'dart/third_party/flutter/devtools'}], 'dep_type': 'cipd'}, 'src/third_party/dart/third_party/pkg/args': Var('dart_git') + '/args.git@3b3f55766af13d895d2020ec001a28e8dc147f91', @@ -179,7 +179,7 @@ deps = { Var('dart_git') + '/async.git@80886150a5e6c58006c8ae5a6c2aa7108638e2a9', 'src/third_party/dart/third_party/pkg/bazel_worker': - Var('dart_git') + '/bazel_worker.git@0885637b037979afbf5bcd05fd748b309fd669c0', + Var('dart_git') + '/bazel_worker.git@ceeba0982d4ff40d32371c9d35f3d2dc1868de20', 'src/third_party/dart/third_party/pkg/boolean_selector': Var('dart_git') + '/boolean_selector.git@665e6921ab246569420376f827bff4585dff0b14', @@ -191,7 +191,7 @@ deps = { Var('dart_git') + '/charcode.git@84ea427711e24abf3b832923959caa7dd9a8514b', 'src/third_party/dart/third_party/pkg/cli_util': - Var('dart_git') + '/cli_util.git@71ba36e2554f7b7717f3f12b5ddd33751a4e3ddd', + Var('dart_git') + '/cli_util.git@b0adbba89442b2ea6fef39c7a82fe79cb31e1168', 'src/third_party/dart/third_party/pkg/clock': Var('dart_git') + '/clock.git' + '@' + Var('dart_clock_rev'), @@ -206,10 +206,10 @@ deps = { Var('dart_git') + '/crypto.git@b5024e4de2b1c474dd558bef593ddbf0bfade152', 'src/third_party/dart/third_party/pkg/csslib': - Var('dart_git') + '/csslib.git@6f35da3d93eb56eb25925779d235858d4090ce6f', + Var('dart_git') + '/csslib.git@02abc1ddf93092efef2be365300f15504d23cd23', 'src/third_party/dart/third_party/pkg/dartdoc': - Var('dart_git') + '/dartdoc.git@c2f284f09bcc49025fe26e86b2d45e1b546f81a3', + Var('dart_git') + '/dartdoc.git@520e64977de7a87b2fd5be56e5c2e1a58d55bdad', 'src/third_party/dart/third_party/pkg/ffi': Var('dart_git') + '/ffi.git@4dd32429880a57b64edaf54c9d5af8a9fa9a4ffb', @@ -311,7 +311,7 @@ deps = { Var('dart_git') + '/stack_trace.git@6788afc61875079b71b3d1c3e65aeaa6a25cbc2f', 'src/third_party/dart/third_party/pkg/stream_channel': - Var('dart_git') + '/stream_channel.git@d7251e61253ec389ee6e045ee1042311bced8f1d', + Var('dart_git') + '/stream_channel.git@3fa3e40c75c210d617b8b943b9b8f580e9866a89', 'src/third_party/dart/third_party/pkg/string_scanner': Var('dart_git') + '/string_scanner.git@1b63e6e5db5933d7be0a45da6e1129fe00262734', @@ -329,7 +329,7 @@ deps = { Var('dart_git') + '/typed_data.git@29ce5a92b03326d0b8035916ac04f528874994bd', 'src/third_party/dart/third_party/pkg/usage': - Var('dart_git') + '/usage.git@e0780cd8b2f8af69a28dc52678ffe8492da27d06', + Var('dart_git') + '/usage.git@f0cb8f7cce8b675255c81488dbab8cf9f2f56404', 'src/third_party/dart/third_party/pkg/watcher': Var('dart_git') + '/watcher.git' + '@' + Var('dart_watcher_rev'), @@ -350,13 +350,13 @@ deps = { Var('dart_git') + '/yaml_edit.git' + '@' + Var('dart_yaml_edit_rev'), 'src/third_party/dart/third_party/pkg_tested/dart_style': - Var('dart_git') + '/dart_style.git@14d9b6fd58cc4744676c12be3cc5eee2a779db82', + Var('dart_git') + '/dart_style.git@08b0294d0a500d5c02168ef57dcb8868d0c3cb48', 'src/third_party/dart/third_party/pkg_tested/package_config': Var('dart_git') + '/package_config.git@fb736aa12316dd2d882b202a438a6946a4b4bea0', 'src/third_party/dart/tools/sdks': - {'packages': [{'version': 'version:2.15.0-82.0.dev', 'package': 'dart/dart-sdk/${{platform}}'}], 'dep_type': 'cipd'}, + {'packages': [{'version': 'version:2.15.0-268.8.beta', '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. @@ -427,7 +427,7 @@ deps = { Var('github_git') + '/google/file.dart.git' + '@' + '427bb20ccc852425d67f2880da2a9b4707c266b4', # 6.1.0 'src/third_party/pkg/flutter_packages': - Var('github_git') + '/flutter/packages.git' + '@' + '5617d089f26dd52da3bf05c9fa4620ef11a7419b', # various + Var('github_git') + '/flutter/packages.git' + '@' + 'a19eca7fe2660c71acf5928a275deda1da318c50', # various 'src/third_party/pkg/gcloud': Var('github_git') + '/dart-lang/gcloud.git' + '@' + '92a33a9d95ea94a4354b052a28b98088d660e0e7', # 0.8.0-dev @@ -570,7 +570,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/embedding_bundle', - 'version': 'last_updated:2021-08-10T22:12:57-0700' + 'version': 'last_updated:2021-10-28T23:34:47-0700' } ], 'condition': 'download_android_deps', @@ -614,7 +614,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/third_party/clang/mac-amd64', - 'version': 'KP-N5ruFeLjj1oFUPn4i0_BP2ThkpDYJh7yprjt0JLIC' + 'version': '82gAwI4HhETNeL4q78EcNPj0KP8WVDTlzGuZF3BqkLkC' } ], 'condition': 'host_os == "mac"', @@ -625,19 +625,18 @@ deps = { 'packages': [ { 'package': 'fuchsia/third_party/clang/linux-amd64', - 'version': 'FMLihg51sSNvqIi8NvP9oVfFAYy5DnKP4SSo6TSeV_oC' + 'version': 'UtjvZhwwsamxwtfza47TNJiXjfWtflY5PswmDuJD47MC' } ], 'condition': 'host_os == "linux"', 'dep_type': 'cipd', }, - # TODO(fxb/4443): Remove this when Fuchsia can provide the Windows Clang Toolchain - 'src/third_party/llvm-build/Release+Asserts': { + 'src/buildtools/windows-x64/clang': { 'packages': [ { - 'package': 'flutter/clang/win-amd64', - 'version': 'git_revision:5ec206df8534d2dd8cb9217c3180e5ddba587393' + 'package': 'fuchsia/third_party/clang/windows-amd64', + 'version': 'a4WSJSNW8w7fYZ5GoOcCM7PlBxrXknK4ni71zZV1R_gC' } ], 'condition': 'download_windows_deps', @@ -651,7 +650,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/mac-amd64', - 'version': 'nblJjRFDAh-l2D4eNhZfTsUsSjrbplStpRa8eBsPKkQC' + 'version': 'kK8g7bQdAYLfekJHBnTzYB9UyQ4dXHjALrZe2JpK0-QC' } ], 'condition': 'host_os == "mac"', @@ -661,7 +660,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': 'tBL8VjITEMr2lN4iUgYcOl_MnOZ7-K0bgCFlF0_XGTEC' + 'version': 'Gc37iAM6P0Hy8R3v2mnV7XVe-jip7899hNWQmZcMV8gC' } ], 'condition': 'host_os == "linux"', diff --git a/README.md b/README.md index 5dcb6503d6d34..d4727ceafed1c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Framework](https://github.com/flutter/flutter), which provides a modern, reactive framework, and a rich set of platform, layout and foundation widgets. If you want to run/contribute to Flutter Web engine, more tooling can be -found at [felt](https://github.com/flutter/engine/tree/master/lib/web_ui/dev#whats-felt). +found at [felt](https://github.com/flutter/engine/tree/main/lib/web_ui/dev#whats-felt). This is a tool written to make web engine development experience easy. If you are new to Flutter, then you will find more general information @@ -30,7 +30,7 @@ Flutter is a fully open source project, and we welcome contributions. Information on how to get started can be found at our [contributor guide](CONTRIBUTING.md). -[Build Status - Cirrus]: https://api.cirrus-ci.com/github/flutter/engine.svg?branch=master +[Build Status - Cirrus]: https://api.cirrus-ci.com/github/flutter/engine.svg?branch=main [Build status]: https://cirrus-ci.com/github/flutter/engine diff --git a/benchmarking/benchmarking.h b/benchmarking/benchmarking.h index d84004f4d8b01..872c697c0ee94 100644 --- a/benchmarking/benchmarking.h +++ b/benchmarking/benchmarking.h @@ -5,13 +5,13 @@ #ifndef FLUTTER_BENCHMARKING_BENCHMARKING_H_ #define FLUTTER_BENCHMARKING_BENCHMARKING_H_ -#include "benchmark/benchmark_api.h" +#include "benchmark/benchmark.h" namespace benchmarking { class ScopedPauseTiming { public: - ScopedPauseTiming(::benchmark::State& state, bool enabled = true) + explicit ScopedPauseTiming(::benchmark::State& state, bool enabled = true) : state_(state), enabled_(enabled) { if (enabled_) { state_.PauseTiming(); diff --git a/build/dart/rules.gni b/build/dart/rules.gni index d578b4890102d..820d65706265f 100644 --- a/build/dart/rules.gni +++ b/build/dart/rules.gni @@ -8,6 +8,7 @@ import("//build/compiled_action.gni") import("//build/module_args/dart.gni") import("//flutter/common/config.gni") import("//third_party/dart/build/dart/dart_action.gni") +import("//third_party/dart/sdk_args.gni") frontend_server_files = exec_script("//third_party/dart/tools/list_dart_files.py", @@ -31,6 +32,9 @@ frontend_server_files += # # Invoker must supply dart_main and package_config. Invoker may optionally # supply aot as a boolean and product as a boolean. +# +# On Android, the invoker may provide output_aot_lib as a string to override +# the default filename for the aot-elf snapshot. template("flutter_snapshot") { assert(!is_fuchsia) assert(defined(invoker.main_dart), "main_dart is a required parameter.") @@ -87,12 +91,12 @@ template("flutter_snapshot") { # build) causes the vm service isolate code to be tree-shaken from an app. # See the pragma on the entrypoint here: # - # https://github.com/dart-lang/sdk/blob/master/sdk/lib/_internal/vm/bin/vmservice_io.dart#L240 + # https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/vm/bin/vmservice_io.dart#L240 # # Also, this define excludes debugging and profiling code from Flutter. args += [ "-Ddart.vm.product=true" ] } else { - if (!is_debug) { + if (flutter_runtime_mode == "profile") { # The following define excludes debugging code from Flutter. args += [ "-Ddart.vm.profile=true" ] } @@ -134,7 +138,12 @@ template("flutter_snapshot") { "--assembly=" + rebase_path(snapshot_assembly), ] } else if (is_android) { - libapp = "$target_gen_dir/android/libs/$android_app_abi/libapp.so" + if (defined(invoker.output_aot_lib)) { + output_aot_lib = invoker.output_aot_lib + } else { + output_aot_lib = "libapp.so" + } + libapp = "$target_gen_dir/android/libs/$android_app_abi/$output_aot_lib" outputs += [ libapp ] args += [ "--snapshot_kind=app-aot-elf", @@ -259,6 +268,13 @@ template("application_snapshot") { assert(false, "Bad snapshot_kind: '$snapshot_kind'") } + # Ensure the compiled appliation (e.g. frontend-server, ...) will use this + # Dart SDK hash when consuming/producing kernel. + # + # (Instead of ensuring every user of the "application_snapshot" passes it's + # own) + snapshot_vm_args += [ "-Dsdk_hash=$sdk_hash" ] + if (flutter_prebuilt_dart_sdk) { action(target_name) { forward_variables_from(invoker, @@ -266,7 +282,7 @@ template("application_snapshot") { "testonly", "visibility", ]) - deps = extra_deps + [ "//flutter:dart_sdk" ] + deps = extra_deps script = "//build/gn_run_binary.py" inputs = extra_inputs outputs = [ output ] @@ -277,7 +293,7 @@ template("application_snapshot") { if (is_win) { ext = ".exe" } - dart = rebase_path("$host_prebuilt_dart_sdk/bin/dart$ext", root_out_dir) + dart = rebase_path("$host_prebuilt_dart_sdk/bin/dart$ext", root_build_dir) args = [ dart ] args += snapshot_vm_args diff --git a/build/dart/tools/dart_pkg.py b/build/dart/tools/dart_pkg.py index 09c7403eab757..9e7d38c8c47b3 100755 --- a/build/dart/tools/dart_pkg.py +++ b/build/dart/tools/dart_pkg.py @@ -202,7 +202,7 @@ def main(): default=[]) parser.add_argument('--sdk-ext-files', metavar='sdk_ext_files', - help='List of .dart files that are part of of sdk_ext.', + help='List of .dart files that are part of sdk_ext.', nargs='*', default=[]) parser.add_argument('--sdk-ext-mappings', diff --git a/ci/bin/format.dart b/ci/bin/format.dart index 700995ea5529a..608ef4b28be99 100644 --- a/ci/bin/format.dart +++ b/ci/bin/format.dart @@ -804,7 +804,7 @@ Future _getDiffBaseRevision(ProcessManager processManager, Directory rep if (upstreamUrl.isEmpty) { upstream = 'origin'; } - await _runGit(['fetch', upstream, 'master'], processRunner); + await _runGit(['fetch', upstream, 'main'], processRunner); String result = ''; try { // This is the preferred command to use, but developer checkouts often do diff --git a/ci/firebase_testlab.py b/ci/firebase_testlab.py index effd543819ecd..12a5b6cad5504 100755 --- a/ci/firebase_testlab.py +++ b/ci/firebase_testlab.py @@ -23,7 +23,7 @@ def RunFirebaseTest(apk, results_dir): # This type of test will give the application a handle to a file, and # we'll write the timeline JSON to that file. # See https://firebase.google.com/docs/test-lab/android/game-loop - # Pixel 4. As of this commit, this is a highly available device in FTL. + # Pixel 5. As of this commit, this is a highly available device in FTL. process = subprocess.Popen( [ 'gcloud', @@ -34,7 +34,7 @@ def RunFirebaseTest(apk, results_dir): '--timeout', '2m', '--results-bucket', bucket, '--results-dir', results_dir, - '--device', 'model=flame,version=29', + '--device', 'model=redfin,version=30', ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -98,8 +98,8 @@ def main(): print(line.strip()) return_code = process.wait() if return_code != 0: - print('Firebase test failed ' + returncode) - sys.exit(process.returncode) + print('Firebase test failed with code: %s' % return_code) + sys.exit(return_code) print('Checking logcat for %s' % results_dir) CheckLogcat(results_dir) diff --git a/ci/licenses.sh b/ci/licenses.sh index 15efdd80ca367..d661acc12a554 100755 --- a/ci/licenses.sh +++ b/ci/licenses.sh @@ -89,7 +89,7 @@ function verify_licenses() ( echo "changed, and then update this file:" echo " flutter/sky/packages/sky_engine/LICENSE" echo "For more information, see the script in:" - echo " https://github.com/flutter/engine/tree/master/tools/licenses" + echo " https://github.com/flutter/engine/tree/main/tools/licenses" echo "" diff -U 6 "flutter/ci/licenses_golden/$(basename "$f")" "$f" echo "=================================================================" @@ -109,7 +109,7 @@ function verify_licenses() ( echo "license tool signature golden file:" echo " ci/licenses_golden/tool_signature" echo "For more information, see the script in:" - echo " https://github.com/flutter/engine/tree/master/tools/licenses" + echo " https://github.com/flutter/engine/tree/main/tools/licenses" echo "" diff -U 6 "flutter/ci/licenses_golden/tool_signature" "out/license_script_output/tool_signature" echo "=================================================================" diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index bec85aa0620c7..037cc20ff2702 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -110,9 +110,6 @@ FILE: ../../../flutter/flow/layers/texture_layer_unittests.cc FILE: ../../../flutter/flow/layers/transform_layer.cc FILE: ../../../flutter/flow/layers/transform_layer.h FILE: ../../../flutter/flow/layers/transform_layer_unittests.cc -FILE: ../../../flutter/flow/matrix_decomposition.cc -FILE: ../../../flutter/flow/matrix_decomposition.h -FILE: ../../../flutter/flow/matrix_decomposition_unittests.cc FILE: ../../../flutter/flow/mutators_stack_unittests.cc FILE: ../../../flutter/flow/paint_region.cc FILE: ../../../flutter/flow/paint_region.h @@ -362,6 +359,8 @@ FILE: ../../../flutter/lib/ui/painting/color_filter.cc FILE: ../../../flutter/lib/ui/painting/color_filter.h FILE: ../../../flutter/lib/ui/painting/engine_layer.cc FILE: ../../../flutter/lib/ui/painting/engine_layer.h +FILE: ../../../flutter/lib/ui/painting/fragment_program.cc +FILE: ../../../flutter/lib/ui/painting/fragment_program.h FILE: ../../../flutter/lib/ui/painting/fragment_shader.cc FILE: ../../../flutter/lib/ui/painting/fragment_shader.h FILE: ../../../flutter/lib/ui/painting/gradient.cc @@ -488,6 +487,8 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/initialization.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart @@ -513,6 +514,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/vertices.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/viewport_metrics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/clipboard.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/color_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/configuration.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/engine_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_change_util.dart @@ -694,6 +696,7 @@ FILE: ../../../flutter/shell/common/canvas_spy_unittests.cc FILE: ../../../flutter/shell/common/context_options.cc FILE: ../../../flutter/shell/common/context_options.h FILE: ../../../flutter/shell/common/dart_native_benchmarks.cc +FILE: ../../../flutter/shell/common/display.cc FILE: ../../../flutter/shell/common/display.h FILE: ../../../flutter/shell/common/display_manager.cc FILE: ../../../flutter/shell/common/display_manager.h @@ -707,6 +710,7 @@ FILE: ../../../flutter/shell/common/persistent_cache_unittests.cc FILE: ../../../flutter/shell/common/pipeline.cc FILE: ../../../flutter/shell/common/pipeline.h FILE: ../../../flutter/shell/common/pipeline_unittests.cc +FILE: ../../../flutter/shell/common/platform_message_handler.h FILE: ../../../flutter/shell/common/platform_view.cc FILE: ../../../flutter/shell/common/platform_view.h FILE: ../../../flutter/shell/common/pointer_data_dispatcher.cc @@ -772,6 +776,8 @@ FILE: ../../../flutter/shell/platform/android/AndroidManifest.xml FILE: ../../../flutter/shell/platform/android/android_context_gl.cc FILE: ../../../flutter/shell/platform/android/android_context_gl.h FILE: ../../../flutter/shell/platform/android/android_context_gl_unittests.cc +FILE: ../../../flutter/shell/platform/android/android_display.cc +FILE: ../../../flutter/shell/platform/android/android_display.h FILE: ../../../flutter/shell/platform/android/android_environment_gl.cc FILE: ../../../flutter/shell/platform/android/android_environment_gl.h FILE: ../../../flutter/shell/platform/android/android_exports.lst @@ -819,6 +825,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Flutt FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSplashView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java @@ -840,6 +847,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/Flutte FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformTaskQueue.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/DeferredComponentManager.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java @@ -937,6 +945,8 @@ FILE: ../../../flutter/shell/platform/android/jni/jni_mock_unittest.cc FILE: ../../../flutter/shell/platform/android/jni/platform_view_android_jni.cc FILE: ../../../flutter/shell/platform/android/jni/platform_view_android_jni.h FILE: ../../../flutter/shell/platform/android/library_loader.cc +FILE: ../../../flutter/shell/platform/android/platform_message_handler_android.cc +FILE: ../../../flutter/shell/platform/android/platform_message_handler_android.h FILE: ../../../flutter/shell/platform/android/platform_message_response_android.cc FILE: ../../../flutter/shell/platform/android/platform_message_response_android.h FILE: ../../../flutter/shell/platform/android/platform_view_android.cc @@ -1022,6 +1032,9 @@ FILE: ../../../flutter/shell/platform/common/public/flutter_plugin_registrar.h FILE: ../../../flutter/shell/platform/common/public/flutter_texture_registrar.h FILE: ../../../flutter/shell/platform/common/test_accessibility_bridge.cc FILE: ../../../flutter/shell/platform/common/test_accessibility_bridge.h +FILE: ../../../flutter/shell/platform/common/text_editing_delta.cc +FILE: ../../../flutter/shell/platform/common/text_editing_delta.h +FILE: ../../../flutter/shell/platform/common/text_editing_delta_unittests.cc FILE: ../../../flutter/shell/platform/common/text_input_model.cc FILE: ../../../flutter/shell/platform/common/text_input_model.h FILE: ../../../flutter/shell/platform/common/text_input_model_unittests.cc @@ -1088,6 +1101,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterHeadlessDartRunner.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterIndirectScribbleDelegate.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyPrimaryResponder.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterKeySecondaryResponder.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h @@ -1112,9 +1126,6 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestora FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm -FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextEditingDelta.h -FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextEditingDelta.mm -FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextEditingDeltaTest.mm 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 @@ -1127,6 +1138,7 @@ 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.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewResponder.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/IOKit.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/KeyCodeMap.mm @@ -1413,9 +1425,11 @@ FILE: ../../../flutter/shell/platform/fuchsia/dart_runner/vmservice/meta/vmservi FILE: ../../../flutter/shell/platform/fuchsia/flutter/accessibility_bridge.cc FILE: ../../../flutter/shell/platform/fuchsia/flutter/accessibility_bridge.h FILE: ../../../flutter/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc -FILE: ../../../flutter/shell/platform/fuchsia/flutter/component.cc -FILE: ../../../flutter/shell/platform/fuchsia/flutter/component.h -FILE: ../../../flutter/shell/platform/fuchsia/flutter/component_unittest.cc +FILE: ../../../flutter/shell/platform/fuchsia/flutter/component_v1.cc +FILE: ../../../flutter/shell/platform/fuchsia/flutter/component_v1.h +FILE: ../../../flutter/shell/platform/fuchsia/flutter/component_v1_unittest.cc +FILE: ../../../flutter/shell/platform/fuchsia/flutter/component_v2.cc +FILE: ../../../flutter/shell/platform/fuchsia/flutter/component_v2.h FILE: ../../../flutter/shell/platform/fuchsia/flutter/engine.cc FILE: ../../../flutter/shell/platform/fuchsia/flutter/engine.h FILE: ../../../flutter/shell/platform/fuchsia/flutter/file_in_namespace_buffer.cc @@ -1495,6 +1509,7 @@ FILE: ../../../flutter/shell/platform/fuchsia/flutter/task_runner_adapter.h FILE: ../../../flutter/shell/platform/fuchsia/flutter/unique_fdio_ns.h FILE: ../../../flutter/shell/platform/fuchsia/flutter/vsync_waiter.cc FILE: ../../../flutter/shell/platform/fuchsia/flutter/vsync_waiter.h +FILE: ../../../flutter/shell/platform/fuchsia/flutter/vsync_waiter_unittest.cc FILE: ../../../flutter/shell/platform/fuchsia/flutter/vulkan_surface.cc FILE: ../../../flutter/shell/platform/fuchsia/flutter/vulkan_surface.h FILE: ../../../flutter/shell/platform/fuchsia/flutter/vulkan_surface_pool.cc @@ -1612,6 +1627,7 @@ FILE: ../../../flutter/shell/platform/linux/fl_platform_plugin.cc FILE: ../../../flutter/shell/platform/linux/fl_platform_plugin.h FILE: ../../../flutter/shell/platform/linux/fl_plugin_registrar.cc FILE: ../../../flutter/shell/platform/linux/fl_plugin_registrar_private.h +FILE: ../../../flutter/shell/platform/linux/fl_plugin_registrar_test.cc FILE: ../../../flutter/shell/platform/linux/fl_plugin_registry.cc FILE: ../../../flutter/shell/platform/linux/fl_renderer.cc FILE: ../../../flutter/shell/platform/linux/fl_renderer.h @@ -1750,7 +1766,9 @@ FILE: ../../../flutter/shell/platform/windows/system_utils.h FILE: ../../../flutter/shell/platform/windows/system_utils_unittests.cc FILE: ../../../flutter/shell/platform/windows/system_utils_win32.cc FILE: ../../../flutter/shell/platform/windows/system_utils_winuwp.cc +FILE: ../../../flutter/shell/platform/windows/task_runner.cc FILE: ../../../flutter/shell/platform/windows/task_runner.h +FILE: ../../../flutter/shell/platform/windows/task_runner_unittests.cc FILE: ../../../flutter/shell/platform/windows/task_runner_win32.cc FILE: ../../../flutter/shell/platform/windows/task_runner_win32.h FILE: ../../../flutter/shell/platform/windows/task_runner_win32_window.cc @@ -1783,6 +1801,7 @@ FILE: ../../../flutter/shell/profiling/sampling_profiler.h FILE: ../../../flutter/shell/profiling/sampling_profiler_unittest.cc FILE: ../../../flutter/shell/version/version.cc FILE: ../../../flutter/shell/version/version.h +FILE: ../../../flutter/shell/vmservice/empty.dart FILE: ../../../flutter/sky/tools/roll/patches/chromium/android_build.patch FILE: ../../../flutter/third_party/accessibility/base/color_utils.h FILE: ../../../flutter/third_party/accessibility/base/compiler_specific.h diff --git a/ci/licenses_golden/licenses_fuchsia b/ci/licenses_golden/licenses_fuchsia index 6634ec0fd254a..4653ad242c2b8 100644 --- a/ci/licenses_golden/licenses_fuchsia +++ b/ci/licenses_golden/licenses_fuchsia @@ -1,4 +1,4 @@ -Signature: 5519bc0896e1524c78ba4a2a7510b001 +Signature: 31268dc3c2c73523221d4ad2fd46d978 UNUSED LICENSES: @@ -65,7 +65,6 @@ FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/sem.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/setjmp.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/shm.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/signal.h -FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/socket.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/stat.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/statfs.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/termios.h @@ -152,6 +151,7 @@ FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/eventfd.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/fcntl.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/file.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/fsuid.h +FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/inotify.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/io.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/ioctl.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/ipc.h @@ -269,7 +269,6 @@ FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/sem.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/setjmp.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/shm.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/signal.h -FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/socket.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/stat.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/statfs.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/termios.h @@ -356,6 +355,7 @@ FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/eventfd.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/fcntl.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/file.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/fsuid.h +FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/inotify.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/io.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/ioctl.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/ipc.h @@ -435,6 +435,7 @@ FILE: ../../../fuchsia/sdk/linux/dart/zircon/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.gesture/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.semantics/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.virtualkeyboard/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.audio.effects/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.auth.oldtokens/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.auth/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.a2dp/meta.json @@ -456,23 +457,29 @@ 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.decl/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.runner/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.test/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.types/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.data/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.deprecatedtimezone/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.developer.tiles/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.diagnostics.types/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.diagnostics/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.driver.test/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.element/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.factory.wlan/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.factory/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.fonts/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.adc/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.light/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.power.statecontrol/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.radar/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hwinfo/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.images/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.input.report/meta.json @@ -485,6 +492,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.io2/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.kernel/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ldsvc/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.legacymetrics/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.lightsensor/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.location.namedplace/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.location.position/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.location.sensor/meta.json @@ -500,6 +508,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.sounds/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.target/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mediacodec/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mediastreams/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mem/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.memorypressure/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.metrics/meta.json @@ -510,7 +519,6 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.http/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.interfaces/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.mdns/meta.json -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.routes/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.power.profile/meta.json @@ -584,6 +592,7 @@ FILE: ../../../fuchsia/sdk/linux/pkg/scenic_cpp/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/svc/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/sync/meta.json +FILE: ../../../fuchsia/sdk/linux/pkg/sys_component_cpp_testing/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/sys_cpp/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/sys_cpp_testing/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/sys_inspect_cpp/meta.json @@ -607,6 +616,7 @@ FILE: ../../../fuchsia/sdk/linux/pkg/vulkan_layers/data/vulkan/explicit_layer.d/ FILE: ../../../fuchsia/sdk/linux/pkg/vulkan_layers/data/vulkan/explicit_layer.d/VkLayer_khronos_validation.json FILE: ../../../fuchsia/sdk/linux/pkg/vulkan_layers/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/zx/meta.json +FILE: ../../../fuchsia/sdk/linux/version_history.json ---------------------------------------------------------------------------------------------------- musl as a whole is licensed under the following standard MIT license: @@ -796,7 +806,6 @@ FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/sem.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/setjmp.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/shm.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/signal.h -FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/socket.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/stat.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/statfs.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/termios.h @@ -883,6 +892,7 @@ FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/eventfd.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/fcntl.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/file.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/fsuid.h +FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/inotify.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/io.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/ioctl.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/ipc.h @@ -1000,7 +1010,6 @@ FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/sem.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/setjmp.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/shm.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/signal.h -FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/socket.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/stat.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/statfs.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/termios.h @@ -1087,6 +1096,7 @@ FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/eventfd.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/fcntl.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/file.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/fsuid.h +FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/inotify.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/io.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/ioctl.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/ipc.h @@ -1166,6 +1176,7 @@ FILE: ../../../fuchsia/sdk/linux/dart/zircon/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.gesture/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.semantics/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.virtualkeyboard/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.audio.effects/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.auth.oldtokens/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.auth/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.a2dp/meta.json @@ -1187,23 +1198,29 @@ 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.decl/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.runner/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.test/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.types/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.data/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.deprecatedtimezone/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.developer.tiles/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.diagnostics.types/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.diagnostics/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.driver.test/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.element/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.factory.wlan/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.factory/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.fonts/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.adc/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.light/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.power.statecontrol/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.radar/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hwinfo/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.images/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.input.report/meta.json @@ -1216,6 +1233,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.io2/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.kernel/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ldsvc/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.legacymetrics/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.lightsensor/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.location.namedplace/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.location.position/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.location.sensor/meta.json @@ -1231,6 +1249,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.sounds/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.target/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mediacodec/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mediastreams/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mem/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.memorypressure/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.metrics/meta.json @@ -1241,7 +1260,6 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.http/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.interfaces/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.mdns/meta.json -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.routes/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.power.profile/meta.json @@ -1315,6 +1333,7 @@ FILE: ../../../fuchsia/sdk/linux/pkg/scenic_cpp/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/svc/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/sync/meta.json +FILE: ../../../fuchsia/sdk/linux/pkg/sys_component_cpp_testing/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/sys_cpp/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/sys_cpp_testing/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/sys_inspect_cpp/meta.json @@ -1338,6 +1357,7 @@ FILE: ../../../fuchsia/sdk/linux/pkg/vulkan_layers/data/vulkan/explicit_layer.d/ FILE: ../../../fuchsia/sdk/linux/pkg/vulkan_layers/data/vulkan/explicit_layer.d/VkLayer_khronos_validation.json FILE: ../../../fuchsia/sdk/linux/pkg/vulkan_layers/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/zx/meta.json +FILE: ../../../fuchsia/sdk/linux/version_history.json ---------------------------------------------------------------------------------------------------- Copyright 2019 The Fuchsia Authors. @@ -1424,7 +1444,6 @@ FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/sem.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/setjmp.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/shm.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/signal.h -FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/socket.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/stat.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/statfs.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/bits/termios.h @@ -1511,6 +1530,7 @@ FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/eventfd.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/fcntl.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/file.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/fsuid.h +FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/inotify.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/io.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/ioctl.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/sys/ipc.h @@ -1628,7 +1648,6 @@ FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/sem.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/setjmp.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/shm.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/signal.h -FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/socket.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/stat.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/statfs.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/bits/termios.h @@ -1715,6 +1734,7 @@ FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/eventfd.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/fcntl.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/file.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/fsuid.h +FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/inotify.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/io.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/ioctl.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/sys/ipc.h @@ -1794,6 +1814,7 @@ FILE: ../../../fuchsia/sdk/linux/dart/zircon/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.gesture/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.semantics/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.virtualkeyboard/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.audio.effects/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.auth.oldtokens/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.auth/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.a2dp/meta.json @@ -1815,23 +1836,29 @@ 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.decl/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.runner/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.test/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.types/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.data/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.deprecatedtimezone/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.developer.tiles/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.diagnostics.types/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.diagnostics/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.driver.test/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.element/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.factory.wlan/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.factory/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.fonts/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.adc/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.light/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.power.statecontrol/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.radar/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hwinfo/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.images/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.input.report/meta.json @@ -1844,6 +1871,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.io2/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.kernel/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ldsvc/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.legacymetrics/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.lightsensor/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.location.namedplace/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.location.position/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.location.sensor/meta.json @@ -1859,6 +1887,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.sounds/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media.target/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.media/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mediacodec/meta.json +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mediastreams/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mem/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.memorypressure/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.metrics/meta.json @@ -1869,7 +1898,6 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.http/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.interfaces/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.mdns/meta.json -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.routes/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net/meta.json FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.power.profile/meta.json @@ -1943,6 +1971,7 @@ FILE: ../../../fuchsia/sdk/linux/pkg/scenic_cpp/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/svc/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/sync/meta.json +FILE: ../../../fuchsia/sdk/linux/pkg/sys_component_cpp_testing/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/sys_cpp/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/sys_cpp_testing/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/sys_inspect_cpp/meta.json @@ -1966,6 +1995,7 @@ FILE: ../../../fuchsia/sdk/linux/pkg/vulkan_layers/data/vulkan/explicit_layer.d/ FILE: ../../../fuchsia/sdk/linux/pkg/vulkan_layers/data/vulkan/explicit_layer.d/VkLayer_khronos_validation.json FILE: ../../../fuchsia/sdk/linux/pkg/vulkan_layers/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/zx/meta.json +FILE: ../../../fuchsia/sdk/linux/version_history.json ---------------------------------------------------------------------------------------------------- The majority of files in this project use the Apache 2.0 License. There are a few exceptions and their license can be found in the source. @@ -2190,6 +2220,142 @@ The above copyright notice and this permission notice shall be included in all c 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 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ==================================================================================================== +==================================================================================================== +LIBRARY: fuchsia_sdk +ORIGIN: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/availability.h + ../../../fuchsia/sdk/linux/LICENSE +TYPE: LicenseType.bsd +FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/availability.h +FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/availability.h +FILE: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/codegen_common.dart +FILE: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/wire_format.dart +FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/reader.dart +FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/src/reader/diagnostic_config.dart +FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/src/reader/diagnostic_data.dart +FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/src/reader/reader.dart +FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_scenic/lib/src/scenic_context.dart +FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_scenic_flutter/lib/src/focus_state.dart +FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_scenic_flutter/lib/src/fuchsia_views_service.dart +FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/trace_processing/metrics/camera_metrics.dart +FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/virtual_camera.dart +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.virtualkeyboard/virtual_keyboard.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.audio.effects/creator.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.audio.effects/processor.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.gatt2/client.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.gatt2/constants.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.gatt2/types.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.decl/capability.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.decl/child.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.decl/collection.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.decl/component.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.decl/environment.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.decl/events.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.decl/expose.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.decl/offer.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.decl/program.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.decl/relative_refs.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.decl/types.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.decl/use.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component.test/realm_builder.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/binder.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/realm.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.diagnostics.types/component.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.diagnostics/log_settings.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.driver.test/realm.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/data_provider_controller.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/codec_connect.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/dai_connect.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/health.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/goldfish_sync.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/port.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.radar/radar.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.input.virtualkeyboard/virtual_keyboard.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.input/keymap.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.intl/calendar.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.intl/time_zones.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.io/rights-abilities.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.io2/inotify.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.kernel/cpu-resource.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.lightsensor/calibrator.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.lightsensor/sensor.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.lightsensor/types.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mediastreams/audio_format.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mediastreams/compression.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mediastreams/encryption.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mediastreams/media_format.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.mediastreams/video_format.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.power.profile/profile.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.settings/keyboard.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.composition/allocator.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.composition/flatland.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.composition/screenshot.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.views/flatland_tokens.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.common/wlan_common.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.ieee80211/constants.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.ieee80211/rsn.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.policy/types.fidl +FILE: ../../../fuchsia/sdk/linux/pkg/fdio/include/lib/fdio/inotify.h +FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/include/lib/fidl/transformer.h +FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/transformer.cc +FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/barrier.h +FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/bridge.h +FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/promise.h +FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/result.h +FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/scheduler.h +FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/scope.h +FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/sequencer.h +FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/single_threaded_executor.h +FILE: ../../../fuchsia/sdk/linux/pkg/scenic_cpp/include/lib/ui/scenic/cpp/view_identity.h +FILE: ../../../fuchsia/sdk/linux/pkg/scenic_cpp/view_identity.cc +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/algorithm.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/atomic.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/cstddef.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/functional.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/algorithm.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/atomic.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/exception.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/functional.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/span.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/tuple.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/type_traits.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/iterator.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/span.h +FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/tuple.h +FILE: ../../../fuchsia/sdk/linux/pkg/sys_component_cpp_testing/internal/errors.cc +FILE: ../../../fuchsia/sdk/linux/pkg/sys_component_cpp_testing/internal/mock_runner.cc +FILE: ../../../fuchsia/sdk/linux/pkg/sys_component_cpp_testing/internal/realm.cc +FILE: ../../../fuchsia/sdk/linux/pkg/sys_component_cpp_testing/realm_builder.cc +FILE: ../../../fuchsia/sdk/linux/pkg/sys_component_cpp_testing/realm_builder_types.cc +FILE: ../../../fuchsia/sdk/linux/pkg/sys_component_cpp_testing/scoped_child.cc +FILE: ../../../fuchsia/sdk/linux/pkg/syslog_structured_backend/fuchsia_syslog.cc +FILE: ../../../fuchsia/sdk/linux/pkg/syslog_structured_backend/include/lib/syslog/structured_backend/cpp/fuchsia_syslog.h +FILE: ../../../fuchsia/sdk/linux/pkg/syslog_structured_backend/include/lib/syslog/structured_backend/fuchsia_syslog.h +---------------------------------------------------------------------------------------------------- +Copyright 2021 The Fuchsia 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. + +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. +==================================================================================================== + ==================================================================================================== LIBRARY: fuchsia_sdk ORIGIN: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/boot/bootfs.h + ../../../fuchsia/sdk/linux/LICENSE @@ -2362,7 +2528,6 @@ FILE: ../../../fuchsia/sdk/linux/pkg/fidl_cpp/include/lib/fidl/cpp/internal/prox FILE: ../../../fuchsia/sdk/linux/pkg/fidl_cpp/include/lib/fidl/cpp/internal/stub.h FILE: ../../../fuchsia/sdk/linux/pkg/fidl_cpp/include/lib/fidl/cpp/internal/stub_controller.h FILE: ../../../fuchsia/sdk/linux/pkg/fidl_cpp/include/lib/fidl/cpp/internal/weak_stub_controller.h -FILE: ../../../fuchsia/sdk/linux/pkg/fidl_cpp/include/lib/fidl/cpp/optional.h FILE: ../../../fuchsia/sdk/linux/pkg/fidl_cpp/include/lib/fidl/cpp/thread_safe_binding_set.h FILE: ../../../fuchsia/sdk/linux/pkg/fidl_cpp/include/lib/fidl/cpp/type_converter.h FILE: ../../../fuchsia/sdk/linux/pkg/fidl_cpp/internal/message_handler.cc @@ -2508,10 +2673,12 @@ TYPE: LicenseType.bsd FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/analyzer.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/boot/crash-reason.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/string_view.h +FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/syscalls-next.h FILE: ../../../fuchsia/sdk/linux/arch/arm64/sysroot/include/zircon/testonly-syscalls.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/analyzer.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/boot/crash-reason.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/string_view.h +FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/syscalls-next.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/testonly-syscalls.h FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_scenic_flutter/lib/fuchsia_view.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_scenic_flutter/lib/src/fuchsia_view.dart @@ -2546,6 +2713,7 @@ 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.types/constants.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 @@ -2556,6 +2724,7 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.factory.wlan/iovar.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/crash_register.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/data_register.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/last_reboot_info.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.adc/adc.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/codec.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/dai.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/dai_format.fidl @@ -2931,7 +3100,6 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/session_shell.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_controller.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_info.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_provider.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/url_body.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.sys/component_controller.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.sys/environment.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.sys/environment_controller.fidl @@ -3023,7 +3191,7 @@ FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/syscalls/intern FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/syscalls/scheduler.h FILE: ../../../fuchsia/sdk/linux/arch/x64/sysroot/include/zircon/utc.h FILE: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/bits.dart -FILE: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/xunion.dart +FILE: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/union.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/inspect.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/src/inspect/inspect.dart FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/src/inspect/internal/_inspect_impl.dart @@ -3192,7 +3360,6 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular.session/modular_config.fid FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular.testing/test_harness.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/annotation.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.modular/story_shell_factory.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net/namelookup.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.recovery.ui/countdown.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.recovery/factory_reset.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.scenic.scheduling/prediction_info.fidl @@ -3414,102 +3581,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== -==================================================================================================== -LIBRARY: fuchsia_sdk -ORIGIN: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/codegen_common.dart + ../../../fuchsia/sdk/linux/LICENSE -TYPE: LicenseType.bsd -FILE: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/codegen_common.dart -FILE: ../../../fuchsia/sdk/linux/dart/fidl/lib/src/wire_format.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/reader.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/src/reader/diagnostic_config.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/src/reader/diagnostic_data.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_inspect/lib/src/reader/reader.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_scenic/lib/src/scenic_context.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_scenic_flutter/lib/src/focus_state.dart -FILE: ../../../fuchsia/sdk/linux/dart/fuchsia_scenic_flutter/lib/src/fuchsia_views_service.dart -FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/trace_processing/metrics/camera_metrics.dart -FILE: ../../../fuchsia/sdk/linux/dart/sl4f/lib/src/virtual_camera.dart -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.accessibility.virtualkeyboard/virtual_keyboard.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.gatt2/client.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.gatt2/constants.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.bluetooth.gatt2/types.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/binder.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.diagnostics.types/component.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/data_provider_controller.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.audio/dai_connect.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.goldfish/goldfish_sync.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.hardware.network/port.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.input.virtualkeyboard/virtual_keyboard.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.intl/calendar.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.intl/time_zones.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.io/rights-abilities.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.io2/inotify.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.power.profile/profile.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.composition/allocator.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.composition/flatland.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.composition/screenshot.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.ui.views/flatland_tokens.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.common/wlan_common.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.ieee80211/constants.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.ieee80211/rsn.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.wlan.policy/types.fidl -FILE: ../../../fuchsia/sdk/linux/pkg/fdio/include/lib/fdio/inotify.h -FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/include/lib/fidl/transformer.h -FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/transformer.cc -FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/barrier.h -FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/bridge.h -FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/promise.h -FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/result.h -FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/scheduler.h -FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/scope.h -FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/sequencer.h -FILE: ../../../fuchsia/sdk/linux/pkg/fit-promise/include/lib/fit/single_threaded_executor.h -FILE: ../../../fuchsia/sdk/linux/pkg/scenic_cpp/include/lib/ui/scenic/cpp/view_identity.h -FILE: ../../../fuchsia/sdk/linux/pkg/scenic_cpp/view_identity.cc -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/algorithm.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/atomic.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/cstddef.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/functional.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/algorithm.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/atomic.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/exception.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/functional.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/span.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/tuple.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/internal/type_traits.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/iterator.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/span.h -FILE: ../../../fuchsia/sdk/linux/pkg/stdcompat/include/lib/stdcompat/tuple.h -FILE: ../../../fuchsia/sdk/linux/pkg/syslog_structured_backend/fuchsia_syslog.cc -FILE: ../../../fuchsia/sdk/linux/pkg/syslog_structured_backend/include/lib/syslog/structured_backend/cpp/fuchsia_syslog.h -FILE: ../../../fuchsia/sdk/linux/pkg/syslog_structured_backend/include/lib/syslog/structured_backend/fuchsia_syslog.h ----------------------------------------------------------------------------------------------------- -Copyright 2021 The Fuchsia 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. - -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. -==================================================================================================== - ==================================================================================================== LIBRARY: fuchsia_sdk ORIGIN: ../../../fuchsia/sdk/linux/dart/fuchsia_services/lib/src/component_context.dart + ../../../LICENSE @@ -3578,43 +3649,6 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== -==================================================================================================== -LIBRARY: fuchsia_sdk -ORIGIN: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/http_error.fidl + ../../../fuchsia/sdk/linux/LICENSE -TYPE: LicenseType.bsd -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/http_error.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/http_header.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/http_service.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/url_loader.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/url_request.fidl -FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.net.oldhttp/url_response.fidl ----------------------------------------------------------------------------------------------------- -Copyright 2015 The Fuchsia 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. - -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. -==================================================================================================== - ==================================================================================================== LIBRARY: fuchsia_sdk ORIGIN: ../../../fuchsia/sdk/linux/fidl/fuchsia.sys.test/cache.fidl + ../../../LICENSE @@ -3683,4 +3717,4 @@ 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: 17 +Total license count: 16 diff --git a/ci/licenses_golden/licenses_skia b/ci/licenses_golden/licenses_skia index c85f637e2756a..5957337f73433 100644 --- a/ci/licenses_golden/licenses_skia +++ b/ci/licenses_golden/licenses_skia @@ -1,4 +1,4 @@ -Signature: ad4c62a38ae1b63a3bc18684ef1b0732 +Signature: ace178a43d63b9d3acfb38736c07c8a2 UNUSED LICENSES: @@ -169,6 +169,39 @@ reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ==================================================================================================== +==================================================================================================== +ORIGIN: ../../../third_party/skia/third_party/musl_compat/LICENSE +TYPE: LicenseType.bsd +---------------------------------------------------------------------------------------------------- +Copyright (c) 2021 Google LLC 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. +==================================================================================================== + ==================================================================================================== ORIGIN: ../../../third_party/skia/third_party/vulkanmemoryallocator/include/LICENSE.txt TYPE: LicenseType.mit @@ -485,6 +518,41 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== +==================================================================================================== +LIBRARY: musl_compat +ORIGIN: ../../../third_party/skia/third_party/musl_compat/locale.c + ../../../third_party/skia/third_party/musl_compat/LICENSE +TYPE: LicenseType.bsd +FILE: ../../../third_party/skia/third_party/musl_compat/locale.c +---------------------------------------------------------------------------------------------------- +Copyright 2021 Google LLC + +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. +==================================================================================================== + ==================================================================================================== LIBRARY: skcms LIBRARY: vulkanmemoryallocator @@ -600,7 +668,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. LIBRARY: skia ORIGIN: ../../../third_party/skia/LICENSE TYPE: LicenseType.bsd -FILE: ../../../third_party/skia/.bazelignore +FILE: ../../../third_party/skia/.bazelrc +FILE: ../../../third_party/skia/.bazelversion FILE: ../../../third_party/skia/.clang-tidy FILE: ../../../third_party/skia/BUILD.bazel FILE: ../../../third_party/skia/CQ_COMMITTERS @@ -618,7 +687,9 @@ FILE: ../../../third_party/skia/animations/paths#1.xml FILE: ../../../third_party/skia/animations/redcross#1.jpg FILE: ../../../third_party/skia/animations/text#1.xml FILE: ../../../third_party/skia/bazel/BUILD.bazel -FILE: ../../../third_party/skia/bazel/libpng.bazel +FILE: ../../../third_party/skia/bazel/common_config_settings/BUILD.bazel +FILE: ../../../third_party/skia/bazel/common_config_settings/defs.bzl +FILE: ../../../third_party/skia/bazel/macros.bzl FILE: ../../../third_party/skia/bench/microbench.json FILE: ../../../third_party/skia/bench/skpbench.json FILE: ../../../third_party/skia/build/fuchsia/skqp/skqp.cmx @@ -652,6 +723,7 @@ FILE: ../../../third_party/skia/docker/skia-release/Dockerfile FILE: ../../../third_party/skia/docker/skia-wasm-release/Dockerfile FILE: ../../../third_party/skia/docker/skia-with-swift-shader-base/Dockerfile FILE: ../../../third_party/skia/docker/skia-with-swift-shader-base/build-with-swift-shader-and-run +FILE: ../../../third_party/skia/experimental/bazel_test/BUILD.bazel FILE: ../../../third_party/skia/experimental/docs/animationCommon.js FILE: ../../../third_party/skia/experimental/docs/backend.js FILE: ../../../third_party/skia/experimental/docs/canvasBackend.js @@ -723,11 +795,24 @@ FILE: ../../../third_party/skia/experimental/wasm-skp-debugger/gpu.js FILE: ../../../third_party/skia/experimental/wasm-skp-debugger/helper.js FILE: ../../../third_party/skia/experimental/wasm-skp-debugger/karma.conf.js FILE: ../../../third_party/skia/experimental/wasm-skp-debugger/package.json -FILE: ../../../third_party/skia/experimental/webgpu-bazel/WORKSPACE FILE: ../../../third_party/skia/experimental/webgpu-bazel/example/index.html FILE: ../../../third_party/skia/experimental/webgpu-bazel/src/BUILD +FILE: ../../../third_party/skia/gm/BUILD.bazel FILE: ../../../third_party/skia/go.mod FILE: ../../../third_party/skia/go.sum +FILE: ../../../third_party/skia/include/android/BUILD.bazel +FILE: ../../../third_party/skia/include/codec/BUILD.bazel +FILE: ../../../third_party/skia/include/config/BUILD.bazel +FILE: ../../../third_party/skia/include/core/BUILD.bazel +FILE: ../../../third_party/skia/include/effects/BUILD.bazel +FILE: ../../../third_party/skia/include/encode/BUILD.bazel +FILE: ../../../third_party/skia/include/gpu/BUILD.bazel +FILE: ../../../third_party/skia/include/pathops/BUILD.bazel +FILE: ../../../third_party/skia/include/ports/BUILD.bazel +FILE: ../../../third_party/skia/include/private/BUILD.bazel +FILE: ../../../third_party/skia/include/sksl/BUILD.bazel +FILE: ../../../third_party/skia/include/third_party/BUILD.bazel +FILE: ../../../third_party/skia/include/utils/BUILD.bazel FILE: ../../../third_party/skia/infra/bots/assets/android_ndk_darwin/VERSION FILE: ../../../third_party/skia/infra/bots/assets/android_ndk_linux/VERSION FILE: ../../../third_party/skia/infra/bots/assets/android_ndk_windows/VERSION @@ -780,6 +865,7 @@ 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-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-Debug-Android_HWASAN.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_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 @@ -790,9 +876,6 @@ 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-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-V1andV2.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-V1only.json -FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian10-Clang-x86_64-Debug-V2only.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 @@ -817,6 +900,7 @@ 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-Debian9-Clang-x86_64-Release-Chromebook_GLES_Docker.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-Graphite.json +FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Mac-Clang-arm64-Debug-Graphite_NoGpu.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-arm64-Release-Graphite.json FILE: ../../../third_party/skia/infra/bots/recipe_modules/build/examples/full.expected/Build-Mac-Clang-x86_64-Debug-ASAN.json @@ -973,10 +1057,21 @@ FILE: ../../../third_party/skia/infra/wasm-common/docker/emsdk-base/Dockerfile FILE: ../../../third_party/skia/infra/wasm-common/docker/gold-karma-chrome-tests/Dockerfile FILE: ../../../third_party/skia/infra/wasm-common/docker/karma-chrome-tests/Dockerfile FILE: ../../../third_party/skia/infra/wasm-common/docker/perf-karma-chrome-tests/Dockerfile +FILE: ../../../third_party/skia/modules/canvaskit/BUILD.bazel FILE: ../../../third_party/skia/modules/canvaskit/catchExceptionNop.js FILE: ../../../third_party/skia/modules/canvaskit/color.js FILE: ../../../third_party/skia/modules/canvaskit/cpu.js FILE: ../../../third_party/skia/modules/canvaskit/debug.js +FILE: ../../../third_party/skia/modules/canvaskit/external_test/typescript_browser/index.html +FILE: ../../../third_party/skia/modules/canvaskit/external_test/typescript_browser/module_uses_ck.ts +FILE: ../../../third_party/skia/modules/canvaskit/external_test/typescript_browser/package-lock.json +FILE: ../../../third_party/skia/modules/canvaskit/external_test/typescript_browser/package.json +FILE: ../../../third_party/skia/modules/canvaskit/external_test/typescript_browser/tsconfig.json +FILE: ../../../third_party/skia/modules/canvaskit/external_test/typescript_browser_es6/index.html +FILE: ../../../third_party/skia/modules/canvaskit/external_test/typescript_browser_es6/module_uses_ck.ts +FILE: ../../../third_party/skia/modules/canvaskit/external_test/typescript_browser_es6/package-lock.json +FILE: ../../../third_party/skia/modules/canvaskit/external_test/typescript_browser_es6/package.json +FILE: ../../../third_party/skia/modules/canvaskit/external_test/typescript_browser_es6/tsconfig.json FILE: ../../../third_party/skia/modules/canvaskit/externs.js FILE: ../../../third_party/skia/modules/canvaskit/font.js FILE: ../../../third_party/skia/modules/canvaskit/fonts/NotoMono-Regular.ttf @@ -1012,6 +1107,7 @@ FILE: ../../../third_party/skia/modules/canvaskit/rt_shader.js FILE: ../../../third_party/skia/modules/canvaskit/skottie.js FILE: ../../../third_party/skia/modules/canvaskit/skp.js FILE: ../../../third_party/skia/modules/canvaskit/util.js +FILE: ../../../third_party/skia/modules/canvaskit/wasm_tools/gms.html FILE: ../../../third_party/skia/modules/canvaskit/wasm_tools/viewer.html FILE: ../../../third_party/skia/modules/pathkit/chaining.js FILE: ../../../third_party/skia/modules/pathkit/externs.js @@ -1095,9 +1191,22 @@ FILE: ../../../third_party/skia/site/featured-background.png FILE: ../../../third_party/skia/specs/web-img-decode/current/index.html FILE: ../../../third_party/skia/specs/web-img-decode/proposed/impl/impl.js FILE: ../../../third_party/skia/specs/web-img-decode/proposed/index.html +FILE: ../../../third_party/skia/src/android/BUILD.bazel +FILE: ../../../third_party/skia/src/codec/BUILD.bazel +FILE: ../../../third_party/skia/src/core/BUILD.bazel FILE: ../../../third_party/skia/src/core/SkOrderedReadBuffer.h +FILE: ../../../third_party/skia/src/effects/BUILD.bazel +FILE: ../../../third_party/skia/src/gpu/BUILD.bazel +FILE: ../../../third_party/skia/src/image/BUILD.bazel +FILE: ../../../third_party/skia/src/images/BUILD.bazel +FILE: ../../../third_party/skia/src/opts/BUILD.bazel +FILE: ../../../third_party/skia/src/pathops/BUILD.bazel +FILE: ../../../third_party/skia/src/ports/BUILD.bazel FILE: ../../../third_party/skia/src/ports/SkTLS_pthread.cpp FILE: ../../../third_party/skia/src/ports/SkTLS_win.cpp +FILE: ../../../third_party/skia/src/sfnt/BUILD.bazel +FILE: ../../../third_party/skia/src/shaders/BUILD.bazel +FILE: ../../../third_party/skia/src/sksl/BUILD.bazel FILE: ../../../third_party/skia/src/sksl/generated/sksl_frag.dehydrated.sksl FILE: ../../../third_party/skia/src/sksl/generated/sksl_gpu.dehydrated.sksl FILE: ../../../third_party/skia/src/sksl/generated/sksl_interp.dehydrated.sksl @@ -1111,6 +1220,11 @@ FILE: ../../../third_party/skia/src/sksl/sksl_gpu.sksl FILE: ../../../third_party/skia/src/sksl/sksl_public.sksl FILE: ../../../third_party/skia/src/sksl/sksl_rt_shader.sksl FILE: ../../../third_party/skia/src/sksl/sksl_vert.sksl +FILE: ../../../third_party/skia/src/utils/BUILD.bazel +FILE: ../../../third_party/skia/third_party/BUILD.bazel +FILE: ../../../third_party/skia/toolchain/BUILD.bazel +FILE: ../../../third_party/skia/toolchain/build_toolchain.bzl +FILE: ../../../third_party/skia/toolchain/clang_toolchain_config.bzl ---------------------------------------------------------------------------------------------------- Copyright (c) 2011 Google Inc. All rights reserved. @@ -1942,8 +2056,6 @@ FILE: ../../../third_party/skia/src/shaders/gradients/Sk4fLinearGradient.h FILE: ../../../third_party/skia/src/sksl/SkSLCompiler.cpp FILE: ../../../third_party/skia/src/sksl/SkSLCompiler.h FILE: ../../../third_party/skia/src/sksl/SkSLContext.h -FILE: ../../../third_party/skia/src/sksl/SkSLIRGenerator.cpp -FILE: ../../../third_party/skia/src/sksl/SkSLIRGenerator.h FILE: ../../../third_party/skia/src/sksl/SkSLMain.cpp FILE: ../../../third_party/skia/src/sksl/SkSLMemoryLayout.h FILE: ../../../third_party/skia/src/sksl/SkSLProgramSettings.h @@ -2246,200 +2358,458 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== LIBRARY: skia -ORIGIN: ../../../third_party/skia/bench/BigPathBench.cpp + ../../../third_party/skia/LICENSE +ORIGIN: ../../../third_party/skia/bench/BigPath.cpp + ../../../third_party/skia/LICENSE TYPE: LicenseType.bsd -FILE: ../../../third_party/skia/bench/BigPathBench.cpp -FILE: ../../../third_party/skia/bench/BitmapRegionDecoderBench.cpp -FILE: ../../../third_party/skia/bench/BitmapRegionDecoderBench.h -FILE: ../../../third_party/skia/bench/CodecBench.cpp -FILE: ../../../third_party/skia/bench/CodecBench.h -FILE: ../../../third_party/skia/bench/CodecBenchPriv.h -FILE: ../../../third_party/skia/bench/ControlBench.cpp -FILE: ../../../third_party/skia/bench/DrawBitmapAABench.cpp -FILE: ../../../third_party/skia/bench/ImageBench.cpp -FILE: ../../../third_party/skia/bench/InterpBench.cpp -FILE: ../../../third_party/skia/bench/MathBench.cpp -FILE: ../../../third_party/skia/bench/MipmapBench.cpp -FILE: ../../../third_party/skia/bench/PictureOverheadBench.cpp -FILE: ../../../third_party/skia/bench/SKPAnimationBench.cpp -FILE: ../../../third_party/skia/bench/SKPAnimationBench.h -FILE: ../../../third_party/skia/bench/Sk4fBench.cpp -FILE: ../../../third_party/skia/bench/SkGlyphCacheBench.cpp -FILE: ../../../third_party/skia/bench/StrokeBench.cpp -FILE: ../../../third_party/skia/bench/TextBlobBench.cpp -FILE: ../../../third_party/skia/bench/TopoSortBench.cpp -FILE: ../../../third_party/skia/bench/nanobench.h -FILE: ../../../third_party/skia/client_utils/android/BRDAllocator.h -FILE: ../../../third_party/skia/client_utils/android/BitmapRegionDecoder.cpp -FILE: ../../../third_party/skia/client_utils/android/BitmapRegionDecoder.h -FILE: ../../../third_party/skia/client_utils/android/BitmapRegionDecoderPriv.h -FILE: ../../../third_party/skia/dm/DMSrcSink.cpp -FILE: ../../../third_party/skia/dm/DMSrcSink.h -FILE: ../../../third_party/skia/experimental/c-api-example/skia-c-example.c -FILE: ../../../third_party/skia/experimental/tools/coreGraphicsPdf2png.cpp -FILE: ../../../third_party/skia/gm/aaxfermodes.cpp -FILE: ../../../third_party/skia/gm/addarc.cpp -FILE: ../../../third_party/skia/gm/all_bitmap_configs.cpp -FILE: ../../../third_party/skia/gm/anisotropic.cpp -FILE: ../../../third_party/skia/gm/annotated_text.cpp -FILE: ../../../third_party/skia/gm/badpaint.cpp -FILE: ../../../third_party/skia/gm/bigrrectaaeffect.cpp -FILE: ../../../third_party/skia/gm/bigtileimagefilter.cpp -FILE: ../../../third_party/skia/gm/blend.cpp -FILE: ../../../third_party/skia/gm/blurredclippedcircle.cpp -FILE: ../../../third_party/skia/gm/bmpfilterqualityrepeat.cpp -FILE: ../../../third_party/skia/gm/concavepaths.cpp -FILE: ../../../third_party/skia/gm/constcolorprocessor.cpp -FILE: ../../../third_party/skia/gm/convex_all_line_paths.cpp -FILE: ../../../third_party/skia/gm/draw_bitmap_rect_skbug4374.cpp -FILE: ../../../third_party/skia/gm/drawable.cpp -FILE: ../../../third_party/skia/gm/drawatlas.cpp -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/image_pict.cpp -FILE: ../../../third_party/skia/gm/image_shader.cpp -FILE: ../../../third_party/skia/gm/imagefilters.cpp -FILE: ../../../third_party/skia/gm/imagefiltersstroked.cpp -FILE: ../../../third_party/skia/gm/imagefilterstransformed.cpp -FILE: ../../../third_party/skia/gm/imagefromyuvtextures.cpp -FILE: ../../../third_party/skia/gm/imagescalealigned.cpp -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/localmatriximagefilter.cpp -FILE: ../../../third_party/skia/gm/localmatriximageshader.cpp -FILE: ../../../third_party/skia/gm/mipmap.cpp -FILE: ../../../third_party/skia/gm/path_stroke_with_zero_length.cpp -FILE: ../../../third_party/skia/gm/pathcontourstart.cpp -FILE: ../../../third_party/skia/gm/pdf_never_embed.cpp -FILE: ../../../third_party/skia/gm/perspshaders.cpp -FILE: ../../../third_party/skia/gm/pictureimagegenerator.cpp -FILE: ../../../third_party/skia/gm/pixelsnap.cpp -FILE: ../../../third_party/skia/gm/plus.cpp -FILE: ../../../third_party/skia/gm/repeated_bitmap.cpp -FILE: ../../../third_party/skia/gm/scaledstrokes.cpp -FILE: ../../../third_party/skia/gm/skbug_257.cpp -FILE: ../../../third_party/skia/gm/smallpaths.cpp -FILE: ../../../third_party/skia/gm/stlouisarch.cpp -FILE: ../../../third_party/skia/gm/textblobcolortrans.cpp -FILE: ../../../third_party/skia/gm/textblobgeometrychange.cpp -FILE: ../../../third_party/skia/gm/textblobmixedsizes.cpp -FILE: ../../../third_party/skia/gm/textblobrandomfont.cpp -FILE: ../../../third_party/skia/gm/textblobtransforms.cpp -FILE: ../../../third_party/skia/gm/textblobuseaftergpufree.cpp -FILE: ../../../third_party/skia/gm/transparency.cpp -FILE: ../../../third_party/skia/gm/xform.cpp -FILE: ../../../third_party/skia/include/codec/SkAndroidCodec.h -FILE: ../../../third_party/skia/include/codec/SkCodec.h -FILE: ../../../third_party/skia/include/core/SkEncodedImageFormat.h -FILE: ../../../third_party/skia/include/core/SkPathBuilder.h -FILE: ../../../third_party/skia/include/core/SkPixmap.h -FILE: ../../../third_party/skia/include/core/SkPngChunkReader.h -FILE: ../../../third_party/skia/include/core/SkPoint3.h -FILE: ../../../third_party/skia/include/core/SkRSXform.h -FILE: ../../../third_party/skia/include/core/SkTraceMemoryDump.h -FILE: ../../../third_party/skia/include/effects/SkTableColorFilter.h -FILE: ../../../third_party/skia/include/gpu/GrContextOptions.h -FILE: ../../../third_party/skia/include/gpu/gl/GrGLTypes.h -FILE: ../../../third_party/skia/include/ports/SkFontMgr_android.h -FILE: ../../../third_party/skia/include/ports/SkFontMgr_directory.h -FILE: ../../../third_party/skia/include/ports/SkFontMgr_empty.h -FILE: ../../../third_party/skia/include/ports/SkFontMgr_fontconfig.h -FILE: ../../../third_party/skia/include/private/SkMutex.h -FILE: ../../../third_party/skia/include/private/SkNx.h -FILE: ../../../third_party/skia/include/private/SkNx_neon.h -FILE: ../../../third_party/skia/include/private/SkNx_sse.h -FILE: ../../../third_party/skia/include/private/SkSemaphore.h -FILE: ../../../third_party/skia/include/private/SkSpinlock.h -FILE: ../../../third_party/skia/include/private/SkTHash.h -FILE: ../../../third_party/skia/include/private/SkThreadID.h -FILE: ../../../third_party/skia/include/svg/SkSVGCanvas.h -FILE: ../../../third_party/skia/include/utils/SkPaintFilterCanvas.h -FILE: ../../../third_party/skia/samplecode/SampleAnimatedText.cpp -FILE: ../../../third_party/skia/samplecode/SampleAtlas.cpp -FILE: ../../../third_party/skia/samplecode/SampleShip.cpp -FILE: ../../../third_party/skia/samplecode/SampleXfer.cpp -FILE: ../../../third_party/skia/src/c/sk_c_from_to.h -FILE: ../../../third_party/skia/src/c/sk_paint.cpp -FILE: ../../../third_party/skia/src/c/sk_types_priv.h -FILE: ../../../third_party/skia/src/codec/SkAndroidCodec.cpp -FILE: ../../../third_party/skia/src/codec/SkAndroidCodecAdapter.cpp -FILE: ../../../third_party/skia/src/codec/SkAndroidCodecAdapter.h -FILE: ../../../third_party/skia/src/codec/SkBmpCodec.cpp -FILE: ../../../third_party/skia/src/codec/SkBmpCodec.h -FILE: ../../../third_party/skia/src/codec/SkBmpMaskCodec.cpp -FILE: ../../../third_party/skia/src/codec/SkBmpMaskCodec.h -FILE: ../../../third_party/skia/src/codec/SkBmpRLECodec.cpp -FILE: ../../../third_party/skia/src/codec/SkBmpRLECodec.h -FILE: ../../../third_party/skia/src/codec/SkBmpStandardCodec.cpp -FILE: ../../../third_party/skia/src/codec/SkBmpStandardCodec.h -FILE: ../../../third_party/skia/src/codec/SkCodec.cpp -FILE: ../../../third_party/skia/src/codec/SkCodecImageGenerator.cpp -FILE: ../../../third_party/skia/src/codec/SkCodecImageGenerator.h -FILE: ../../../third_party/skia/src/codec/SkIcoCodec.cpp -FILE: ../../../third_party/skia/src/codec/SkIcoCodec.h -FILE: ../../../third_party/skia/src/codec/SkJpegCodec.cpp -FILE: ../../../third_party/skia/src/codec/SkJpegCodec.h -FILE: ../../../third_party/skia/src/codec/SkJpegDecoderMgr.cpp -FILE: ../../../third_party/skia/src/codec/SkJpegDecoderMgr.h -FILE: ../../../third_party/skia/src/codec/SkJpegUtility.cpp -FILE: ../../../third_party/skia/src/codec/SkJpegUtility.h -FILE: ../../../third_party/skia/src/codec/SkMaskSwizzler.cpp -FILE: ../../../third_party/skia/src/codec/SkMaskSwizzler.h -FILE: ../../../third_party/skia/src/codec/SkMasks.cpp -FILE: ../../../third_party/skia/src/codec/SkMasks.h -FILE: ../../../third_party/skia/src/codec/SkPngCodec.cpp -FILE: ../../../third_party/skia/src/codec/SkPngCodec.h -FILE: ../../../third_party/skia/src/codec/SkSampledCodec.cpp -FILE: ../../../third_party/skia/src/codec/SkSampledCodec.h -FILE: ../../../third_party/skia/src/codec/SkSampler.cpp -FILE: ../../../third_party/skia/src/codec/SkSampler.h -FILE: ../../../third_party/skia/src/codec/SkSwizzler.cpp -FILE: ../../../third_party/skia/src/codec/SkSwizzler.h -FILE: ../../../third_party/skia/src/codec/SkWbmpCodec.cpp -FILE: ../../../third_party/skia/src/codec/SkWbmpCodec.h -FILE: ../../../third_party/skia/src/codec/SkWebpCodec.cpp -FILE: ../../../third_party/skia/src/codec/SkWebpCodec.h -FILE: ../../../third_party/skia/src/core/Sk4px.h -FILE: ../../../third_party/skia/src/core/SkBigPicture.cpp -FILE: ../../../third_party/skia/src/core/SkBigPicture.h -FILE: ../../../third_party/skia/src/core/SkFontMgr.cpp -FILE: ../../../third_party/skia/src/core/SkLatticeIter.cpp -FILE: ../../../third_party/skia/src/core/SkLatticeIter.h -FILE: ../../../third_party/skia/src/core/SkLocalMatrixImageFilter.cpp -FILE: ../../../third_party/skia/src/core/SkMiniRecorder.cpp -FILE: ../../../third_party/skia/src/core/SkMiniRecorder.h -FILE: ../../../third_party/skia/src/core/SkMipmapAccessor.cpp -FILE: ../../../third_party/skia/src/core/SkMipmapAccessor.h -FILE: ../../../third_party/skia/src/core/SkNextID.h -FILE: ../../../third_party/skia/src/core/SkOpts.cpp -FILE: ../../../third_party/skia/src/core/SkOpts.h -FILE: ../../../third_party/skia/src/core/SkPathBuilder.cpp -FILE: ../../../third_party/skia/src/core/SkPathPriv.h -FILE: ../../../third_party/skia/src/core/SkPictureCommon.h -FILE: ../../../third_party/skia/src/core/SkPictureImageGenerator.cpp -FILE: ../../../third_party/skia/src/core/SkPixmap.cpp -FILE: ../../../third_party/skia/src/core/SkPixmapPriv.h -FILE: ../../../third_party/skia/src/core/SkPoint3.cpp -FILE: ../../../third_party/skia/src/core/SkRecord.cpp -FILE: ../../../third_party/skia/src/core/SkRecordPattern.h -FILE: ../../../third_party/skia/src/core/SkRecords.cpp -FILE: ../../../third_party/skia/src/core/SkSemaphore.cpp -FILE: ../../../third_party/skia/src/core/SkSharedMutex.cpp -FILE: ../../../third_party/skia/src/core/SkSharedMutex.h -FILE: ../../../third_party/skia/src/core/SkSpinlock.cpp -FILE: ../../../third_party/skia/src/core/SkTDPQueue.h -FILE: ../../../third_party/skia/src/core/SkThreadID.cpp -FILE: ../../../third_party/skia/src/core/SkTime.cpp -FILE: ../../../third_party/skia/src/core/SkXfermodeInterpretation.cpp -FILE: ../../../third_party/skia/src/core/SkXfermodeInterpretation.h -FILE: ../../../third_party/skia/src/core/SkYUVPlanesCache.cpp -FILE: ../../../third_party/skia/src/core/SkYUVPlanesCache.h -FILE: ../../../third_party/skia/src/effects/SkTableColorFilter.cpp -FILE: ../../../third_party/skia/src/effects/imagefilters/SkImageImageFilter.cpp +FILE: ../../../third_party/skia/bench/BigPath.cpp +FILE: ../../../third_party/skia/bench/BigPath.h +FILE: ../../../third_party/skia/bench/CanvasSaveRestoreBench.cpp +FILE: ../../../third_party/skia/bench/TriangulatorBench.cpp +FILE: ../../../third_party/skia/experimental/graphite/include/BackendTexture.h +FILE: ../../../third_party/skia/experimental/graphite/include/Context.h +FILE: ../../../third_party/skia/experimental/graphite/include/GraphiteTypes.h +FILE: ../../../third_party/skia/experimental/graphite/include/SkStuff.h +FILE: ../../../third_party/skia/experimental/graphite/include/TextureInfo.h +FILE: ../../../third_party/skia/experimental/graphite/include/mtl/MtlBackendContext.h +FILE: ../../../third_party/skia/experimental/graphite/include/mtl/MtlTypes.h +FILE: ../../../third_party/skia/experimental/graphite/src/BackendTexture.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/Caps.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/Caps.h +FILE: ../../../third_party/skia/experimental/graphite/src/CommandBuffer.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/CommandBuffer.h +FILE: ../../../third_party/skia/experimental/graphite/src/Context.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/ContextPriv.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/ContextPriv.h +FILE: ../../../third_party/skia/experimental/graphite/src/ContextUtils.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/ContextUtils.h +FILE: ../../../third_party/skia/experimental/graphite/src/CopyTask.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/CopyTask.h +FILE: ../../../third_party/skia/experimental/graphite/src/Device.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/Device.h +FILE: ../../../third_party/skia/experimental/graphite/src/DrawContext.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/DrawContext.h +FILE: ../../../third_party/skia/experimental/graphite/src/DrawList.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/DrawList.h +FILE: ../../../third_party/skia/experimental/graphite/src/DrawOrder.h +FILE: ../../../third_party/skia/experimental/graphite/src/DrawPass.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/DrawPass.h +FILE: ../../../third_party/skia/experimental/graphite/src/DrawTypes.h +FILE: ../../../third_party/skia/experimental/graphite/src/EnumBitMask.h +FILE: ../../../third_party/skia/experimental/graphite/src/Gpu.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/Gpu.h +FILE: ../../../third_party/skia/experimental/graphite/src/GpuWorkSubmission.h +FILE: ../../../third_party/skia/experimental/graphite/src/GraphicsPipeline.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/GraphicsPipeline.h +FILE: ../../../third_party/skia/experimental/graphite/src/GraphicsPipelineDesc.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/GraphicsPipelineDesc.h +FILE: ../../../third_party/skia/experimental/graphite/src/Image_Graphite.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/Image_Graphite.h +FILE: ../../../third_party/skia/experimental/graphite/src/ProgramCache.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/ProgramCache.h +FILE: ../../../third_party/skia/experimental/graphite/src/Recorder.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/Recorder.h +FILE: ../../../third_party/skia/experimental/graphite/src/Recording.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/Recording.h +FILE: ../../../third_party/skia/experimental/graphite/src/RenderPassTask.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/RenderPassTask.h +FILE: ../../../third_party/skia/experimental/graphite/src/Renderer.h +FILE: ../../../third_party/skia/experimental/graphite/src/ResourceProvider.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/ResourceProvider.h +FILE: ../../../third_party/skia/experimental/graphite/src/ResourceTypes.h +FILE: ../../../third_party/skia/experimental/graphite/src/SkStuff.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/Surface_Graphite.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/Surface_Graphite.h +FILE: ../../../third_party/skia/experimental/graphite/src/Task.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/Task.h +FILE: ../../../third_party/skia/experimental/graphite/src/TaskGraph.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/TaskGraph.h +FILE: ../../../third_party/skia/experimental/graphite/src/Texture.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/Texture.h +FILE: ../../../third_party/skia/experimental/graphite/src/TextureInfo.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/TextureProxy.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/TextureProxy.h +FILE: ../../../third_party/skia/experimental/graphite/src/Uniform.h +FILE: ../../../third_party/skia/experimental/graphite/src/UniformCache.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/UniformCache.h +FILE: ../../../third_party/skia/experimental/graphite/src/UniformManager.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/UniformManager.h +FILE: ../../../third_party/skia/experimental/graphite/src/geom/BoundsManager.h +FILE: ../../../third_party/skia/experimental/graphite/src/geom/IntersectionTree.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/geom/IntersectionTree.h +FILE: ../../../third_party/skia/experimental/graphite/src/geom/Rect.h +FILE: ../../../third_party/skia/experimental/graphite/src/geom/Shape.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/geom/Shape.h +FILE: ../../../third_party/skia/experimental/graphite/src/geom/Transform.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/geom/Transform_graphite.h +FILE: ../../../third_party/skia/experimental/graphite/src/geom/VectorTypes.h +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlCaps.h +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlCaps.mm +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlCommandBuffer.h +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlCommandBuffer.mm +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlGpu.h +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlGpu.mm +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlGraphicsPipeline.h +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlGraphicsPipeline.mm +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlResourceProvider.h +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlResourceProvider.mm +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlTexture.h +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlTexture.mm +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlTypesPriv.mm +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlUtils.h +FILE: ../../../third_party/skia/experimental/graphite/src/render/StencilAndFillPathRenderer.cpp +FILE: ../../../third_party/skia/experimental/lowp-basic/QMath.h +FILE: ../../../third_party/skia/experimental/lowp-basic/bilerp-study.cpp +FILE: ../../../third_party/skia/experimental/lowp-basic/lerp-study.cpp +FILE: ../../../third_party/skia/experimental/lowp-basic/lowp_experiments.cpp +FILE: ../../../third_party/skia/experimental/tskit/bindings/bindings.h +FILE: ../../../third_party/skia/experimental/tskit/bindings/core.cpp +FILE: ../../../third_party/skia/experimental/tskit/bindings/extension.cpp +FILE: ../../../third_party/skia/experimental/webgpu-bazel/src/bindings.cpp +FILE: ../../../third_party/skia/gm/composecolorfilter.cpp +FILE: ../../../third_party/skia/gm/crbug_1167277.cpp +FILE: ../../../third_party/skia/gm/crbug_1174186.cpp +FILE: ../../../third_party/skia/gm/crbug_1174354.cpp +FILE: ../../../third_party/skia/gm/crbug_1177833.cpp +FILE: ../../../third_party/skia/gm/crbug_1257515.cpp +FILE: ../../../third_party/skia/gm/crop_imagefilter.cpp +FILE: ../../../third_party/skia/gm/fillrect_gradient.cpp +FILE: ../../../third_party/skia/gm/graphitestart.cpp +FILE: ../../../third_party/skia/gm/hardstop_gradients_many.cpp +FILE: ../../../third_party/skia/gm/lazytiling.cpp +FILE: ../../../third_party/skia/gm/particles.cpp +FILE: ../../../third_party/skia/include/core/SkBlender.h +FILE: ../../../third_party/skia/include/effects/SkBlenders.h +FILE: ../../../third_party/skia/include/gpu/GrSurfaceInfo.h +FILE: ../../../third_party/skia/include/gpu/gl/egl/GrGLMakeEGLInterface.h +FILE: ../../../third_party/skia/include/gpu/gl/glx/GrGLMakeGLXInterface.h +FILE: ../../../third_party/skia/include/private/GrDawnTypesPriv.h +FILE: ../../../third_party/skia/include/private/GrMockTypesPriv.h +FILE: ../../../third_party/skia/include/sksl/DSLSymbols.h +FILE: ../../../third_party/skia/modules/canvaskit/paragraph_bindings_gen.cpp +FILE: ../../../third_party/skia/src/core/SkBlendModeBlender.cpp +FILE: ../../../third_party/skia/src/core/SkBlendModeBlender.h +FILE: ../../../third_party/skia/src/core/SkBlenderBase.h +FILE: ../../../third_party/skia/src/core/SkMatrixInvert.cpp +FILE: ../../../third_party/skia/src/core/SkMatrixInvert.h +FILE: ../../../third_party/skia/src/core/SkVMBlitter.h +FILE: ../../../third_party/skia/src/core/SkYUVAInfoLocation.h +FILE: ../../../third_party/skia/src/effects/SkBlenders.cpp +FILE: ../../../third_party/skia/src/effects/imagefilters/SkCropImageFilter.cpp +FILE: ../../../third_party/skia/src/effects/imagefilters/SkCropImageFilter.h +FILE: ../../../third_party/skia/src/effects/imagefilters/SkRuntimeImageFilter.cpp +FILE: ../../../third_party/skia/src/effects/imagefilters/SkRuntimeImageFilter.h +FILE: ../../../third_party/skia/src/gpu/BaseDevice.cpp +FILE: ../../../third_party/skia/src/gpu/BaseDevice.h +FILE: ../../../third_party/skia/src/gpu/GrDstProxyView.h +FILE: ../../../third_party/skia/src/gpu/GrEagerVertexAllocator.cpp +FILE: ../../../third_party/skia/src/gpu/GrMeshDrawTarget.cpp +FILE: ../../../third_party/skia/src/gpu/GrMeshDrawTarget.h +FILE: ../../../third_party/skia/src/gpu/GrOpsTypes.h +FILE: ../../../third_party/skia/src/gpu/GrPersistentCacheUtils.cpp +FILE: ../../../third_party/skia/src/gpu/GrSubRunAllocator.cpp +FILE: ../../../third_party/skia/src/gpu/GrSubRunAllocator.h +FILE: ../../../third_party/skia/src/gpu/GrSurfaceInfo.cpp +FILE: ../../../third_party/skia/src/gpu/GrWritePixelsRenderTask.cpp +FILE: ../../../third_party/skia/src/gpu/GrWritePixelsRenderTask.h +FILE: ../../../third_party/skia/src/gpu/GrYUVATextureProxies.cpp +FILE: ../../../third_party/skia/src/gpu/GrYUVATextureProxies.h +FILE: ../../../third_party/skia/src/gpu/SurfaceFillContext.cpp +FILE: ../../../third_party/skia/src/gpu/SurfaceFillContext.h +FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DTypesPriv.cpp +FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnTypesPriv.cpp +FILE: ../../../third_party/skia/src/gpu/geometry/GrInnerFanTriangulator.h +FILE: ../../../third_party/skia/src/gpu/gl/egl/GrGLMakeNativeInterface_egl.cpp +FILE: ../../../third_party/skia/src/gpu/gl/glx/GrGLMakeNativeInterface_glx.cpp +FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlTypesPriv.mm +FILE: ../../../third_party/skia/src/gpu/ops/FillPathFlags.h +FILE: ../../../third_party/skia/src/gpu/vk/GrVkBuffer.cpp +FILE: ../../../third_party/skia/src/gpu/vk/GrVkBuffer.h +FILE: ../../../third_party/skia/src/sksl/SkSLBuiltinTypes.cpp +FILE: ../../../third_party/skia/src/sksl/SkSLBuiltinTypes.h +FILE: ../../../third_party/skia/src/sksl/SkSLContext.cpp +FILE: ../../../third_party/skia/src/sksl/SkSLIntrinsicList.h +FILE: ../../../third_party/skia/src/sksl/SkSLMangler.h +FILE: ../../../third_party/skia/src/sksl/SkSLOperators.cpp +FILE: ../../../third_party/skia/src/sksl/SkSLOperators.h +FILE: ../../../third_party/skia/src/sksl/SkSLThreadContext.h +FILE: ../../../third_party/skia/src/sksl/analysis/SkSLCanExitWithoutReturningValue.cpp +FILE: ../../../third_party/skia/src/sksl/analysis/SkSLCheckProgramUnrolledSize.cpp +FILE: ../../../third_party/skia/src/sksl/analysis/SkSLGetLoopUnrollInfo.cpp +FILE: ../../../third_party/skia/src/sksl/analysis/SkSLIsConstantExpression.cpp +FILE: ../../../third_party/skia/src/sksl/analysis/SkSLProgramUsage.cpp +FILE: ../../../third_party/skia/src/sksl/analysis/SkSLProgramVisitor.h +FILE: ../../../third_party/skia/src/sksl/analysis/SkSLSwitchCaseContainsExit.cpp +FILE: ../../../third_party/skia/src/sksl/codegen/SkVMDebugInfo.cpp +FILE: ../../../third_party/skia/src/sksl/codegen/SkVMDebugInfo.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLBinaryExpression.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLBlock.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLChildCall.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLChildCall.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorArray.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorArray.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorArrayCast.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorArrayCast.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorCompound.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorCompound.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorCompoundCast.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorCompoundCast.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorDiagonalMatrix.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorDiagonalMatrix.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorMatrixResize.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorMatrixResize.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorScalarCast.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorScalarCast.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorSplat.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorSplat.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorStruct.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorStruct.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLDoStatement.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLExpressionStatement.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLFieldAccess.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLForStatement.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLFunctionCall.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLFunctionDefinition.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLIfStatement.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLIndexExpression.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLMethodReference.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLPostfixExpression.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLSwitchStatement.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLSwizzle.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLTernaryExpression.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLTypeReference.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLVarDeclarations.cpp +FILE: ../../../third_party/skia/src/sksl/transform/SkSLEliminateDeadFunctions.cpp +FILE: ../../../third_party/skia/src/sksl/transform/SkSLEliminateDeadGlobalVariables.cpp +FILE: ../../../third_party/skia/src/sksl/transform/SkSLEliminateDeadLocalVariables.cpp +FILE: ../../../third_party/skia/src/sksl/transform/SkSLEliminateUnreachableCode.cpp +FILE: ../../../third_party/skia/src/sksl/transform/SkSLProgramWriter.h +---------------------------------------------------------------------------------------------------- +Copyright 2021 Google LLC + +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 the copyright holder 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. +==================================================================================================== + +==================================================================================================== +LIBRARY: skia +ORIGIN: ../../../third_party/skia/bench/BigPathBench.cpp + ../../../third_party/skia/LICENSE +TYPE: LicenseType.bsd +FILE: ../../../third_party/skia/bench/BigPathBench.cpp +FILE: ../../../third_party/skia/bench/BitmapRegionDecoderBench.cpp +FILE: ../../../third_party/skia/bench/BitmapRegionDecoderBench.h +FILE: ../../../third_party/skia/bench/CodecBench.cpp +FILE: ../../../third_party/skia/bench/CodecBench.h +FILE: ../../../third_party/skia/bench/CodecBenchPriv.h +FILE: ../../../third_party/skia/bench/ControlBench.cpp +FILE: ../../../third_party/skia/bench/DrawBitmapAABench.cpp +FILE: ../../../third_party/skia/bench/ImageBench.cpp +FILE: ../../../third_party/skia/bench/InterpBench.cpp +FILE: ../../../third_party/skia/bench/MathBench.cpp +FILE: ../../../third_party/skia/bench/MipmapBench.cpp +FILE: ../../../third_party/skia/bench/PictureOverheadBench.cpp +FILE: ../../../third_party/skia/bench/SKPAnimationBench.cpp +FILE: ../../../third_party/skia/bench/SKPAnimationBench.h +FILE: ../../../third_party/skia/bench/Sk4fBench.cpp +FILE: ../../../third_party/skia/bench/SkGlyphCacheBench.cpp +FILE: ../../../third_party/skia/bench/StrokeBench.cpp +FILE: ../../../third_party/skia/bench/TextBlobBench.cpp +FILE: ../../../third_party/skia/bench/TopoSortBench.cpp +FILE: ../../../third_party/skia/bench/nanobench.h +FILE: ../../../third_party/skia/client_utils/android/BRDAllocator.h +FILE: ../../../third_party/skia/client_utils/android/BitmapRegionDecoder.cpp +FILE: ../../../third_party/skia/client_utils/android/BitmapRegionDecoder.h +FILE: ../../../third_party/skia/client_utils/android/BitmapRegionDecoderPriv.h +FILE: ../../../third_party/skia/dm/DMSrcSink.cpp +FILE: ../../../third_party/skia/dm/DMSrcSink.h +FILE: ../../../third_party/skia/experimental/c-api-example/skia-c-example.c +FILE: ../../../third_party/skia/experimental/tools/coreGraphicsPdf2png.cpp +FILE: ../../../third_party/skia/gm/aaxfermodes.cpp +FILE: ../../../third_party/skia/gm/addarc.cpp +FILE: ../../../third_party/skia/gm/all_bitmap_configs.cpp +FILE: ../../../third_party/skia/gm/anisotropic.cpp +FILE: ../../../third_party/skia/gm/annotated_text.cpp +FILE: ../../../third_party/skia/gm/badpaint.cpp +FILE: ../../../third_party/skia/gm/bigrrectaaeffect.cpp +FILE: ../../../third_party/skia/gm/bigtileimagefilter.cpp +FILE: ../../../third_party/skia/gm/blend.cpp +FILE: ../../../third_party/skia/gm/blurredclippedcircle.cpp +FILE: ../../../third_party/skia/gm/bmpfilterqualityrepeat.cpp +FILE: ../../../third_party/skia/gm/concavepaths.cpp +FILE: ../../../third_party/skia/gm/constcolorprocessor.cpp +FILE: ../../../third_party/skia/gm/convex_all_line_paths.cpp +FILE: ../../../third_party/skia/gm/draw_bitmap_rect_skbug4374.cpp +FILE: ../../../third_party/skia/gm/drawable.cpp +FILE: ../../../third_party/skia/gm/drawatlas.cpp +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/image_pict.cpp +FILE: ../../../third_party/skia/gm/image_shader.cpp +FILE: ../../../third_party/skia/gm/imagefilters.cpp +FILE: ../../../third_party/skia/gm/imagefiltersstroked.cpp +FILE: ../../../third_party/skia/gm/imagefilterstransformed.cpp +FILE: ../../../third_party/skia/gm/imagefromyuvtextures.cpp +FILE: ../../../third_party/skia/gm/imagescalealigned.cpp +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/localmatriximagefilter.cpp +FILE: ../../../third_party/skia/gm/localmatriximageshader.cpp +FILE: ../../../third_party/skia/gm/mipmap.cpp +FILE: ../../../third_party/skia/gm/path_stroke_with_zero_length.cpp +FILE: ../../../third_party/skia/gm/pathcontourstart.cpp +FILE: ../../../third_party/skia/gm/pdf_never_embed.cpp +FILE: ../../../third_party/skia/gm/perspshaders.cpp +FILE: ../../../third_party/skia/gm/pictureimagegenerator.cpp +FILE: ../../../third_party/skia/gm/pixelsnap.cpp +FILE: ../../../third_party/skia/gm/plus.cpp +FILE: ../../../third_party/skia/gm/repeated_bitmap.cpp +FILE: ../../../third_party/skia/gm/scaledstrokes.cpp +FILE: ../../../third_party/skia/gm/skbug_257.cpp +FILE: ../../../third_party/skia/gm/smallpaths.cpp +FILE: ../../../third_party/skia/gm/stlouisarch.cpp +FILE: ../../../third_party/skia/gm/textblobcolortrans.cpp +FILE: ../../../third_party/skia/gm/textblobgeometrychange.cpp +FILE: ../../../third_party/skia/gm/textblobmixedsizes.cpp +FILE: ../../../third_party/skia/gm/textblobrandomfont.cpp +FILE: ../../../third_party/skia/gm/textblobtransforms.cpp +FILE: ../../../third_party/skia/gm/textblobuseaftergpufree.cpp +FILE: ../../../third_party/skia/gm/transparency.cpp +FILE: ../../../third_party/skia/gm/xform.cpp +FILE: ../../../third_party/skia/include/codec/SkAndroidCodec.h +FILE: ../../../third_party/skia/include/codec/SkCodec.h +FILE: ../../../third_party/skia/include/core/SkEncodedImageFormat.h +FILE: ../../../third_party/skia/include/core/SkPathBuilder.h +FILE: ../../../third_party/skia/include/core/SkPixmap.h +FILE: ../../../third_party/skia/include/core/SkPngChunkReader.h +FILE: ../../../third_party/skia/include/core/SkPoint3.h +FILE: ../../../third_party/skia/include/core/SkRSXform.h +FILE: ../../../third_party/skia/include/core/SkTraceMemoryDump.h +FILE: ../../../third_party/skia/include/effects/SkTableColorFilter.h +FILE: ../../../third_party/skia/include/gpu/GrContextOptions.h +FILE: ../../../third_party/skia/include/gpu/gl/GrGLTypes.h +FILE: ../../../third_party/skia/include/ports/SkFontMgr_android.h +FILE: ../../../third_party/skia/include/ports/SkFontMgr_directory.h +FILE: ../../../third_party/skia/include/ports/SkFontMgr_empty.h +FILE: ../../../third_party/skia/include/ports/SkFontMgr_fontconfig.h +FILE: ../../../third_party/skia/include/private/SkMutex.h +FILE: ../../../third_party/skia/include/private/SkNx.h +FILE: ../../../third_party/skia/include/private/SkNx_neon.h +FILE: ../../../third_party/skia/include/private/SkNx_sse.h +FILE: ../../../third_party/skia/include/private/SkSemaphore.h +FILE: ../../../third_party/skia/include/private/SkSpinlock.h +FILE: ../../../third_party/skia/include/private/SkTHash.h +FILE: ../../../third_party/skia/include/private/SkThreadID.h +FILE: ../../../third_party/skia/include/svg/SkSVGCanvas.h +FILE: ../../../third_party/skia/include/utils/SkPaintFilterCanvas.h +FILE: ../../../third_party/skia/samplecode/SampleAnimatedText.cpp +FILE: ../../../third_party/skia/samplecode/SampleAtlas.cpp +FILE: ../../../third_party/skia/samplecode/SampleShip.cpp +FILE: ../../../third_party/skia/samplecode/SampleXfer.cpp +FILE: ../../../third_party/skia/src/c/sk_c_from_to.h +FILE: ../../../third_party/skia/src/c/sk_paint.cpp +FILE: ../../../third_party/skia/src/c/sk_types_priv.h +FILE: ../../../third_party/skia/src/codec/SkAndroidCodec.cpp +FILE: ../../../third_party/skia/src/codec/SkAndroidCodecAdapter.cpp +FILE: ../../../third_party/skia/src/codec/SkAndroidCodecAdapter.h +FILE: ../../../third_party/skia/src/codec/SkBmpCodec.cpp +FILE: ../../../third_party/skia/src/codec/SkBmpCodec.h +FILE: ../../../third_party/skia/src/codec/SkBmpMaskCodec.cpp +FILE: ../../../third_party/skia/src/codec/SkBmpMaskCodec.h +FILE: ../../../third_party/skia/src/codec/SkBmpRLECodec.cpp +FILE: ../../../third_party/skia/src/codec/SkBmpRLECodec.h +FILE: ../../../third_party/skia/src/codec/SkBmpStandardCodec.cpp +FILE: ../../../third_party/skia/src/codec/SkBmpStandardCodec.h +FILE: ../../../third_party/skia/src/codec/SkCodec.cpp +FILE: ../../../third_party/skia/src/codec/SkCodecImageGenerator.cpp +FILE: ../../../third_party/skia/src/codec/SkCodecImageGenerator.h +FILE: ../../../third_party/skia/src/codec/SkIcoCodec.cpp +FILE: ../../../third_party/skia/src/codec/SkIcoCodec.h +FILE: ../../../third_party/skia/src/codec/SkJpegCodec.cpp +FILE: ../../../third_party/skia/src/codec/SkJpegCodec.h +FILE: ../../../third_party/skia/src/codec/SkJpegDecoderMgr.cpp +FILE: ../../../third_party/skia/src/codec/SkJpegDecoderMgr.h +FILE: ../../../third_party/skia/src/codec/SkJpegUtility.cpp +FILE: ../../../third_party/skia/src/codec/SkJpegUtility.h +FILE: ../../../third_party/skia/src/codec/SkMaskSwizzler.cpp +FILE: ../../../third_party/skia/src/codec/SkMaskSwizzler.h +FILE: ../../../third_party/skia/src/codec/SkMasks.cpp +FILE: ../../../third_party/skia/src/codec/SkMasks.h +FILE: ../../../third_party/skia/src/codec/SkPngCodec.cpp +FILE: ../../../third_party/skia/src/codec/SkPngCodec.h +FILE: ../../../third_party/skia/src/codec/SkSampledCodec.cpp +FILE: ../../../third_party/skia/src/codec/SkSampledCodec.h +FILE: ../../../third_party/skia/src/codec/SkSampler.cpp +FILE: ../../../third_party/skia/src/codec/SkSampler.h +FILE: ../../../third_party/skia/src/codec/SkSwizzler.cpp +FILE: ../../../third_party/skia/src/codec/SkSwizzler.h +FILE: ../../../third_party/skia/src/codec/SkWbmpCodec.cpp +FILE: ../../../third_party/skia/src/codec/SkWbmpCodec.h +FILE: ../../../third_party/skia/src/codec/SkWebpCodec.cpp +FILE: ../../../third_party/skia/src/codec/SkWebpCodec.h +FILE: ../../../third_party/skia/src/core/Sk4px.h +FILE: ../../../third_party/skia/src/core/SkBigPicture.cpp +FILE: ../../../third_party/skia/src/core/SkBigPicture.h +FILE: ../../../third_party/skia/src/core/SkFontMgr.cpp +FILE: ../../../third_party/skia/src/core/SkLatticeIter.cpp +FILE: ../../../third_party/skia/src/core/SkLatticeIter.h +FILE: ../../../third_party/skia/src/core/SkLocalMatrixImageFilter.cpp +FILE: ../../../third_party/skia/src/core/SkMiniRecorder.cpp +FILE: ../../../third_party/skia/src/core/SkMiniRecorder.h +FILE: ../../../third_party/skia/src/core/SkMipmapAccessor.cpp +FILE: ../../../third_party/skia/src/core/SkMipmapAccessor.h +FILE: ../../../third_party/skia/src/core/SkNextID.h +FILE: ../../../third_party/skia/src/core/SkOpts.cpp +FILE: ../../../third_party/skia/src/core/SkOpts.h +FILE: ../../../third_party/skia/src/core/SkPathBuilder.cpp +FILE: ../../../third_party/skia/src/core/SkPathPriv.h +FILE: ../../../third_party/skia/src/core/SkPictureCommon.h +FILE: ../../../third_party/skia/src/core/SkPictureImageGenerator.cpp +FILE: ../../../third_party/skia/src/core/SkPixmap.cpp +FILE: ../../../third_party/skia/src/core/SkPixmapPriv.h +FILE: ../../../third_party/skia/src/core/SkPoint3.cpp +FILE: ../../../third_party/skia/src/core/SkRecord.cpp +FILE: ../../../third_party/skia/src/core/SkRecordPattern.h +FILE: ../../../third_party/skia/src/core/SkRecords.cpp +FILE: ../../../third_party/skia/src/core/SkSemaphore.cpp +FILE: ../../../third_party/skia/src/core/SkSharedMutex.cpp +FILE: ../../../third_party/skia/src/core/SkSharedMutex.h +FILE: ../../../third_party/skia/src/core/SkSpinlock.cpp +FILE: ../../../third_party/skia/src/core/SkTDPQueue.h +FILE: ../../../third_party/skia/src/core/SkThreadID.cpp +FILE: ../../../third_party/skia/src/core/SkTime.cpp +FILE: ../../../third_party/skia/src/core/SkXfermodeInterpretation.cpp +FILE: ../../../third_party/skia/src/core/SkXfermodeInterpretation.h +FILE: ../../../third_party/skia/src/core/SkYUVPlanesCache.cpp +FILE: ../../../third_party/skia/src/core/SkYUVPlanesCache.h +FILE: ../../../third_party/skia/src/effects/SkTableColorFilter.cpp +FILE: ../../../third_party/skia/src/effects/imagefilters/SkImageImageFilter.cpp FILE: ../../../third_party/skia/src/gpu/GrAutoLocaleSetter.h FILE: ../../../third_party/skia/src/gpu/GrBlurUtils.cpp FILE: ../../../third_party/skia/src/gpu/GrBlurUtils.h @@ -2744,162 +3114,49 @@ FILE: ../../../third_party/skia/src/effects/imagefilters/SkAlphaThresholdImageFi FILE: ../../../third_party/skia/src/effects/imagefilters/SkComposeImageFilter.cpp FILE: ../../../third_party/skia/src/effects/imagefilters/SkDisplacementMapImageFilter.cpp FILE: ../../../third_party/skia/src/effects/imagefilters/SkDropShadowImageFilter.cpp -FILE: ../../../third_party/skia/src/effects/imagefilters/SkTileImageFilter.cpp -FILE: ../../../third_party/skia/src/gpu/GrBlend.h -FILE: ../../../third_party/skia/src/gpu/GrCaps.h -FILE: ../../../third_party/skia/src/gpu/GrGeometryProcessor.h -FILE: ../../../third_party/skia/src/gpu/GrPaint.cpp -FILE: ../../../third_party/skia/src/gpu/GrRectanizerSkyline.cpp -FILE: ../../../third_party/skia/src/gpu/effects/GrBezierEffect.cpp -FILE: ../../../third_party/skia/src/gpu/effects/GrBezierEffect.h -FILE: ../../../third_party/skia/src/gpu/effects/GrBicubicEffect.h -FILE: ../../../third_party/skia/src/gpu/effects/GrBitmapTextGeoProc.cpp -FILE: ../../../third_party/skia/src/gpu/effects/GrBitmapTextGeoProc.h -FILE: ../../../third_party/skia/src/gpu/effects/GrDistanceFieldGeoProc.cpp -FILE: ../../../third_party/skia/src/gpu/effects/GrDistanceFieldGeoProc.h -FILE: ../../../third_party/skia/src/gpu/gl/GrGLContext.cpp -FILE: ../../../third_party/skia/src/gpu/gl/GrGLContext.h -FILE: ../../../third_party/skia/src/gpu/gl/GrGLExtensions.cpp -FILE: ../../../third_party/skia/src/gpu/gl/GrGLVertexArray.cpp -FILE: ../../../third_party/skia/src/gpu/gl/GrGLVertexArray.h -FILE: ../../../third_party/skia/src/gpu/ops/GrOvalOpFactory.cpp -FILE: ../../../third_party/skia/src/gpu/ops/GrOvalOpFactory.h -FILE: ../../../third_party/skia/src/lazy/SkDiscardableMemoryPool.cpp -FILE: ../../../third_party/skia/src/lazy/SkDiscardableMemoryPool.h -FILE: ../../../third_party/skia/src/pathops/SkOpCoincidence.h -FILE: ../../../third_party/skia/src/pathops/SkOpContour.cpp -FILE: ../../../third_party/skia/src/pathops/SkOpContour.h -FILE: ../../../third_party/skia/src/pathops/SkPathOpsDebug.cpp -FILE: ../../../third_party/skia/src/pathops/SkPathOpsDebug.h -FILE: ../../../third_party/skia/src/pdf/SkPDFResourceDict.cpp -FILE: ../../../third_party/skia/src/pdf/SkPDFResourceDict.h -FILE: ../../../third_party/skia/src/ports/SkDiscardableMemory_none.cpp -FILE: ../../../third_party/skia/src/ports/SkFontConfigTypeface.h -FILE: ../../../third_party/skia/src/ports/SkFontMgr_FontConfigInterface.cpp -FILE: ../../../third_party/skia/src/ports/SkOSFile_posix.cpp -FILE: ../../../third_party/skia/src/ports/SkOSFile_win.cpp -FILE: ../../../third_party/skia/src/sfnt/SkOTTable_name.cpp -FILE: ../../../third_party/skia/src/sfnt/SkTTCFHeader.h -FILE: ../../../third_party/skia/src/shaders/SkColorFilterShader.cpp -FILE: ../../../third_party/skia/src/shaders/SkPerlinNoiseShader.cpp -FILE: ../../../third_party/skia/src/utils/SkCanvasStack.cpp -FILE: ../../../third_party/skia/src/utils/SkCanvasStack.h -FILE: ../../../third_party/skia/src/utils/SkCanvasStateUtils.cpp ----------------------------------------------------------------------------------------------------- -Copyright 2013 Google Inc. - -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 the copyright holder 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. -==================================================================================================== - -==================================================================================================== -LIBRARY: skia -ORIGIN: ../../../third_party/skia/bench/BulkRectBench.cpp + ../../../third_party/skia/LICENSE -TYPE: LicenseType.bsd -FILE: ../../../third_party/skia/bench/BulkRectBench.cpp -FILE: ../../../third_party/skia/bench/DDLRecorderBench.cpp -FILE: ../../../third_party/skia/bench/SkSLBench.cpp -FILE: ../../../third_party/skia/experimental/wasm-skp-debugger/debugger_bindings.cpp -FILE: ../../../third_party/skia/gm/asyncrescaleandread.cpp -FILE: ../../../third_party/skia/gm/crbug_908646.cpp -FILE: ../../../third_party/skia/gm/crbug_946965.cpp -FILE: ../../../third_party/skia/gm/patharcto.cpp -FILE: ../../../third_party/skia/gm/runtimecolorfilter.cpp -FILE: ../../../third_party/skia/gm/runtimefunctions.cpp -FILE: ../../../third_party/skia/gm/runtimeintrinsics.cpp -FILE: ../../../third_party/skia/gm/runtimeshader.cpp -FILE: ../../../third_party/skia/gm/skbug_9319.cpp -FILE: ../../../third_party/skia/include/effects/SkImageFilters.h -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/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 -FILE: ../../../third_party/skia/modules/particles/include/SkParticleBinding.h -FILE: ../../../third_party/skia/modules/particles/include/SkParticleData.h -FILE: ../../../third_party/skia/modules/particles/include/SkParticleDrawable.h -FILE: ../../../third_party/skia/modules/particles/include/SkParticleEffect.h -FILE: ../../../third_party/skia/modules/particles/include/SkParticleSerialization.h -FILE: ../../../third_party/skia/modules/particles/include/SkReflected.h -FILE: ../../../third_party/skia/modules/particles/src/SkParticleBinding.cpp -FILE: ../../../third_party/skia/modules/particles/src/SkParticleDrawable.cpp -FILE: ../../../third_party/skia/modules/particles/src/SkParticleEffect.cpp -FILE: ../../../third_party/skia/modules/particles/src/SkReflected.cpp -FILE: ../../../third_party/skia/modules/skresources/include/SkResources.h -FILE: ../../../third_party/skia/modules/skresources/src/SkResources.cpp -FILE: ../../../third_party/skia/samplecode/SampleFilterBounds.cpp -FILE: ../../../third_party/skia/samplecode/SampleImageFilterDAG.cpp -FILE: ../../../third_party/skia/src/core/SkImageFilterTypes.cpp -FILE: ../../../third_party/skia/src/core/SkImageFilterTypes.h -FILE: ../../../third_party/skia/src/core/SkImageFilter_Base.h -FILE: ../../../third_party/skia/src/core/SkRuntimeEffect.cpp -FILE: ../../../third_party/skia/src/core/SkVM.cpp -FILE: ../../../third_party/skia/src/core/SkVM.h -FILE: ../../../third_party/skia/src/gpu/GrClientMappedBufferManager.cpp -FILE: ../../../third_party/skia/src/gpu/GrClientMappedBufferManager.h -FILE: ../../../third_party/skia/src/gpu/GrCopyRenderTask.cpp -FILE: ../../../third_party/skia/src/gpu/GrCopyRenderTask.h -FILE: ../../../third_party/skia/src/gpu/GrImageInfo.h -FILE: ../../../third_party/skia/src/gpu/GrPersistentCacheUtils.h -FILE: ../../../third_party/skia/src/gpu/GrProgramInfo.cpp -FILE: ../../../third_party/skia/src/gpu/GrProgramInfo.h -FILE: ../../../third_party/skia/src/gpu/GrRenderTask.cpp -FILE: ../../../third_party/skia/src/gpu/GrRenderTask.h -FILE: ../../../third_party/skia/src/gpu/GrShaderUtils.cpp -FILE: ../../../third_party/skia/src/gpu/GrShaderUtils.h -FILE: ../../../third_party/skia/src/gpu/GrSurfaceProxyView.h -FILE: ../../../third_party/skia/src/gpu/GrSwizzle.cpp -FILE: ../../../third_party/skia/src/gpu/GrTextureResolveManager.h -FILE: ../../../third_party/skia/src/gpu/GrTextureResolveRenderTask.cpp -FILE: ../../../third_party/skia/src/gpu/GrTextureResolveRenderTask.h -FILE: ../../../third_party/skia/src/gpu/GrTransferFromRenderTask.cpp -FILE: ../../../third_party/skia/src/gpu/GrTransferFromRenderTask.h -FILE: ../../../third_party/skia/src/gpu/GrUtil.h -FILE: ../../../third_party/skia/src/gpu/GrWaitRenderTask.cpp -FILE: ../../../third_party/skia/src/gpu/GrWaitRenderTask.h -FILE: ../../../third_party/skia/src/gpu/geometry/GrQuadBuffer.h -FILE: ../../../third_party/skia/src/gpu/geometry/GrQuadUtils.cpp -FILE: ../../../third_party/skia/src/gpu/geometry/GrQuadUtils.h -FILE: ../../../third_party/skia/src/gpu/gl/GrGLAssembleGLESInterfaceAutogen.cpp -FILE: ../../../third_party/skia/src/gpu/gl/GrGLAssembleGLInterfaceAutogen.cpp -FILE: ../../../third_party/skia/src/gpu/gl/GrGLAssembleHelpers.cpp -FILE: ../../../third_party/skia/src/gpu/gl/GrGLAssembleWebGLInterfaceAutogen.cpp -FILE: ../../../third_party/skia/src/gpu/gl/GrGLTypesPriv.cpp -FILE: ../../../third_party/skia/src/gpu/mock/GrMockCaps.cpp -FILE: ../../../third_party/skia/src/gpu/mock/GrMockTypes.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLExternalFunction.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLExternalFunctionCall.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLExternalFunctionReference.h +FILE: ../../../third_party/skia/src/effects/imagefilters/SkTileImageFilter.cpp +FILE: ../../../third_party/skia/src/gpu/GrBlend.h +FILE: ../../../third_party/skia/src/gpu/GrCaps.h +FILE: ../../../third_party/skia/src/gpu/GrGeometryProcessor.h +FILE: ../../../third_party/skia/src/gpu/GrPaint.cpp +FILE: ../../../third_party/skia/src/gpu/GrRectanizerSkyline.cpp +FILE: ../../../third_party/skia/src/gpu/effects/GrBezierEffect.cpp +FILE: ../../../third_party/skia/src/gpu/effects/GrBezierEffect.h +FILE: ../../../third_party/skia/src/gpu/effects/GrBicubicEffect.h +FILE: ../../../third_party/skia/src/gpu/effects/GrBitmapTextGeoProc.cpp +FILE: ../../../third_party/skia/src/gpu/effects/GrBitmapTextGeoProc.h +FILE: ../../../third_party/skia/src/gpu/effects/GrDistanceFieldGeoProc.cpp +FILE: ../../../third_party/skia/src/gpu/effects/GrDistanceFieldGeoProc.h +FILE: ../../../third_party/skia/src/gpu/gl/GrGLContext.cpp +FILE: ../../../third_party/skia/src/gpu/gl/GrGLContext.h +FILE: ../../../third_party/skia/src/gpu/gl/GrGLExtensions.cpp +FILE: ../../../third_party/skia/src/gpu/gl/GrGLVertexArray.cpp +FILE: ../../../third_party/skia/src/gpu/gl/GrGLVertexArray.h +FILE: ../../../third_party/skia/src/gpu/ops/GrOvalOpFactory.cpp +FILE: ../../../third_party/skia/src/gpu/ops/GrOvalOpFactory.h +FILE: ../../../third_party/skia/src/lazy/SkDiscardableMemoryPool.cpp +FILE: ../../../third_party/skia/src/lazy/SkDiscardableMemoryPool.h +FILE: ../../../third_party/skia/src/pathops/SkOpCoincidence.h +FILE: ../../../third_party/skia/src/pathops/SkOpContour.cpp +FILE: ../../../third_party/skia/src/pathops/SkOpContour.h +FILE: ../../../third_party/skia/src/pathops/SkPathOpsDebug.cpp +FILE: ../../../third_party/skia/src/pathops/SkPathOpsDebug.h +FILE: ../../../third_party/skia/src/pdf/SkPDFResourceDict.cpp +FILE: ../../../third_party/skia/src/pdf/SkPDFResourceDict.h +FILE: ../../../third_party/skia/src/ports/SkDiscardableMemory_none.cpp +FILE: ../../../third_party/skia/src/ports/SkFontConfigTypeface.h +FILE: ../../../third_party/skia/src/ports/SkFontMgr_FontConfigInterface.cpp +FILE: ../../../third_party/skia/src/ports/SkOSFile_posix.cpp +FILE: ../../../third_party/skia/src/ports/SkOSFile_win.cpp +FILE: ../../../third_party/skia/src/sfnt/SkOTTable_name.cpp +FILE: ../../../third_party/skia/src/sfnt/SkTTCFHeader.h +FILE: ../../../third_party/skia/src/shaders/SkColorFilterShader.cpp +FILE: ../../../third_party/skia/src/shaders/SkPerlinNoiseShader.cpp +FILE: ../../../third_party/skia/src/utils/SkCanvasStack.cpp +FILE: ../../../third_party/skia/src/utils/SkCanvasStack.h +FILE: ../../../third_party/skia/src/utils/SkCanvasStateUtils.cpp ---------------------------------------------------------------------------------------------------- -Copyright 2019 Google LLC +Copyright 2013 Google Inc. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -2932,209 +3189,87 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== LIBRARY: skia -ORIGIN: ../../../third_party/skia/bench/CanvasSaveRestoreBench.cpp + ../../../third_party/skia/LICENSE +ORIGIN: ../../../third_party/skia/bench/BulkRectBench.cpp + ../../../third_party/skia/LICENSE TYPE: LicenseType.bsd -FILE: ../../../third_party/skia/bench/CanvasSaveRestoreBench.cpp -FILE: ../../../third_party/skia/bench/TriangulatorBench.cpp -FILE: ../../../third_party/skia/experimental/graphite/include/Context.h -FILE: ../../../third_party/skia/experimental/graphite/include/GraphiteTypes.h -FILE: ../../../third_party/skia/experimental/graphite/include/SkStuff.h -FILE: ../../../third_party/skia/experimental/graphite/include/TextureInfo.h -FILE: ../../../third_party/skia/experimental/graphite/include/mtl/MtlBackendContext.h -FILE: ../../../third_party/skia/experimental/graphite/include/mtl/MtlTypes.h -FILE: ../../../third_party/skia/experimental/graphite/include/private/GraphiteTypesPriv.h -FILE: ../../../third_party/skia/experimental/graphite/src/Caps.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/Caps.h -FILE: ../../../third_party/skia/experimental/graphite/src/CommandBuffer.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/CommandBuffer.h -FILE: ../../../third_party/skia/experimental/graphite/src/Context.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/ContextPriv.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/ContextPriv.h -FILE: ../../../third_party/skia/experimental/graphite/src/Device.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/Device.h -FILE: ../../../third_party/skia/experimental/graphite/src/DrawContext.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/DrawContext.h -FILE: ../../../third_party/skia/experimental/graphite/src/DrawList.h -FILE: ../../../third_party/skia/experimental/graphite/src/DrawPass.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/DrawPass.h -FILE: ../../../third_party/skia/experimental/graphite/src/Gpu.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/Gpu.h -FILE: ../../../third_party/skia/experimental/graphite/src/GpuWorkSubmission.h -FILE: ../../../third_party/skia/experimental/graphite/src/Image_Graphite.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/Image_Graphite.h -FILE: ../../../third_party/skia/experimental/graphite/src/Recorder.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/Recorder.h -FILE: ../../../third_party/skia/experimental/graphite/src/Recording.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/Recording.h -FILE: ../../../third_party/skia/experimental/graphite/src/RenderPassTask.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/RenderPassTask.h -FILE: ../../../third_party/skia/experimental/graphite/src/RenderPipeline.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/RenderPipeline.h -FILE: ../../../third_party/skia/experimental/graphite/src/RenderPipelineDesc.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/RenderPipelineDesc.h -FILE: ../../../third_party/skia/experimental/graphite/src/ResourceProvider.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/ResourceProvider.h -FILE: ../../../third_party/skia/experimental/graphite/src/SkStuff.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/Surface_Graphite.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/Surface_Graphite.h -FILE: ../../../third_party/skia/experimental/graphite/src/Task.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/Task.h -FILE: ../../../third_party/skia/experimental/graphite/src/TaskGraph.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/TaskGraph.h -FILE: ../../../third_party/skia/experimental/graphite/src/Texture.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/Texture.h -FILE: ../../../third_party/skia/experimental/graphite/src/TextureInfo.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/TextureProxy.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/TextureProxy.h -FILE: ../../../third_party/skia/experimental/graphite/src/geom/BoundsManager.h -FILE: ../../../third_party/skia/experimental/graphite/src/geom/Rect.h -FILE: ../../../third_party/skia/experimental/graphite/src/geom/Shape.cpp -FILE: ../../../third_party/skia/experimental/graphite/src/geom/Shape.h -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlCaps.h -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlCaps.mm -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlCommandBuffer.h -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlCommandBuffer.mm -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlGpu.h -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlGpu.mm -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlRenderPipeline.h -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlRenderPipeline.mm -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlResourceProvider.h -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlResourceProvider.mm -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlTexture.h -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlTexture.mm -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlTypesPriv.mm -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlUtils.h -FILE: ../../../third_party/skia/experimental/lowp-basic/QMath.h -FILE: ../../../third_party/skia/experimental/lowp-basic/bilerp-study.cpp -FILE: ../../../third_party/skia/experimental/lowp-basic/lerp-study.cpp -FILE: ../../../third_party/skia/experimental/lowp-basic/lowp_experiments.cpp -FILE: ../../../third_party/skia/experimental/tskit/bindings/bindings.h -FILE: ../../../third_party/skia/experimental/tskit/bindings/core.cpp -FILE: ../../../third_party/skia/experimental/tskit/bindings/extension.cpp -FILE: ../../../third_party/skia/experimental/webgpu-bazel/src/bindings.cpp -FILE: ../../../third_party/skia/gm/composecolorfilter.cpp -FILE: ../../../third_party/skia/gm/crbug_1167277.cpp -FILE: ../../../third_party/skia/gm/crbug_1174186.cpp -FILE: ../../../third_party/skia/gm/crbug_1174354.cpp -FILE: ../../../third_party/skia/gm/crbug_1177833.cpp -FILE: ../../../third_party/skia/gm/crbug_1257515.cpp -FILE: ../../../third_party/skia/gm/crop_imagefilter.cpp -FILE: ../../../third_party/skia/gm/fillrect_gradient.cpp -FILE: ../../../third_party/skia/gm/hardstop_gradients_many.cpp -FILE: ../../../third_party/skia/gm/lazytiling.cpp -FILE: ../../../third_party/skia/gm/particles.cpp -FILE: ../../../third_party/skia/include/core/SkBlender.h -FILE: ../../../third_party/skia/include/effects/SkBlenders.h -FILE: ../../../third_party/skia/include/gpu/GrSurfaceInfo.h -FILE: ../../../third_party/skia/include/gpu/gl/egl/GrGLMakeEGLInterface.h -FILE: ../../../third_party/skia/include/gpu/gl/glx/GrGLMakeGLXInterface.h -FILE: ../../../third_party/skia/include/private/GrDawnTypesPriv.h -FILE: ../../../third_party/skia/include/private/GrMockTypesPriv.h -FILE: ../../../third_party/skia/include/sksl/DSLSymbols.h -FILE: ../../../third_party/skia/modules/canvaskit/paragraph_bindings_gen.cpp -FILE: ../../../third_party/skia/src/core/SkBlendModeBlender.cpp -FILE: ../../../third_party/skia/src/core/SkBlendModeBlender.h -FILE: ../../../third_party/skia/src/core/SkBlenderBase.h -FILE: ../../../third_party/skia/src/core/SkMatrixInvert.cpp -FILE: ../../../third_party/skia/src/core/SkMatrixInvert.h -FILE: ../../../third_party/skia/src/core/SkVMBlitter.h -FILE: ../../../third_party/skia/src/core/SkYUVAInfoLocation.h -FILE: ../../../third_party/skia/src/effects/SkBlenders.cpp -FILE: ../../../third_party/skia/src/effects/imagefilters/SkCropImageFilter.cpp -FILE: ../../../third_party/skia/src/effects/imagefilters/SkCropImageFilter.h -FILE: ../../../third_party/skia/src/effects/imagefilters/SkRuntimeImageFilter.cpp -FILE: ../../../third_party/skia/src/effects/imagefilters/SkRuntimeImageFilter.h -FILE: ../../../third_party/skia/src/gpu/BaseDevice.cpp -FILE: ../../../third_party/skia/src/gpu/BaseDevice.h -FILE: ../../../third_party/skia/src/gpu/GrDstProxyView.h -FILE: ../../../third_party/skia/src/gpu/GrEagerVertexAllocator.cpp -FILE: ../../../third_party/skia/src/gpu/GrMeshDrawTarget.cpp -FILE: ../../../third_party/skia/src/gpu/GrMeshDrawTarget.h -FILE: ../../../third_party/skia/src/gpu/GrOpsTypes.h -FILE: ../../../third_party/skia/src/gpu/GrPersistentCacheUtils.cpp -FILE: ../../../third_party/skia/src/gpu/GrSubRunAllocator.cpp -FILE: ../../../third_party/skia/src/gpu/GrSubRunAllocator.h -FILE: ../../../third_party/skia/src/gpu/GrSurfaceInfo.cpp -FILE: ../../../third_party/skia/src/gpu/GrWritePixelsRenderTask.cpp -FILE: ../../../third_party/skia/src/gpu/GrWritePixelsRenderTask.h -FILE: ../../../third_party/skia/src/gpu/GrYUVATextureProxies.cpp -FILE: ../../../third_party/skia/src/gpu/GrYUVATextureProxies.h -FILE: ../../../third_party/skia/src/gpu/SurfaceFillContext.cpp -FILE: ../../../third_party/skia/src/gpu/SurfaceFillContext.h -FILE: ../../../third_party/skia/src/gpu/d3d/GrD3DTypesPriv.cpp -FILE: ../../../third_party/skia/src/gpu/dawn/GrDawnTypesPriv.cpp -FILE: ../../../third_party/skia/src/gpu/geometry/GrInnerFanTriangulator.h -FILE: ../../../third_party/skia/src/gpu/gl/egl/GrGLMakeNativeInterface_egl.cpp -FILE: ../../../third_party/skia/src/gpu/gl/glx/GrGLMakeNativeInterface_glx.cpp -FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlTypesPriv.mm -FILE: ../../../third_party/skia/src/gpu/tessellate/GrTessTypes.h -FILE: ../../../third_party/skia/src/gpu/v2/Device.cpp -FILE: ../../../third_party/skia/src/gpu/v2/Device_v2.h -FILE: ../../../third_party/skia/src/gpu/v2/SurfaceDrawContext.cpp -FILE: ../../../third_party/skia/src/gpu/v2/SurfaceDrawContext_v2.h -FILE: ../../../third_party/skia/src/gpu/v2/SurfaceFillContext_v2.cpp -FILE: ../../../third_party/skia/src/gpu/v2/SurfaceFillContext_v2.h -FILE: ../../../third_party/skia/src/gpu/vk/GrVkBuffer.cpp -FILE: ../../../third_party/skia/src/gpu/vk/GrVkBuffer.h -FILE: ../../../third_party/skia/src/sksl/SkSLBuiltinTypes.cpp -FILE: ../../../third_party/skia/src/sksl/SkSLBuiltinTypes.h -FILE: ../../../third_party/skia/src/sksl/SkSLContext.cpp -FILE: ../../../third_party/skia/src/sksl/SkSLIntrinsicList.h -FILE: ../../../third_party/skia/src/sksl/SkSLMangler.h -FILE: ../../../third_party/skia/src/sksl/SkSLOperators.cpp -FILE: ../../../third_party/skia/src/sksl/SkSLOperators.h -FILE: ../../../third_party/skia/src/sksl/SkSLThreadContext.h -FILE: ../../../third_party/skia/src/sksl/analysis/SkSLCanExitWithoutReturningValue.cpp -FILE: ../../../third_party/skia/src/sksl/analysis/SkSLCheckProgramUnrolledSize.cpp -FILE: ../../../third_party/skia/src/sksl/analysis/SkSLGetLoopUnrollInfo.cpp -FILE: ../../../third_party/skia/src/sksl/analysis/SkSLIsConstantExpression.cpp -FILE: ../../../third_party/skia/src/sksl/analysis/SkSLProgramUsage.cpp -FILE: ../../../third_party/skia/src/sksl/analysis/SkSLProgramVisitor.h -FILE: ../../../third_party/skia/src/sksl/analysis/SkSLSwitchCaseContainsExit.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLBinaryExpression.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLBlock.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLChildCall.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLChildCall.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorArray.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorArray.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorArrayCast.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorArrayCast.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorCompound.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorCompound.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorCompoundCast.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorCompoundCast.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorDiagonalMatrix.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorDiagonalMatrix.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorMatrixResize.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorMatrixResize.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorScalarCast.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorScalarCast.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorSplat.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorSplat.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorStruct.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLConstructorStruct.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLDoStatement.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLExpressionStatement.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLFieldAccess.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLForStatement.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLFunctionCall.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLFunctionDefinition.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLIfStatement.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLIndexExpression.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLMethodReference.h -FILE: ../../../third_party/skia/src/sksl/ir/SkSLPostfixExpression.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLSwitchStatement.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLSwizzle.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLTernaryExpression.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLTypeReference.cpp -FILE: ../../../third_party/skia/src/sksl/ir/SkSLVarDeclarations.cpp -FILE: ../../../third_party/skia/src/sksl/transform/SkSLEliminateDeadFunctions.cpp -FILE: ../../../third_party/skia/src/sksl/transform/SkSLEliminateDeadGlobalVariables.cpp -FILE: ../../../third_party/skia/src/sksl/transform/SkSLEliminateDeadLocalVariables.cpp -FILE: ../../../third_party/skia/src/sksl/transform/SkSLEliminateUnreachableCode.cpp -FILE: ../../../third_party/skia/src/sksl/transform/SkSLProgramWriter.h +FILE: ../../../third_party/skia/bench/BulkRectBench.cpp +FILE: ../../../third_party/skia/bench/DDLRecorderBench.cpp +FILE: ../../../third_party/skia/bench/SkSLBench.cpp +FILE: ../../../third_party/skia/experimental/wasm-skp-debugger/debugger_bindings.cpp +FILE: ../../../third_party/skia/gm/asyncrescaleandread.cpp +FILE: ../../../third_party/skia/gm/crbug_908646.cpp +FILE: ../../../third_party/skia/gm/crbug_946965.cpp +FILE: ../../../third_party/skia/gm/patharcto.cpp +FILE: ../../../third_party/skia/gm/runtimecolorfilter.cpp +FILE: ../../../third_party/skia/gm/runtimefunctions.cpp +FILE: ../../../third_party/skia/gm/runtimeintrinsics.cpp +FILE: ../../../third_party/skia/gm/runtimeshader.cpp +FILE: ../../../third_party/skia/gm/skbug_9319.cpp +FILE: ../../../third_party/skia/include/effects/SkImageFilters.h +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/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 +FILE: ../../../third_party/skia/modules/particles/include/SkParticleBinding.h +FILE: ../../../third_party/skia/modules/particles/include/SkParticleData.h +FILE: ../../../third_party/skia/modules/particles/include/SkParticleDrawable.h +FILE: ../../../third_party/skia/modules/particles/include/SkParticleEffect.h +FILE: ../../../third_party/skia/modules/particles/include/SkParticleSerialization.h +FILE: ../../../third_party/skia/modules/particles/include/SkReflected.h +FILE: ../../../third_party/skia/modules/particles/src/SkParticleBinding.cpp +FILE: ../../../third_party/skia/modules/particles/src/SkParticleDrawable.cpp +FILE: ../../../third_party/skia/modules/particles/src/SkParticleEffect.cpp +FILE: ../../../third_party/skia/modules/particles/src/SkReflected.cpp +FILE: ../../../third_party/skia/modules/skresources/include/SkResources.h +FILE: ../../../third_party/skia/modules/skresources/src/SkResources.cpp +FILE: ../../../third_party/skia/samplecode/SampleFilterBounds.cpp +FILE: ../../../third_party/skia/samplecode/SampleImageFilterDAG.cpp +FILE: ../../../third_party/skia/src/core/SkImageFilterTypes.cpp +FILE: ../../../third_party/skia/src/core/SkImageFilterTypes.h +FILE: ../../../third_party/skia/src/core/SkImageFilter_Base.h +FILE: ../../../third_party/skia/src/core/SkRuntimeEffect.cpp +FILE: ../../../third_party/skia/src/core/SkVM.cpp +FILE: ../../../third_party/skia/src/core/SkVM.h +FILE: ../../../third_party/skia/src/gpu/GrClientMappedBufferManager.cpp +FILE: ../../../third_party/skia/src/gpu/GrClientMappedBufferManager.h +FILE: ../../../third_party/skia/src/gpu/GrCopyRenderTask.cpp +FILE: ../../../third_party/skia/src/gpu/GrCopyRenderTask.h +FILE: ../../../third_party/skia/src/gpu/GrImageInfo.h +FILE: ../../../third_party/skia/src/gpu/GrPersistentCacheUtils.h +FILE: ../../../third_party/skia/src/gpu/GrProgramInfo.cpp +FILE: ../../../third_party/skia/src/gpu/GrProgramInfo.h +FILE: ../../../third_party/skia/src/gpu/GrRenderTask.cpp +FILE: ../../../third_party/skia/src/gpu/GrRenderTask.h +FILE: ../../../third_party/skia/src/gpu/GrShaderUtils.cpp +FILE: ../../../third_party/skia/src/gpu/GrShaderUtils.h +FILE: ../../../third_party/skia/src/gpu/GrSurfaceProxyView.h +FILE: ../../../third_party/skia/src/gpu/GrSwizzle.cpp +FILE: ../../../third_party/skia/src/gpu/GrTextureResolveManager.h +FILE: ../../../third_party/skia/src/gpu/GrTextureResolveRenderTask.cpp +FILE: ../../../third_party/skia/src/gpu/GrTextureResolveRenderTask.h +FILE: ../../../third_party/skia/src/gpu/GrTransferFromRenderTask.cpp +FILE: ../../../third_party/skia/src/gpu/GrTransferFromRenderTask.h +FILE: ../../../third_party/skia/src/gpu/GrUtil.h +FILE: ../../../third_party/skia/src/gpu/GrWaitRenderTask.cpp +FILE: ../../../third_party/skia/src/gpu/GrWaitRenderTask.h +FILE: ../../../third_party/skia/src/gpu/geometry/GrQuadBuffer.h +FILE: ../../../third_party/skia/src/gpu/geometry/GrQuadUtils.cpp +FILE: ../../../third_party/skia/src/gpu/geometry/GrQuadUtils.h +FILE: ../../../third_party/skia/src/gpu/gl/GrGLAssembleGLESInterfaceAutogen.cpp +FILE: ../../../third_party/skia/src/gpu/gl/GrGLAssembleGLInterfaceAutogen.cpp +FILE: ../../../third_party/skia/src/gpu/gl/GrGLAssembleHelpers.cpp +FILE: ../../../third_party/skia/src/gpu/gl/GrGLAssembleWebGLInterfaceAutogen.cpp +FILE: ../../../third_party/skia/src/gpu/gl/GrGLTypesPriv.cpp +FILE: ../../../third_party/skia/src/gpu/mock/GrMockCaps.cpp +FILE: ../../../third_party/skia/src/gpu/mock/GrMockTypes.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLExternalFunction.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLExternalFunctionCall.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLExternalFunctionReference.h ---------------------------------------------------------------------------------------------------- -Copyright 2021 Google LLC +Copyright 2019 Google LLC Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -3939,7 +4074,6 @@ FILE: ../../../third_party/skia/fuzz/FuzzSkParagraph.cpp FILE: ../../../third_party/skia/gm/3d.cpp FILE: ../../../third_party/skia/gm/bc1_transparency.cpp FILE: ../../../third_party/skia/gm/bicubic.cpp -FILE: ../../../third_party/skia/gm/colrv1.cpp FILE: ../../../third_party/skia/gm/compressed_textures.cpp FILE: ../../../third_party/skia/gm/crbug_1073670.cpp FILE: ../../../third_party/skia/gm/crbug_1086705.cpp @@ -4059,7 +4193,6 @@ FILE: ../../../third_party/skia/src/gpu/GrThreadSafeCache.cpp FILE: ../../../third_party/skia/src/gpu/GrThreadSafeCache.h FILE: ../../../third_party/skia/src/gpu/geometry/GrAATriangulator.cpp FILE: ../../../third_party/skia/src/gpu/geometry/GrAATriangulator.h -FILE: ../../../third_party/skia/src/gpu/geometry/GrWangsFormula.h FILE: ../../../third_party/skia/src/gpu/mock/GrMockOpTarget.h FILE: ../../../third_party/skia/src/gpu/ops/DrawAtlasPathOp.cpp FILE: ../../../third_party/skia/src/gpu/ops/DrawAtlasPathOp.h @@ -4069,9 +4202,9 @@ FILE: ../../../third_party/skia/src/gpu/ops/SmallPathAtlasMgr.cpp FILE: ../../../third_party/skia/src/gpu/ops/SmallPathAtlasMgr.h FILE: ../../../third_party/skia/src/gpu/ops/SmallPathShapeData.cpp FILE: ../../../third_party/skia/src/gpu/ops/SmallPathShapeData.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrMiddleOutPolygonTriangulator.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeIterator.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrVectorXform.h +FILE: ../../../third_party/skia/src/gpu/tessellate/MiddleOutPolygonTriangulator.h +FILE: ../../../third_party/skia/src/gpu/tessellate/StrokeIterator.h +FILE: ../../../third_party/skia/src/gpu/tessellate/WangsFormula.h FILE: ../../../third_party/skia/src/gpu/text/GrSDFTControl.cpp FILE: ../../../third_party/skia/src/gpu/text/GrSDFTControl.h FILE: ../../../third_party/skia/src/opts/SkOpts_skx.cpp @@ -4113,109 +4246,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== -==================================================================================================== -LIBRARY: skia -ORIGIN: ../../../third_party/skia/bench/MSKPBench.cpp + ../../../third_party/skia/LICENSE -TYPE: LicenseType.bsd -FILE: ../../../third_party/skia/bench/MSKPBench.cpp -FILE: ../../../third_party/skia/bench/MSKPBench.h -FILE: ../../../third_party/skia/experimental/graphite/include/private/MtlTypesPriv.h -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlTrampoline.h -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlTrampoline.mm -FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlUtils.mm -FILE: ../../../third_party/skia/fuzz/FuzzDDLThreading.cpp -FILE: ../../../third_party/skia/gm/aarecteffect.cpp -FILE: ../../../third_party/skia/gm/colorspace.cpp -FILE: ../../../third_party/skia/gm/drawglyphs.cpp -FILE: ../../../third_party/skia/gm/largeclippedpath.cpp -FILE: ../../../third_party/skia/gm/skbug_12212.cpp -FILE: ../../../third_party/skia/include/private/GrMtlTypesPriv.h -FILE: ../../../third_party/skia/include/utils/SkOrderedFontMgr.h -FILE: ../../../third_party/skia/infra/bots/task_drivers/recreate_skps/recreate_skps.go -FILE: ../../../third_party/skia/modules/androidkit/src/AndroidKit.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/Canvas.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/ColorFilters.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/Font.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/Gradients.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/Image.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/ImageFilter.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/Matrix.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/Paint.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/Path.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/PathBuilder.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/RuntimeShaderBuilder.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/Shader.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/SkottieAnimation.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/Surface.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/Surface.h -FILE: ../../../third_party/skia/modules/androidkit/src/SurfaceThread.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/SurfaceThread.h -FILE: ../../../third_party/skia/modules/androidkit/src/Text.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/Utils.cpp -FILE: ../../../third_party/skia/modules/androidkit/src/Utils.h -FILE: ../../../third_party/skia/modules/skottie/src/effects/FractalNoiseEffect.cpp -FILE: ../../../third_party/skia/modules/skottie/src/effects/SphereEffect.cpp -FILE: ../../../third_party/skia/modules/skottie/src/effects/ThresholdEffect.cpp -FILE: ../../../third_party/skia/modules/skunicode/src/SkUnicode_icu.h -FILE: ../../../third_party/skia/modules/skunicode/src/SkUnicode_icu_builtin.cpp -FILE: ../../../third_party/skia/modules/skunicode/src/SkUnicode_icu_runtime.cpp -FILE: ../../../third_party/skia/modules/svg/include/SkSVGFeImage.h -FILE: ../../../third_party/skia/modules/svg/include/SkSVGFeLightSource.h -FILE: ../../../third_party/skia/modules/svg/include/SkSVGImage.h -FILE: ../../../third_party/skia/modules/svg/include/SkSVGMask.h -FILE: ../../../third_party/skia/modules/svg/src/SkSVGFeImage.cpp -FILE: ../../../third_party/skia/modules/svg/src/SkSVGFeLightSource.cpp -FILE: ../../../third_party/skia/modules/svg/src/SkSVGImage.cpp -FILE: ../../../third_party/skia/modules/svg/src/SkSVGMask.cpp -FILE: ../../../third_party/skia/src/gpu/GrDrawIndirectCommand.h -FILE: ../../../third_party/skia/src/gpu/GrRenderTaskCluster.cpp -FILE: ../../../third_party/skia/src/gpu/GrRenderTaskCluster.h -FILE: ../../../third_party/skia/src/gpu/GrThreadSafePipelineBuilder.cpp -FILE: ../../../third_party/skia/src/gpu/GrThreadSafePipelineBuilder.h -FILE: ../../../third_party/skia/src/gpu/mock/GrMockRenderTask.h -FILE: ../../../third_party/skia/src/gpu/mock/GrMockSurfaceProxy.h -FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlFramebuffer.h -FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlFramebuffer.mm -FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlPipeline.h -FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlRenderCommandEncoder.h -FILE: ../../../third_party/skia/src/gpu/ops/AtlasRenderTask.cpp -FILE: ../../../third_party/skia/src/gpu/ops/AtlasRenderTask.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrCullTest.h -FILE: ../../../third_party/skia/src/shaders/SkTransformShader.cpp -FILE: ../../../third_party/skia/src/shaders/SkTransformShader.h -FILE: ../../../third_party/skia/src/utils/SkOrderedFontMgr.cpp ----------------------------------------------------------------------------------------------------- -Copyright 2021 Google Inc. - -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 the copyright holder 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. -==================================================================================================== - ==================================================================================================== LIBRARY: skia ORIGIN: ../../../third_party/skia/bench/ReadPixBench.cpp + ../../../third_party/skia/LICENSE @@ -5384,6 +5414,129 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== +==================================================================================================== +LIBRARY: skia +ORIGIN: ../../../third_party/skia/bench/graphite/IntersectionTreeBench.cpp + ../../../third_party/skia/LICENSE +TYPE: LicenseType.bsd +FILE: ../../../third_party/skia/bench/MSKPBench.cpp +FILE: ../../../third_party/skia/bench/MSKPBench.h +FILE: ../../../third_party/skia/bench/graphite/IntersectionTreeBench.cpp +FILE: ../../../third_party/skia/experimental/graphite/include/private/MtlTypesPriv.h +FILE: ../../../third_party/skia/experimental/graphite/src/Buffer.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/Buffer.h +FILE: ../../../third_party/skia/experimental/graphite/src/DrawBufferManager.cpp +FILE: ../../../third_party/skia/experimental/graphite/src/DrawBufferManager.h +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlBlitCommandEncoder.h +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlBuffer.h +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlBuffer.mm +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlRenderCommandEncoder.h +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlTrampoline.h +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlTrampoline.mm +FILE: ../../../third_party/skia/experimental/graphite/src/mtl/MtlUtils.mm +FILE: ../../../third_party/skia/fuzz/FuzzDDLThreading.cpp +FILE: ../../../third_party/skia/gm/aarecteffect.cpp +FILE: ../../../third_party/skia/gm/colorspace.cpp +FILE: ../../../third_party/skia/gm/colrv1.cpp +FILE: ../../../third_party/skia/gm/drawglyphs.cpp +FILE: ../../../third_party/skia/gm/largeclippedpath.cpp +FILE: ../../../third_party/skia/gm/skbug_12212.cpp +FILE: ../../../third_party/skia/include/private/GrMtlTypesPriv.h +FILE: ../../../third_party/skia/include/utils/SkOrderedFontMgr.h +FILE: ../../../third_party/skia/infra/bots/task_drivers/recreate_skps/recreate_skps.go +FILE: ../../../third_party/skia/modules/androidkit/src/AndroidKit.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/Canvas.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/ColorFilters.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/Font.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/FontChainAdapter.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/Gradients.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/Image.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/ImageFilter.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/Matrix.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/Paint.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/Path.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/PathBuilder.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/RuntimeShaderBuilder.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/Shader.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/SkottieAnimation.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/Surface.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/Surface.h +FILE: ../../../third_party/skia/modules/androidkit/src/SurfaceThread.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/SurfaceThread.h +FILE: ../../../third_party/skia/modules/androidkit/src/Text.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/Utils.cpp +FILE: ../../../third_party/skia/modules/androidkit/src/Utils.h +FILE: ../../../third_party/skia/modules/skottie/src/effects/BuldgeEffect.cpp +FILE: ../../../third_party/skia/modules/skottie/src/effects/CCTonerEffect.cpp +FILE: ../../../third_party/skia/modules/skottie/src/effects/DirectionalBlur.cpp +FILE: ../../../third_party/skia/modules/skottie/src/effects/FractalNoiseEffect.cpp +FILE: ../../../third_party/skia/modules/skottie/src/effects/SkSLEffect.cpp +FILE: ../../../third_party/skia/modules/skottie/src/effects/SphereEffect.cpp +FILE: ../../../third_party/skia/modules/skottie/src/effects/ThresholdEffect.cpp +FILE: ../../../third_party/skia/modules/skunicode/src/SkUnicode_icu.h +FILE: ../../../third_party/skia/modules/skunicode/src/SkUnicode_icu_builtin.cpp +FILE: ../../../third_party/skia/modules/skunicode/src/SkUnicode_icu_runtime.cpp +FILE: ../../../third_party/skia/modules/svg/include/SkSVGFeImage.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGFeLightSource.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGImage.h +FILE: ../../../third_party/skia/modules/svg/include/SkSVGMask.h +FILE: ../../../third_party/skia/modules/svg/src/SkSVGFeImage.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGFeLightSource.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGImage.cpp +FILE: ../../../third_party/skia/modules/svg/src/SkSVGMask.cpp +FILE: ../../../third_party/skia/src/gpu/GrDrawIndirectCommand.h +FILE: ../../../third_party/skia/src/gpu/GrRenderTaskCluster.cpp +FILE: ../../../third_party/skia/src/gpu/GrRenderTaskCluster.h +FILE: ../../../third_party/skia/src/gpu/GrThreadSafePipelineBuilder.cpp +FILE: ../../../third_party/skia/src/gpu/GrThreadSafePipelineBuilder.h +FILE: ../../../third_party/skia/src/gpu/mock/GrMockRenderTask.h +FILE: ../../../third_party/skia/src/gpu/mock/GrMockSurfaceProxy.h +FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlFramebuffer.h +FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlFramebuffer.mm +FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlPipeline.h +FILE: ../../../third_party/skia/src/gpu/mtl/GrMtlRenderCommandEncoder.h +FILE: ../../../third_party/skia/src/gpu/ops/AtlasRenderTask.cpp +FILE: ../../../third_party/skia/src/gpu/ops/AtlasRenderTask.h +FILE: ../../../third_party/skia/src/gpu/tessellate/CullTest.h +FILE: ../../../third_party/skia/src/gpu/tessellate/Tessellation.h +FILE: ../../../third_party/skia/src/shaders/SkTransformShader.cpp +FILE: ../../../third_party/skia/src/shaders/SkTransformShader.h +FILE: ../../../third_party/skia/src/sksl/SkSLGLSL.h +FILE: ../../../third_party/skia/src/sksl/ir/SkSLExpression.cpp +FILE: ../../../third_party/skia/src/sksl/lex/TransitionTable.cpp +FILE: ../../../third_party/skia/src/sksl/lex/TransitionTable.h +FILE: ../../../third_party/skia/src/utils/SkOrderedFontMgr.cpp +---------------------------------------------------------------------------------------------------- +Copyright 2021 Google Inc. + +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 the copyright holder 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. +==================================================================================================== + ==================================================================================================== LIBRARY: skia ORIGIN: ../../../third_party/skia/docs/examples/50_percent_gray.cpp + ../../../third_party/skia/LICENSE @@ -5555,6 +5708,7 @@ FILE: ../../../third_party/skia/docs/examples/unexpected_setAlphaType.cpp FILE: ../../../third_party/skia/docs/examples/upscale_checkerboard.cpp FILE: ../../../third_party/skia/docs/examples/weird_RRect_bug.cpp FILE: ../../../third_party/skia/docs/examples/zero_off_dashing.cpp +FILE: ../../../third_party/skia/experimental/bazel_test/bazel_test.cpp FILE: ../../../third_party/skia/gm/clear_swizzle.cpp FILE: ../../../third_party/skia/gm/gpu_blur_utils.cpp FILE: ../../../third_party/skia/src/gpu/GrFinishCallbacks.cpp @@ -5563,8 +5717,8 @@ FILE: ../../../third_party/skia/src/gpu/GrVx.h FILE: ../../../third_party/skia/src/gpu/ops/AtlasInstancedHelper.cpp FILE: ../../../third_party/skia/src/gpu/ops/StrokeTessellateOp.cpp FILE: ../../../third_party/skia/src/gpu/ops/StrokeTessellateOp.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeHardwareTessellator.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeHardwareTessellator.h +FILE: ../../../third_party/skia/src/gpu/tessellate/StrokeHardwareTessellator.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/StrokeHardwareTessellator.h FILE: ../../../third_party/skia/src/gpu/tessellate/shaders/GrStrokeTessellationShader.cpp FILE: ../../../third_party/skia/src/gpu/tessellate/shaders/GrStrokeTessellationShader.h FILE: ../../../third_party/skia/src/gpu/tessellate/shaders/GrStrokeTessellationShader_HardwareImpl.cpp @@ -5651,7 +5805,6 @@ FILE: ../../../third_party/skia/modules/skottie/src/Composition.cpp FILE: ../../../third_party/skia/modules/skottie/src/Composition.h FILE: ../../../third_party/skia/modules/skottie/src/Layer.cpp FILE: ../../../third_party/skia/modules/skottie/src/Layer.h -FILE: ../../../third_party/skia/modules/skottie/src/effects/CCToner.cpp FILE: ../../../third_party/skia/modules/skottie/src/effects/DropShadowEffect.cpp FILE: ../../../third_party/skia/modules/skottie/src/effects/Effects.cpp FILE: ../../../third_party/skia/modules/skottie/src/effects/Effects.h @@ -5813,6 +5966,7 @@ FILE: ../../../third_party/skia/experimental/sorttoy/Fake.h FILE: ../../../third_party/skia/experimental/sorttoy/SortKey.h FILE: ../../../third_party/skia/experimental/sorttoy/sorttoy.cpp FILE: ../../../third_party/skia/experimental/sorttoy/sorttypes.h +FILE: ../../../third_party/skia/gm/batchedconvexpaths.cpp FILE: ../../../third_party/skia/gm/destcolor.cpp FILE: ../../../third_party/skia/gm/dsl_processor_test.cpp FILE: ../../../third_party/skia/include/core/SkStringView.h @@ -5836,15 +5990,18 @@ FILE: ../../../third_party/skia/src/gpu/ops/PathStencilCoverOp.cpp FILE: ../../../third_party/skia/src/gpu/ops/PathStencilCoverOp.h FILE: ../../../third_party/skia/src/gpu/ops/PathTessellateOp.cpp FILE: ../../../third_party/skia/src/gpu/ops/PathTessellateOp.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrPathCurveTessellator.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrPathCurveTessellator.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrPathTessellator.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrPathWedgeTessellator.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrPathWedgeTessellator.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrPathXform.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeFixedCountTessellator.cpp -FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeFixedCountTessellator.h -FILE: ../../../third_party/skia/src/gpu/tessellate/GrStrokeTessellator.h +FILE: ../../../third_party/skia/src/gpu/tessellate/AffineMatrix.h +FILE: ../../../third_party/skia/src/gpu/tessellate/PatchWriter.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/PatchWriter.h +FILE: ../../../third_party/skia/src/gpu/tessellate/PathCurveTessellator.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/PathCurveTessellator.h +FILE: ../../../third_party/skia/src/gpu/tessellate/PathTessellator.h +FILE: ../../../third_party/skia/src/gpu/tessellate/PathWedgeTessellator.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/PathWedgeTessellator.h +FILE: ../../../third_party/skia/src/gpu/tessellate/StrokeFixedCountTessellator.cpp +FILE: ../../../third_party/skia/src/gpu/tessellate/StrokeFixedCountTessellator.h +FILE: ../../../third_party/skia/src/gpu/tessellate/StrokeTessellator.h +FILE: ../../../third_party/skia/src/gpu/tessellate/Tessellation.cpp FILE: ../../../third_party/skia/src/gpu/tessellate/shaders/GrTessellationShader.cpp FILE: ../../../third_party/skia/src/sksl/SkSLDSLParser.cpp FILE: ../../../third_party/skia/src/sksl/SkSLDSLParser.h @@ -5862,6 +6019,7 @@ FILE: ../../../third_party/skia/src/sksl/dsl/priv/DSLFPs.cpp FILE: ../../../third_party/skia/src/sksl/dsl/priv/DSLFPs.h FILE: ../../../third_party/skia/src/sksl/dsl/priv/DSL_priv.h FILE: ../../../third_party/skia/src/sksl/ir/SkSLFunctionDeclaration.cpp +FILE: ../../../third_party/skia/src/sksl/ir/SkSLModifiers.cpp FILE: ../../../third_party/skia/src/sksl/ir/SkSLPoison.h FILE: ../../../third_party/skia/src/sksl/ir/SkSLVariable.cpp FILE: ../../../third_party/skia/src/sksl/transform/SkSLBuiltinVariableScanner.cpp @@ -6610,6 +6768,7 @@ 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/core/SkTBlockList.h +FILE: ../../../third_party/skia/src/gpu/BufferWriter.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 @@ -6619,7 +6778,6 @@ FILE: ../../../third_party/skia/src/gpu/GrGlyph.h FILE: ../../../third_party/skia/src/gpu/GrGpu.cpp FILE: ../../../third_party/skia/src/gpu/GrRectanizer.h FILE: ../../../third_party/skia/src/gpu/GrRectanizerPow2.cpp -FILE: ../../../third_party/skia/src/gpu/GrVertexWriter.h FILE: ../../../third_party/skia/src/gpu/SkGr.cpp FILE: ../../../third_party/skia/src/gpu/geometry/GrRect.h FILE: ../../../third_party/skia/src/gpu/v1/Device_v1.h @@ -6742,7 +6900,6 @@ FILE: ../../../third_party/skia/infra/bots/gen_tasks_logic/job_builder.go FILE: ../../../third_party/skia/infra/bots/gen_tasks_logic/nano_flags.go FILE: ../../../third_party/skia/infra/bots/gen_tasks_logic/schema.go FILE: ../../../third_party/skia/infra/bots/gen_tasks_logic/task_builder.go -FILE: ../../../third_party/skia/infra/bots/task_drivers/canary/canary.go FILE: ../../../third_party/skia/infra/bots/task_drivers/compile_wasm_gm_tests/compile_wasm_gm_tests.go FILE: ../../../third_party/skia/infra/bots/task_drivers/fm_driver/fm_driver.go FILE: ../../../third_party/skia/infra/bots/task_drivers/g3_canary/g3_canary.go @@ -7577,4 +7734,4 @@ 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. ==================================================================================================== -Total license count: 65 +Total license count: 67 diff --git a/ci/licenses_golden/licenses_third_party b/ci/licenses_golden/licenses_third_party index a6911c9d7b1a3..47f7c61f2aca0 100644 --- a/ci/licenses_golden/licenses_third_party +++ b/ci/licenses_golden/licenses_third_party @@ -1,4 +1,4 @@ -Signature: 54dcf784ac6819534ce6d1938e91ba91 +Signature: fc54f271ff2fc1a2dd860c34464f5f7c UNUSED LICENSES: @@ -9787,6 +9787,11 @@ FILE: ../../../third_party/dart/benchmarks/MapLookup/dart/maps.dart FILE: ../../../third_party/dart/benchmarks/MapLookup/dart2/MapLookup.dart FILE: ../../../third_party/dart/benchmarks/MapLookup/dart2/maps.dart FILE: ../../../third_party/dart/benchmarks/MapLookup/generate_maps.dart +FILE: ../../../third_party/dart/benchmarks/NativeCall/dart/NativeCall.dart +FILE: ../../../third_party/dart/benchmarks/NativeCall/dart/dlopen_helper.dart +FILE: ../../../third_party/dart/benchmarks/NativeCall/dart2/NativeCall.dart +FILE: ../../../third_party/dart/benchmarks/NativeCall/dart2/dlopen_helper.dart +FILE: ../../../third_party/dart/benchmarks/NativeCall/native/native_functions.c FILE: ../../../third_party/dart/benchmarks/ObjectHash/dart/ObjectHash.dart FILE: ../../../third_party/dart/benchmarks/ObjectHash/dart2/ObjectHash.dart FILE: ../../../third_party/dart/benchmarks/SDKArtifactSizes/dart/SDKArtifactSizes.dart @@ -9801,6 +9806,8 @@ FILE: ../../../third_party/dart/benchmarks/StringPool/dart/version2.dart FILE: ../../../third_party/dart/benchmarks/StringPool/dart2/StringPool.dart FILE: ../../../third_party/dart/benchmarks/StringPool/dart2/StringPool100.dart FILE: ../../../third_party/dart/runtime/bin/abstract_socket_test.cc +FILE: ../../../third_party/dart/runtime/bin/platform_macos_cocoa.h +FILE: ../../../third_party/dart/runtime/bin/platform_macos_cocoa.mm FILE: ../../../third_party/dart/runtime/bin/secure_socket_utils_test.cc FILE: ../../../third_party/dart/runtime/bin/utils.cc FILE: ../../../third_party/dart/runtime/bin/virtual_memory.h @@ -9913,7 +9920,6 @@ FILE: ../../../third_party/dart/runtime/bin/ffi_test/ffi_test_functions_generate FILE: ../../../third_party/dart/runtime/bin/ffi_unit_test/run_ffi_unit_tests.cc FILE: ../../../third_party/dart/runtime/bin/file_win.h FILE: ../../../third_party/dart/runtime/bin/platform_macos.h -FILE: ../../../third_party/dart/runtime/bin/platform_macos_test.cc FILE: ../../../third_party/dart/runtime/bin/priority_heap_test.cc FILE: ../../../third_party/dart/runtime/include/dart_api_dl.c FILE: ../../../third_party/dart/runtime/include/dart_api_dl.h @@ -10517,7 +10523,6 @@ FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/objectpool_ FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/persistent_handles.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/ports.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/timeline_page.dart -FILE: ../../../third_party/dart/runtime/observatory/lib/src/elements/view_footer.dart FILE: ../../../third_party/dart/runtime/observatory/lib/src/sample_profile/sample_profile.dart FILE: ../../../third_party/dart/runtime/observatory/web/timeline.js FILE: ../../../third_party/dart/runtime/observatory_2/lib/allocation_profile.dart @@ -10536,7 +10541,6 @@ FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/objectpoo FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/persistent_handles.dart FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/ports.dart FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/timeline_page.dart -FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/elements/view_footer.dart FILE: ../../../third_party/dart/runtime/observatory_2/lib/src/sample_profile/sample_profile.dart FILE: ../../../third_party/dart/runtime/observatory_2/web/timeline.js FILE: ../../../third_party/dart/runtime/vm/atomic_test.cc diff --git a/ci/lint.sh b/ci/lint.sh index eeddd3c7b4ce3..6355e637fd1e1 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -41,6 +41,5 @@ cd "$SCRIPT_DIR" "$DART" \ --disable-dart-dev \ "$SRC_DIR/flutter/tools/clang_tidy/bin/main.dart" \ - --compile-commands="$COMPILE_COMMANDS" \ - --repo="$SRC_DIR/flutter" \ + --src-dir="$SRC_DIR" \ "$@" diff --git a/common/config.gni b/common/config.gni index 11b4ba0c9288b..57292c750d9f4 100644 --- a/common/config.gni +++ b/common/config.gni @@ -69,13 +69,22 @@ if (is_ios || is_mac) { # prebuilt Dart SDK location if (flutter_prebuilt_dart_sdk) { - _os_name = target_os - if (_os_name == "mac") { - _os_name = "macos" - } else if (_os_name == "win" || _os_name == "winuwp") { - _os_name = "windows" + _target_os_name = target_os + if (_target_os_name == "mac") { + _target_os_name = "macos" + } else if (_target_os_name == "win" || _target_os_name == "winuwp") { + _target_os_name = "windows" } + + _host_os_name = host_os + if (_host_os_name == "mac") { + _host_os_name = "macos" + } else if (_host_os_name == "win" || _host_os_name == "winuwp") { + _host_os_name = "windows" + } + target_prebuilt_dart_sdk = - "//flutter/prebuilts/$_os_name-$target_cpu/dart-sdk" - host_prebuilt_dart_sdk = "//flutter/prebuilts/$_os_name-$host_cpu/dart-sdk" + "//flutter/prebuilts/$_target_os_name-$target_cpu/dart-sdk" + host_prebuilt_dart_sdk = + "//flutter/prebuilts/$_host_os_name-$host_cpu/dart-sdk" } diff --git a/common/graphics/gl_context_switch.h b/common/graphics/gl_context_switch.h index 18710ce627a60..1f5a89164eec3 100644 --- a/common/graphics/gl_context_switch.h +++ b/common/graphics/gl_context_switch.h @@ -57,7 +57,7 @@ class GLContextResult { bool GetResult(); protected: - GLContextResult(bool static_result); + explicit GLContextResult(bool static_result); bool result_; FML_DISALLOW_COPY_AND_ASSIGN(GLContextResult); @@ -78,7 +78,7 @@ class GLContextDefaultResult : public GLContextResult { /// /// @param static_result a static value that will be returned from /// |GetResult| - GLContextDefaultResult(bool static_result); + explicit GLContextDefaultResult(bool static_result); ~GLContextDefaultResult() override; @@ -99,7 +99,7 @@ class GLContextSwitch final : public GLContextResult { /// @param context The context that is going to be set as the current /// context. The |GLContextSwitch| should not outlive the owner of the gl /// context wrapped inside the `context`. - GLContextSwitch(std::unique_ptr context); + explicit GLContextSwitch(std::unique_ptr context); ~GLContextSwitch() override; diff --git a/common/graphics/persistent_cache.cc b/common/graphics/persistent_cache.cc index 88889b757b780..5c38120bd5b2f 100644 --- a/common/graphics/persistent_cache.cc +++ b/common/graphics/persistent_cache.cc @@ -253,7 +253,7 @@ std::vector PersistentCache::LoadSkSLs() const { rapidjson::ParseResult parse_result = json_doc.Parse(reinterpret_cast(mapping->GetMapping()), mapping->GetSize()); - if (parse_result != rapidjson::ParseErrorCode::kParseErrorNone) { + if (parse_result.IsError()) { FML_LOG(ERROR) << "Failed to parse json file: " << kAssetFileName; } else { for (auto& item : json_doc["data"].GetObject()) { diff --git a/common/graphics/persistent_cache.h b/common/graphics/persistent_cache.h index b10651876b940..3266654069e39 100644 --- a/common/graphics/persistent_cache.h +++ b/common/graphics/persistent_cache.h @@ -59,7 +59,7 @@ class PersistentCache : public GrContextOptions::PersistentCache { static const uint32_t kSignature = 0xA869593F; static const uint32_t kVersion1 = 1; - CacheObjectHeader(uint32_t p_key_size) : key_size(p_key_size) {} + explicit CacheObjectHeader(uint32_t p_key_size) : key_size(p_key_size) {} uint32_t signature = kSignature; uint32_t version = kVersion1; @@ -163,7 +163,7 @@ class PersistentCache : public GrContextOptions::PersistentCache { bool IsValid() const; - PersistentCache(bool read_only = false); + explicit PersistentCache(bool read_only = false); // |GrContextOptions::PersistentCache| void store(const SkData& key, const SkData& data) override; diff --git a/common/settings.h b/common/settings.h index da4b7a1ec436f..b9b35b1a3b788 100644 --- a/common/settings.h +++ b/common/settings.h @@ -116,6 +116,10 @@ struct Settings { // case the primary path to the library can not be loaded. std::vector application_library_path; + // Path to a library containing compiled Dart code usable for launching + // the VM service isolate. + std::vector vmservice_snapshot_library_path; + std::string application_kernel_asset; // deprecated std::string application_kernel_list_asset; // deprecated MappingsCallback application_kernels; @@ -123,6 +127,8 @@ struct Settings { std::string temp_directory_path; std::vector dart_flags; // Arguments passed as a List to Dart's entrypoint function. + // TODO(93459): Remove it when it is no longer used. + // https://github.com/flutter/flutter/issues/93459 std::vector dart_entrypoint_args; // Isolate settings diff --git a/flow/BUILD.gn b/flow/BUILD.gn index fe9bb90c2e839..642541ee8dfc0 100644 --- a/flow/BUILD.gn +++ b/flow/BUILD.gn @@ -60,8 +60,6 @@ source_set("flow") { "layers/texture_layer.h", "layers/transform_layer.cc", "layers/transform_layer.h", - "matrix_decomposition.cc", - "matrix_decomposition.h", "paint_region.cc", "paint_region.h", "paint_utils.cc", @@ -160,7 +158,6 @@ if (enable_unittests) { "layers/shader_mask_layer_unittests.cc", "layers/texture_layer_unittests.cc", "layers/transform_layer_unittests.cc", - "matrix_decomposition_unittests.cc", "mutators_stack_unittests.cc", "raster_cache_unittests.cc", "rtree_unittests.cc", diff --git a/flow/compositor_context.cc b/flow/compositor_context.cc index b300d68f10d0f..5f38d9e2f0414 100644 --- a/flow/compositor_context.cc +++ b/flow/compositor_context.cc @@ -4,11 +4,45 @@ #include "flutter/flow/compositor_context.h" +#include #include "flutter/flow/layers/layer_tree.h" #include "third_party/skia/include/core/SkCanvas.h" namespace flutter { +std::optional FrameDamage::ComputeClipRect( + flutter::LayerTree& layer_tree) { + if (layer_tree.root_layer()) { + PaintRegionMap empty_paint_region_map; + DiffContext context(layer_tree.frame_size(), + layer_tree.device_pixel_ratio(), + layer_tree.paint_region_map(), + prev_layer_tree_ ? prev_layer_tree_->paint_region_map() + : empty_paint_region_map); + context.PushCullRect(SkRect::MakeIWH(layer_tree.frame_size().width(), + layer_tree.frame_size().height())); + { + DiffContext::AutoSubtreeRestore subtree(&context); + const Layer* prev_root_layer = nullptr; + if (!prev_layer_tree_ || + prev_layer_tree_->frame_size() != layer_tree.frame_size()) { + // If there is no previous layer tree assume the entire frame must be + // repainted. + context.MarkSubtreeDirty(SkRect::MakeIWH( + layer_tree.frame_size().width(), layer_tree.frame_size().height())); + } else { + prev_root_layer = prev_layer_tree_->root_layer(); + } + layer_tree.root_layer()->Diff(&context, prev_root_layer); + } + + damage_ = context.ComputeDamage(additional_damage_); + return SkRect::Make(damage_->buffer_damage); + } else { + return std::nullopt; + } +} + CompositorContext::CompositorContext(fml::Milliseconds frame_budget) : raster_time_(frame_budget), ui_time_(frame_budget) {} @@ -24,7 +58,6 @@ void CompositorContext::BeginFrame(ScopedFrame& frame, void CompositorContext::EndFrame(ScopedFrame& frame, bool enable_instrumentation) { - raster_cache_.SweepAfterFrame(); if (enable_instrumentation) { raster_time_.Stop(); } @@ -69,9 +102,15 @@ CompositorContext::ScopedFrame::~ScopedFrame() { RasterStatus CompositorContext::ScopedFrame::Raster( flutter::LayerTree& layer_tree, - bool ignore_raster_cache) { + bool ignore_raster_cache, + FrameDamage* frame_damage) { TRACE_EVENT0("flutter", "CompositorContext::ScopedFrame::Raster"); - bool root_needs_readback = layer_tree.Preroll(*this, ignore_raster_cache); + + std::optional clip_rect = + frame_damage ? frame_damage->ComputeClipRect(layer_tree) : std::nullopt; + + bool root_needs_readback = layer_tree.Preroll( + *this, ignore_raster_cache, clip_rect ? *clip_rect : kGiantRect); bool needs_save_layer = root_needs_readback && !surface_supports_readback(); PostPrerollResult post_preroll_result = PostPrerollResult::kSuccess; if (view_embedder_ && raster_thread_merger_) { @@ -85,11 +124,18 @@ RasterStatus CompositorContext::ScopedFrame::Raster( if (post_preroll_result == PostPrerollResult::kSkipAndRetryFrame) { return RasterStatus::kSkipAndRetry; } + + SkAutoCanvasRestore restore(canvas(), clip_rect.has_value()); + // Clearing canvas after preroll reduces one render target switch when preroll // paints some raster cache. if (canvas()) { + if (clip_rect) { + canvas()->clipRect(*clip_rect); + } + if (needs_save_layer) { - FML_LOG(INFO) << "Using SaveLayer to protect non-readback surface"; + TRACE_EVENT0("flutter", "Canvas::saveLayer"); SkRect bounds = SkRect::Make(layer_tree.frame_size()); SkPaint paint; paint.setBlendMode(SkBlendMode::kSrc); diff --git a/flow/compositor_context.h b/flow/compositor_context.h index 7c10f3b147e58..6a973d861bc23 100644 --- a/flow/compositor_context.h +++ b/flow/compositor_context.h @@ -9,6 +9,7 @@ #include #include "flutter/common/graphics/texture.h" +#include "flutter/flow/diff_context.h" #include "flutter/flow/embedded_views.h" #include "flutter/flow/instrumentation.h" #include "flutter/flow/raster_cache.h" @@ -54,6 +55,43 @@ enum class RasterStatus { kYielded, }; +class FrameDamage { + public: + // Sets previous layer tree for calculating frame damage. If not set, entire + // frame will be repainted. + void SetPreviousLayerTree(const LayerTree* prev_layer_tree) { + prev_layer_tree_ = prev_layer_tree; + } + + // Adds additional damage (accumulated for double / triple buffering). + // This is area that will be repainted alongside any changed part. + void AddAdditonalDamage(const SkIRect& damage) { + additional_damage_.join(damage); + } + + // Calculates clip rect for current rasterization. This is diff of layer tree + // and previous layer tree + any additional provideddamage. + // If previous layer tree is not specified, clip rect will be nulloptional, + // but the paint region of layer_tree will be calculated so that it can be + // used for diffing of subsequent frames. + std::optional ComputeClipRect(flutter::LayerTree& layer_tree); + + // See Damage::frame_damage. + std::optional GetFrameDamage() const { + return damage_ ? std::make_optional(damage_->frame_damage) : std::nullopt; + } + + // See Damage::buffer_damage. + std::optional GetBufferDamage() { + return damage_ ? std::make_optional(damage_->buffer_damage) : std::nullopt; + } + + private: + SkIRect additional_damage_ = SkIRect::MakeEmpty(); + std::optional damage_; + const LayerTree* prev_layer_tree_ = nullptr; +}; + class CompositorContext { public: class ScopedFrame { @@ -84,7 +122,8 @@ class CompositorContext { GrDirectContext* gr_context() const { return gr_context_; } virtual RasterStatus Raster(LayerTree& layer_tree, - bool ignore_raster_cache); + bool ignore_raster_cache, + FrameDamage* frame_damage); private: CompositorContext& context_; @@ -99,7 +138,8 @@ class CompositorContext { FML_DISALLOW_COPY_AND_ASSIGN(ScopedFrame); }; - CompositorContext(fml::Milliseconds frame_budget = fml::kDefaultFrameBudget); + explicit CompositorContext( + fml::Milliseconds frame_budget = fml::kDefaultFrameBudget); virtual ~CompositorContext(); diff --git a/flow/diff_context.cc b/flow/diff_context.cc index f38d83e261e4c..c9782594aff0a 100644 --- a/flow/diff_context.cc +++ b/flow/diff_context.cc @@ -7,8 +7,6 @@ namespace flutter { -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - DiffContext::DiffContext(SkISize frame_size, double frame_device_pixel_ratio, PaintRegionMap& this_frame_paint_region_map, @@ -22,6 +20,8 @@ DiffContext::DiffContext(SkISize frame_size, void DiffContext::BeginSubtree() { state_stack_.push_back(state_); state_.rect_index_ = rects_->size(); + state_.has_filter_bounds_adjustment = false; + state_.has_texture = false; if (state_.transform_override) { state_.transform = *state_.transform_override; state_.transform_override = std::nullopt; @@ -30,12 +30,19 @@ void DiffContext::BeginSubtree() { void DiffContext::EndSubtree() { FML_DCHECK(!state_stack_.empty()); + if (state_.has_filter_bounds_adjustment) { + filter_bounds_adjustment_stack_.pop_back(); + } state_ = std::move(state_stack_.back()); state_stack_.pop_back(); } DiffContext::State::State() - : dirty(false), cull_rect(kGiantRect), rect_index_(0) {} + : dirty(false), + cull_rect(kGiantRect), + rect_index_(0), + has_filter_bounds_adjustment(false), + has_texture(false) {} void DiffContext::PushTransform(const SkMatrix& transform) { state_.transform.preConcat(transform); @@ -45,6 +52,21 @@ void DiffContext::SetTransform(const SkMatrix& transform) { state_.transform_override = transform; } +void DiffContext::PushFilterBoundsAdjustment(FilterBoundsAdjustment filter) { + FML_DCHECK(state_.has_filter_bounds_adjustment == false); + state_.has_filter_bounds_adjustment = true; + filter_bounds_adjustment_stack_.push_back(filter); +} + +SkRect DiffContext::ApplyFilterBoundsAdjustment(SkRect rect) const { + // Apply filter bounds adjustment in reverse order + for (auto i = filter_bounds_adjustment_stack_.rbegin(); + i != filter_bounds_adjustment_stack_.rend(); ++i) { + rect = (*i)(rect); + } + return rect; +} + Damage DiffContext::ComputeDamage( const SkIRect& accumulated_buffer_damage) const { SkRect buffer_damage = SkRect::Make(accumulated_buffer_damage); @@ -96,14 +118,22 @@ void DiffContext::MarkSubtreeDirty(const PaintRegion& previous_paint_region) { state_.dirty = true; } +void DiffContext::MarkSubtreeDirty(const SkRect& previous_paint_region) { + FML_DCHECK(!IsSubtreeDirty()); + AddDamage(previous_paint_region); + state_.dirty = true; +} + void DiffContext::AddLayerBounds(const SkRect& rect) { // During painting we cull based on non-overriden transform and then // override the transform right before paint. Do the same thing here to get // identical paint rect. - auto transformed_rect = state_.transform.mapRect(rect); + auto transformed_rect = + ApplyFilterBoundsAdjustment(state_.transform.mapRect(rect)); if (transformed_rect.intersects(state_.cull_rect)) { auto paint_rect = state_.transform_override - ? state_.transform_override->mapRect(rect) + ? ApplyFilterBoundsAdjustment( + state_.transform_override->mapRect(rect)) : transformed_rect; rects_->push_back(paint_rect); if (IsSubtreeDirty()) { @@ -112,6 +142,16 @@ void DiffContext::AddLayerBounds(const SkRect& rect) { } } +void DiffContext::MarkSubtreeHasTextureLayer() { + // Set the has_texture flag on current state and all parent states. That + // way we'll know that we can't skip diff for retained layers because + // they contain a TextureLayer. + for (auto& state : state_stack_) { + state.has_texture = true; + } + state_.has_texture = true; +} + void DiffContext::AddExistingPaintRegion(const PaintRegion& region) { // Adding paint region for retained layer implies that current subtree is not // dirty, so we know, for example, that the inherited transforms must match @@ -134,7 +174,8 @@ PaintRegion DiffContext::CurrentSubtreeRegion() const { bool has_readback = std::any_of( readbacks_.begin(), readbacks_.end(), [&](const Readback& r) { return r.position >= state_.rect_index_; }); - return PaintRegion(rects_, state_.rect_index_, rects_->size(), has_readback); + return PaintRegion(rects_, state_.rect_index_, rects_->size(), has_readback, + state_.has_texture); } void DiffContext::AddDamage(const PaintRegion& damage) { @@ -176,6 +217,4 @@ void DiffContext::Statistics::LogStatistics() { #endif // !FLUTTER_RELEASE } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - } // namespace flutter diff --git a/flow/diff_context.h b/flow/diff_context.h index 04972bccc1113..b57df7a946366 100644 --- a/flow/diff_context.h +++ b/flow/diff_context.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_FLOW_DIFF_CONTEXT_H_ #define FLUTTER_FLOW_DIFF_CONTEXT_H_ +#include #include #include #include @@ -16,8 +17,6 @@ namespace flutter { -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - class Layer; // Represents area that needs to be updated in front buffer (frame_damage) and @@ -75,6 +74,14 @@ class DiffContext { // Pushes cull rect for current subtree bool PushCullRect(const SkRect& clip); + // Function that adjusts layer bounds (in device coordinates) depending + // on filter. + using FilterBoundsAdjustment = std::function; + + // Pushes filter bounds adjustment to current subtree. Every layer in this + // subtree will have bounds adjusted by this function. + void PushFilterBoundsAdjustment(FilterBoundsAdjustment filter); + // Returns transform matrix for current subtree const SkMatrix& GetTransform() const { return state_.transform; } @@ -93,9 +100,14 @@ class DiffContext { // added to damage. void MarkSubtreeDirty( const PaintRegion& previous_paint_region = PaintRegion()); + void MarkSubtreeDirty(const SkRect& previous_paint_region); bool IsSubtreeDirty() const { return state_.dirty; } + // Marks that current subtree contains a TextureLayer. This is needed to + // ensure that we'll Diff the TextureLayer even if inside retained layer. + void MarkSubtreeHasTextureLayer(); + // Add layer bounds to current paint region; rect is in "local" (layer) // coordinates. void AddLayerBounds(const SkRect& rect); @@ -191,6 +203,13 @@ class DiffContext { SkMatrix transform; std::optional transform_override; size_t rect_index_; + + // Whether this subtree has filter bounds adjustment function. If so, + // it will need to be removed from stack when subtree is closed. + bool has_filter_bounds_adjustment; + + // Whether there is a texture layer in this subtree. + bool has_texture; }; std::shared_ptr> rects_; @@ -198,6 +217,11 @@ class DiffContext { SkISize frame_size_; double frame_device_pixel_ratio_; std::vector state_stack_; + std::vector filter_bounds_adjustment_stack_; + + // Applies the filter bounds adjustment stack on provided rect. + // Rect must be in device coordinates. + SkRect ApplyFilterBoundsAdjustment(SkRect rect) const; SkRect damage_ = SkRect::MakeEmpty(); @@ -219,8 +243,6 @@ class DiffContext { Statistics statistics_; }; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - } // namespace flutter #endif // FLUTTER_FLOW_DIFF_CONTEXT_H_ diff --git a/flow/display_list.cc b/flow/display_list.cc index fb7dfd696fd08..4ed97a8766ef8 100644 --- a/flow/display_list.cc +++ b/flow/display_list.cc @@ -7,10 +7,7 @@ #include "flutter/flow/display_list.h" #include "flutter/flow/display_list_canvas.h" #include "flutter/flow/display_list_utils.h" -#include "flutter/fml/logging.h" -#include "third_party/skia/include/core/SkImageFilter.h" -#include "third_party/skia/include/core/SkMaskFilter.h" #include "third_party/skia/include/core/SkPath.h" #include "third_party/skia/include/core/SkRSXform.h" #include "third_party/skia/include/core/SkTextBlob.h" @@ -74,7 +71,7 @@ struct DLOp { struct Set##name##Op final : DLOp { \ static const auto kType = DisplayListOpType::kSet##name; \ \ - Set##name##Op(bool value) : value(value) {} \ + explicit Set##name##Op(bool value) : value(value) {} \ \ const bool value; \ \ @@ -88,17 +85,17 @@ DEFINE_SET_BOOL_OP(InvertColors) #undef DEFINE_SET_BOOL_OP // 4 byte header + 4 byte payload packs into minimum 8 bytes -#define DEFINE_SET_ENUM_OP(name) \ - struct SetStroke##name##Op final : DLOp { \ - static const auto kType = DisplayListOpType::kSetStroke##name; \ - \ - SetStroke##name##Op(SkPaint::name value) : value(value) {} \ - \ - const SkPaint::name value; \ - \ - void dispatch(Dispatcher& dispatcher) const { \ - dispatcher.setStroke##name(value); \ - } \ +#define DEFINE_SET_ENUM_OP(name) \ + struct SetStroke##name##Op final : DLOp { \ + static const auto kType = DisplayListOpType::kSetStroke##name; \ + \ + explicit SetStroke##name##Op(SkPaint::name value) : value(value) {} \ + \ + const SkPaint::name value; \ + \ + void dispatch(Dispatcher& dispatcher) const { \ + dispatcher.setStroke##name(value); \ + } \ }; DEFINE_SET_ENUM_OP(Cap) DEFINE_SET_ENUM_OP(Join) @@ -108,7 +105,7 @@ DEFINE_SET_ENUM_OP(Join) struct SetStyleOp final : DLOp { static const auto kType = DisplayListOpType::kSetStyle; - SetStyleOp(SkPaint::Style style) : style(style) {} + explicit SetStyleOp(SkPaint::Style style) : style(style) {} const SkPaint::Style style; @@ -118,7 +115,7 @@ struct SetStyleOp final : DLOp { struct SetStrokeWidthOp final : DLOp { static const auto kType = DisplayListOpType::kSetStrokeWidth; - SetStrokeWidthOp(SkScalar width) : width(width) {} + explicit SetStrokeWidthOp(SkScalar width) : width(width) {} const SkScalar width; @@ -130,7 +127,7 @@ struct SetStrokeWidthOp final : DLOp { struct SetStrokeMiterOp final : DLOp { static const auto kType = DisplayListOpType::kSetStrokeMiter; - SetStrokeMiterOp(SkScalar limit) : limit(limit) {} + explicit SetStrokeMiterOp(SkScalar limit) : limit(limit) {} const SkScalar limit; @@ -143,7 +140,7 @@ struct SetStrokeMiterOp final : DLOp { struct SetColorOp final : DLOp { static const auto kType = DisplayListOpType::kSetColor; - SetColorOp(SkColor color) : color(color) {} + explicit SetColorOp(SkColor color) : color(color) {} const SkColor color; @@ -153,7 +150,7 @@ struct SetColorOp final : DLOp { struct SetBlendModeOp final : DLOp { static const auto kType = DisplayListOpType::kSetBlendMode; - SetBlendModeOp(SkBlendMode mode) : mode(mode) {} + explicit SetBlendModeOp(SkBlendMode mode) : mode(mode) {} const SkBlendMode mode; @@ -165,26 +162,26 @@ struct SetBlendModeOp final : DLOp { // Set: 4 byte header + an sk_sp (ptr) uses 16 bytes due to the // alignment of the ptr. // (4 bytes unused) -#define DEFINE_SET_CLEAR_SKREF_OP(name, field) \ - struct Clear##name##Op final : DLOp { \ - static const auto kType = DisplayListOpType::kClear##name; \ - \ - Clear##name##Op() {} \ - \ - void dispatch(Dispatcher& dispatcher) const { \ - dispatcher.set##name(nullptr); \ - } \ - }; \ - struct Set##name##Op final : DLOp { \ - static const auto kType = DisplayListOpType::kSet##name; \ - \ - Set##name##Op(sk_sp field) : field(std::move(field)) {} \ - \ - sk_sp field; \ - \ - void dispatch(Dispatcher& dispatcher) const { \ - dispatcher.set##name(field); \ - } \ +#define DEFINE_SET_CLEAR_SKREF_OP(name, field) \ + struct Clear##name##Op final : DLOp { \ + static const auto kType = DisplayListOpType::kClear##name; \ + \ + Clear##name##Op() {} \ + \ + void dispatch(Dispatcher& dispatcher) const { \ + dispatcher.set##name(nullptr); \ + } \ + }; \ + struct Set##name##Op final : DLOp { \ + static const auto kType = DisplayListOpType::kSet##name; \ + \ + explicit Set##name##Op(sk_sp field) : field(std::move(field)) {} \ + \ + sk_sp field; \ + \ + void dispatch(Dispatcher& dispatcher) const { \ + dispatcher.set##name(field); \ + } \ }; DEFINE_SET_CLEAR_SKREF_OP(Blender, blender) DEFINE_SET_CLEAR_SKREF_OP(Shader, shader) @@ -201,7 +198,7 @@ DEFINE_SET_CLEAR_SKREF_OP(PathEffect, effect) struct SetMaskBlurFilter##name##Op final : DLOp { \ static const auto kType = DisplayListOpType::kSetMaskBlurFilter##name; \ \ - SetMaskBlurFilter##name##Op(SkScalar sigma) : sigma(sigma) {} \ + explicit SetMaskBlurFilter##name##Op(SkScalar sigma) : sigma(sigma) {} \ \ SkScalar sigma; \ \ @@ -227,7 +224,7 @@ struct SaveOp final : DLOp { struct SaveLayerOp final : DLOp { static const auto kType = DisplayListOpType::kSaveLayer; - SaveLayerOp(bool with_paint) : with_paint(with_paint) {} + explicit SaveLayerOp(bool with_paint) : with_paint(with_paint) {} bool with_paint; @@ -286,7 +283,7 @@ struct ScaleOp final : DLOp { struct RotateOp final : DLOp { static const auto kType = DisplayListOpType::kRotate; - RotateOp(SkScalar degrees) : degrees(degrees) {} + explicit RotateOp(SkScalar degrees) : degrees(degrees) {} const SkScalar degrees; @@ -435,17 +432,17 @@ struct DrawColorOp final : DLOp { // (4 bytes unused) // SkOval is same as SkRect // SkRRect is 52 more bytes, which packs efficiently into 56 bytes total -#define DEFINE_DRAW_1ARG_OP(op_name, arg_type, arg_name) \ - struct Draw##op_name##Op final : DLOp { \ - static const auto kType = DisplayListOpType::kDraw##op_name; \ - \ - Draw##op_name##Op(arg_type arg_name) : arg_name(arg_name) {} \ - \ - const arg_type arg_name; \ - \ - void dispatch(Dispatcher& dispatcher) const { \ - dispatcher.draw##op_name(arg_name); \ - } \ +#define DEFINE_DRAW_1ARG_OP(op_name, arg_type, arg_name) \ + struct Draw##op_name##Op final : DLOp { \ + static const auto kType = DisplayListOpType::kDraw##op_name; \ + \ + explicit Draw##op_name##Op(arg_type arg_name) : arg_name(arg_name) {} \ + \ + const arg_type arg_name; \ + \ + void dispatch(Dispatcher& dispatcher) const { \ + dispatcher.draw##op_name(arg_name); \ + } \ }; DEFINE_DRAW_1ARG_OP(Rect, SkRect, rect) DEFINE_DRAW_1ARG_OP(Oval, SkRect, oval) @@ -457,7 +454,7 @@ DEFINE_DRAW_1ARG_OP(RRect, SkRRect, rrect) struct DrawPathOp final : DLOp { static const auto kType = DisplayListOpType::kDrawPath; - DrawPathOp(SkPath path) : path(path) {} + explicit DrawPathOp(SkPath path) : path(path) {} const SkPath path; @@ -521,7 +518,7 @@ struct DrawArcOp final : DLOp { struct Draw##name##Op final : DLOp { \ static const auto kType = DisplayListOpType::kDraw##name; \ \ - Draw##name##Op(uint32_t count) : count(count) {} \ + explicit Draw##name##Op(uint32_t count) : count(count) {} \ \ const uint32_t count; \ \ @@ -807,7 +804,7 @@ struct DrawSkPictureMatrixOp final : DLOp { struct DrawDisplayListOp final : DLOp { static const auto kType = DisplayListOpType::kDrawDisplayList; - DrawDisplayListOp(const sk_sp display_list) + explicit DrawDisplayListOp(const sk_sp display_list) : display_list(std::move(display_list)) {} sk_sp display_list; @@ -871,7 +868,7 @@ void DisplayList::Dispatch(Dispatcher& dispatcher, uint8_t* ptr, uint8_t* end) const { while (ptr < end) { - auto op = (const DLOp*)ptr; + auto op = reinterpret_cast(ptr); ptr += op->size; FML_DCHECK(ptr <= end); switch (op->type) { @@ -893,7 +890,7 @@ void DisplayList::Dispatch(Dispatcher& dispatcher, static void DisposeOps(uint8_t* ptr, uint8_t* end) { while (ptr < end) { - auto op = (const DLOp*)ptr; + auto op = reinterpret_cast(ptr); ptr += op->size; FML_DCHECK(ptr <= end); switch (op->type) { @@ -925,8 +922,8 @@ static bool CompareOps(uint8_t* ptrA, uint8_t* bulkStartA = ptrA; uint8_t* bulkStartB = ptrB; while (ptrA < endA && ptrB < endB) { - auto opA = (const DLOp*)ptrA; - auto opB = (const DLOp*)ptrB; + auto opA = reinterpret_cast(ptrA); + auto opB = reinterpret_cast(ptrB); if (opA->type != opB->type || opA->size != opB->size) { return false; } @@ -1050,7 +1047,7 @@ void* DisplayListBuilder::Push(size_t pod, int op_inc, Args&&... args) { memset(storage_.get() + used_, 0, allocated_ - used_); } FML_DCHECK(used_ + size <= allocated_); - auto op = (T*)(storage_.get() + used_); + auto op = reinterpret_cast(storage_.get() + used_); used_ += size; new (op) T{std::forward(args)...}; op->type = T::kType; @@ -1085,65 +1082,85 @@ DisplayListBuilder::~DisplayListBuilder() { } } -void DisplayListBuilder::setAntiAlias(bool aa) { - Push(0, 0, aa); +void DisplayListBuilder::onSetAntiAlias(bool aa) { + Push(0, 0, current_anti_alias_ = aa); } -void DisplayListBuilder::setDither(bool dither) { - Push(0, 0, dither); +void DisplayListBuilder::onSetDither(bool dither) { + Push(0, 0, current_dither_ = dither); } -void DisplayListBuilder::setInvertColors(bool invert) { - Push(0, 0, invert); +void DisplayListBuilder::onSetInvertColors(bool invert) { + Push(0, 0, current_invert_colors_ = invert); } -void DisplayListBuilder::setStrokeCap(SkPaint::Cap cap) { - Push(0, 0, cap); +void DisplayListBuilder::onSetStrokeCap(SkPaint::Cap cap) { + Push(0, 0, current_stroke_cap_ = cap); } -void DisplayListBuilder::setStrokeJoin(SkPaint::Join join) { - Push(0, 0, join); +void DisplayListBuilder::onSetStrokeJoin(SkPaint::Join join) { + Push(0, 0, current_stroke_join_ = join); } -void DisplayListBuilder::setStyle(SkPaint::Style style) { - Push(0, 0, style); +void DisplayListBuilder::onSetStyle(SkPaint::Style style) { + Push(0, 0, current_style_ = style); } -void DisplayListBuilder::setStrokeWidth(SkScalar width) { - Push(0, 0, width); +void DisplayListBuilder::onSetStrokeWidth(SkScalar width) { + Push(0, 0, current_stroke_width_ = width); } -void DisplayListBuilder::setStrokeMiter(SkScalar limit) { - Push(0, 0, limit); +void DisplayListBuilder::onSetStrokeMiter(SkScalar limit) { + Push(0, 0, current_stroke_miter_ = limit); } -void DisplayListBuilder::setColor(SkColor color) { - Push(0, 0, color); +void DisplayListBuilder::onSetColor(SkColor color) { + Push(0, 0, current_color_ = color); } -void DisplayListBuilder::setBlendMode(SkBlendMode mode) { - Push(0, 0, mode); +void DisplayListBuilder::onSetBlendMode(SkBlendMode mode) { + current_blender_ = nullptr; + Push(0, 0, current_blend_mode_ = mode); } -void DisplayListBuilder::setBlender(sk_sp blender) { - blender // - ? Push(0, 0, std::move(blender)) - : Push(0, 0); +void DisplayListBuilder::onSetBlender(sk_sp blender) { + // setBlender(nullptr) should be redirected to setBlendMode(SrcOver) + // by the set method, if not then the following is inefficient but works + FML_DCHECK(blender); + SkPaint p; + p.setBlender(blender); + if (p.asBlendMode()) { + setBlendMode(p.asBlendMode().value()); + } else { + // |current_blender_| supersedes any value of |current_blend_mode_| + (current_blender_ = blender) // + ? Push(0, 0, std::move(blender)) + : Push(0, 0); + } } -void DisplayListBuilder::setShader(sk_sp shader) { - shader // +void DisplayListBuilder::onSetShader(sk_sp shader) { + (current_shader_ = shader) // ? Push(0, 0, std::move(shader)) : Push(0, 0); } -void DisplayListBuilder::setImageFilter(sk_sp filter) { - filter // +void DisplayListBuilder::onSetImageFilter(sk_sp filter) { + (current_image_filter_ = filter) // ? Push(0, 0, std::move(filter)) : Push(0, 0); } -void DisplayListBuilder::setColorFilter(sk_sp filter) { - filter // +void DisplayListBuilder::onSetColorFilter(sk_sp filter) { + (current_color_filter_ = filter) // ? Push(0, 0, std::move(filter)) : Push(0, 0); } -void DisplayListBuilder::setPathEffect(sk_sp effect) { - effect // +void DisplayListBuilder::onSetPathEffect(sk_sp effect) { + (current_path_effect_ = effect) // ? Push(0, 0, std::move(effect)) : Push(0, 0); } -void DisplayListBuilder::setMaskFilter(sk_sp filter) { - Push(0, 0, std::move(filter)); +void DisplayListBuilder::onSetMaskFilter(sk_sp filter) { + current_mask_sigma_ = kInvalidSigma; + (current_mask_filter_ = filter) // + ? Push(0, 0, std::move(filter)) + : Push(0, 0); } -void DisplayListBuilder::setMaskBlurFilter(SkBlurStyle style, SkScalar sigma) { +void DisplayListBuilder::onSetMaskBlurFilter(SkBlurStyle style, + SkScalar sigma) { + // Valid sigma is checked by setMaskBlurFilter + FML_DCHECK(mask_sigma_valid(sigma)); + current_mask_filter_ = nullptr; + current_mask_style_ = style; + current_mask_sigma_ = sigma; switch (style) { case kNormal_SkBlurStyle: Push(0, 0, sigma); @@ -1160,6 +1177,56 @@ void DisplayListBuilder::setMaskBlurFilter(SkBlurStyle style, SkScalar sigma) { } } +void DisplayListBuilder::setAttributesFromPaint( + const SkPaint& paint, + const DisplayListAttributeFlags flags) { + if (flags.applies_anti_alias()) { + setAntiAlias(paint.isAntiAlias()); + } + if (flags.applies_dither()) { + setDither(paint.isDither()); + } + if (flags.applies_alpha_or_color()) { + setColor(paint.getColor()); + } + if (flags.applies_blend()) { + skstd::optional mode_optional = paint.asBlendMode(); + if (mode_optional) { + setBlendMode(mode_optional.value()); + } else { + setBlender(sk_ref_sp(paint.getBlender())); + } + } + if (flags.applies_style()) { + setStyle(paint.getStyle()); + } + if (flags.is_stroked(paint.getStyle())) { + setStrokeWidth(paint.getStrokeWidth()); + setStrokeMiter(paint.getStrokeMiter()); + setStrokeCap(paint.getStrokeCap()); + setStrokeJoin(paint.getStrokeJoin()); + } + if (flags.applies_shader()) { + setShader(sk_ref_sp(paint.getShader())); + } + if (flags.applies_color_filter()) { + // invert colors is a Flutter::Paint thing, not an SkPaint thing + // we must clear it because it is a second potential color filter + // that is composed with the paint's color filter. + setInvertColors(false); + setColorFilter(sk_ref_sp(paint.getColorFilter())); + } + if (flags.applies_image_filter()) { + setImageFilter(sk_ref_sp(paint.getImageFilter())); + } + if (flags.applies_path_effect()) { + setPathEffect(sk_ref_sp(paint.getPathEffect())); + } + if (flags.applies_mask_filter()) { + setMaskFilter(sk_ref_sp(paint.getMaskFilter())); + } +} + void DisplayListBuilder::save() { save_level_++; Push(0, 1); @@ -1170,24 +1237,36 @@ void DisplayListBuilder::restore() { save_level_--; } } -void DisplayListBuilder::saveLayer(const SkRect* bounds, bool with_paint) { +void DisplayListBuilder::saveLayer(const SkRect* bounds, + bool restore_with_paint) { save_level_++; bounds // - ? Push(0, 1, *bounds, with_paint) - : Push(0, 1, with_paint); + ? Push(0, 1, *bounds, restore_with_paint) + : Push(0, 1, restore_with_paint); } void DisplayListBuilder::translate(SkScalar tx, SkScalar ty) { - Push(0, 1, tx, ty); + if (SkScalarIsFinite(tx) && SkScalarIsFinite(ty) && + (tx != 0.0 || ty != 0.0)) { + Push(0, 1, tx, ty); + } } void DisplayListBuilder::scale(SkScalar sx, SkScalar sy) { - Push(0, 1, sx, sy); + if (SkScalarIsFinite(sx) && SkScalarIsFinite(sy) && + (sx != 1.0 || sy != 1.0)) { + Push(0, 1, sx, sy); + } } void DisplayListBuilder::rotate(SkScalar degrees) { - Push(0, 1, degrees); + if (SkScalarMod(degrees, 360.0) != 0.0) { + Push(0, 1, degrees); + } } void DisplayListBuilder::skew(SkScalar sx, SkScalar sy) { - Push(0, 1, sx, sy); + if (SkScalarIsFinite(sx) && SkScalarIsFinite(sy) && + (sx != 0.0 || sy != 0.0)) { + Push(0, 1, sx, sy); + } } // clang-format off @@ -1196,7 +1275,10 @@ void DisplayListBuilder::skew(SkScalar sx, SkScalar sy) { void DisplayListBuilder::transform2DAffine( SkScalar mxx, SkScalar mxy, SkScalar mxt, SkScalar myx, SkScalar myy, SkScalar myt) { - if (!(mxx == 1 && mxy == 0 && mxt == 0 && + if (SkScalarsAreFinite(mxx, myx) && + SkScalarsAreFinite(mxy, myy) && + SkScalarsAreFinite(mxt, myt) && + !(mxx == 1 && mxy == 0 && mxt == 0 && myx == 0 && myy == 1 && myt == 0)) { Push(0, 1, mxx, mxy, mxt, @@ -1215,7 +1297,10 @@ void DisplayListBuilder::transformFullPerspective( mwx == 0 && mwy == 0 && mwz == 0 && mwt == 1) { transform2DAffine(mxx, mxy, mxt, myx, myy, myt); - } else { + } else if (SkScalarsAreFinite(mxx, mxy) && SkScalarsAreFinite(mxz, mxt) && + SkScalarsAreFinite(myx, myy) && SkScalarsAreFinite(myz, myt) && + SkScalarsAreFinite(mzx, mzy) && SkScalarsAreFinite(mzz, mzt) && + SkScalarsAreFinite(mwx, mwy) && SkScalarsAreFinite(mwz, mwt)) { Push(0, 1, mxx, mxy, mxz, mxt, myx, myy, myz, myt, @@ -1368,7 +1453,7 @@ void DisplayListBuilder::drawImageLattice(const sk_sp image, const SkCanvas::Lattice& lattice, const SkRect& dst, SkFilterMode filter, - bool with_paint) { + bool render_with_attributes) { int xDivCount = lattice.fXCount; int yDivCount = lattice.fYCount; FML_DCHECK((lattice.fRectTypes == nullptr) || (lattice.fColors != nullptr)); @@ -1379,9 +1464,9 @@ void DisplayListBuilder::drawImageLattice(const sk_sp image, (xDivCount + yDivCount) * sizeof(int) + cellCount * (sizeof(SkColor) + sizeof(SkCanvas::Lattice::RectType)); SkIRect src = lattice.fBounds ? *lattice.fBounds : image->bounds(); - void* pod = this->Push(bytes, 1, std::move(image), - xDivCount, yDivCount, cellCount, - src, dst, filter, with_paint); + void* pod = this->Push( + bytes, 1, std::move(image), xDivCount, yDivCount, cellCount, src, dst, + filter, render_with_attributes); CopyV(pod, lattice.fXDivs, xDivCount, lattice.fYDivs, yDivCount, lattice.fColors, cellCount, lattice.fRectTypes, cellCount); } @@ -1427,14 +1512,26 @@ void DisplayListBuilder::drawPicture(const sk_sp picture, ? Push(0, 1, std::move(picture), *matrix, render_with_attributes) : Push(0, 1, std::move(picture), render_with_attributes); + // The non-nested op count accumulated in the |Push| method will include + // this call to |drawPicture| for non-nested op count metrics. + // But, for nested op count metrics we want the |drawPicture| call itself + // to be transparent. So we subtract 1 from our accumulated nested count to + // balance out against the 1 that was accumulated into the regular count. + // This behavior is identical to the way SkPicture computes nested op counts. + nested_op_count_ += picture->approximateOpCount(true) - 1; nested_bytes_ += picture->approximateBytesUsed(); - nested_op_count_ += picture->approximateOpCount(true); } void DisplayListBuilder::drawDisplayList( const sk_sp display_list) { Push(0, 1, std::move(display_list)); + // The non-nested op count accumulated in the |Push| method will include + // this call to |drawDisplayList| for non-nested op count metrics. + // But, for nested op count metrics we want the |drawDisplayList| call itself + // to be transparent. So we subtract 1 from our accumulated nested count to + // balance out against the 1 that was accumulated into the regular count. + // This behavior is identical to the way SkPicture computes nested op counts. + nested_op_count_ += display_list->op_count(true) - 1; nested_bytes_ += display_list->bytes(true); - nested_op_count_ += display_list->op_count(true); } void DisplayListBuilder::drawTextBlob(const sk_sp blob, SkScalar x, @@ -1451,4 +1548,165 @@ void DisplayListBuilder::drawShadow(const SkPath& path, : Push(0, 1, path, color, elevation, dpr); } +// clang-format off +// Flags common to all primitives that apply colors +#define PAINT_FLAGS (kUsesDither_ | \ + kUsesColor_ | \ + kUsesAlpha_ | \ + kUsesBlend_ | \ + kUsesShader_ | \ + kUsesColorFilter_ | \ + kUsesImageFilter_) + +// Flags common to all primitives that stroke or fill +#define STROKE_OR_FILL_FLAGS (kIsDrawnGeometry_ | \ + kUsesAntiAlias_ | \ + kUsesMaskFilter_ | \ + kUsesPathEffect_) + +// Flags common to primitives that stroke geometry +#define STROKE_FLAGS (kIsStrokedGeometry_ | \ + kUsesAntiAlias_ | \ + kUsesMaskFilter_ | \ + kUsesPathEffect_) + +// Flags common to primitives that render an image with paint attributes +#define IMAGE_FLAGS_BASE (kIsNonGeometric_ | \ + kUsesAlpha_ | \ + kUsesDither_ | \ + kUsesBlend_ | \ + kUsesColorFilter_ | \ + kUsesImageFilter_) +// clang-format on + +const DisplayListAttributeFlags DisplayListOpFlags::kSaveLayerFlags = + DisplayListAttributeFlags(kIgnoresPaint_); + +const DisplayListAttributeFlags DisplayListOpFlags::kSaveLayerWithPaintFlags = + DisplayListAttributeFlags(kIsNonGeometric_ | // + kUsesAlpha_ | // + kUsesBlend_ | // + kUsesColorFilter_ | // + kUsesImageFilter_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawColorFlags = + DisplayListAttributeFlags(kFloodsSurface_ | kIgnoresPaint_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawPaintFlags = + DisplayListAttributeFlags(PAINT_FLAGS | kFloodsSurface_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawHVLineFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_FLAGS | kMayHaveCaps_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawLineFlags = + kDrawHVLineFlags.with(kMayHaveDiagonalCaps_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawRectFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_OR_FILL_FLAGS | + kMayHaveJoins_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawOvalFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_OR_FILL_FLAGS); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawCircleFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_OR_FILL_FLAGS); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawRRectFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_OR_FILL_FLAGS); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawDRRectFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_OR_FILL_FLAGS); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawPathFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_OR_FILL_FLAGS | + kMayHaveCaps_ | kMayHaveDiagonalCaps_ | + kMayHaveJoins_ | kMayHaveAcuteJoins_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawArcNoCenterFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_OR_FILL_FLAGS | + kMayHaveCaps_ | kMayHaveDiagonalCaps_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawArcWithCenterFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_OR_FILL_FLAGS | + kMayHaveJoins_ | kMayHaveAcuteJoins_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawPointsAsPointsFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_FLAGS | // + kMayHaveCaps_ | kButtCapIsSquare_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawPointsAsLinesFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_FLAGS | // + kMayHaveCaps_ | kMayHaveDiagonalCaps_); + +// Polygon mode just draws (count-1) separate lines, no joins +const DisplayListAttributeFlags DisplayListOpFlags::kDrawPointsAsPolygonFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_FLAGS | // + kMayHaveCaps_ | kMayHaveDiagonalCaps_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawVerticesFlags = + DisplayListAttributeFlags(kIsNonGeometric_ | // + kUsesDither_ | // + kUsesAlpha_ | // + kUsesShader_ | // + kUsesBlend_ | // + kUsesColorFilter_ | // + kUsesImageFilter_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawImageFlags = + DisplayListAttributeFlags(kIgnoresPaint_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawImageWithPaintFlags = + DisplayListAttributeFlags(IMAGE_FLAGS_BASE | // + kUsesAntiAlias_ | kUsesMaskFilter_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawImageRectFlags = + DisplayListAttributeFlags(kIgnoresPaint_); + +const DisplayListAttributeFlags + DisplayListOpFlags::kDrawImageRectWithPaintFlags = + DisplayListAttributeFlags(IMAGE_FLAGS_BASE | // + kUsesAntiAlias_ | kUsesMaskFilter_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawImageNineFlags = + DisplayListAttributeFlags(kIgnoresPaint_); + +const DisplayListAttributeFlags + DisplayListOpFlags::kDrawImageNineWithPaintFlags = + DisplayListAttributeFlags(IMAGE_FLAGS_BASE); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawImageLatticeFlags = + DisplayListAttributeFlags(kIgnoresPaint_); + +const DisplayListAttributeFlags + DisplayListOpFlags::kDrawImageLatticeWithPaintFlags = + DisplayListAttributeFlags(IMAGE_FLAGS_BASE); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawAtlasFlags = + DisplayListAttributeFlags(kIgnoresPaint_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawAtlasWithPaintFlags = + DisplayListAttributeFlags(IMAGE_FLAGS_BASE); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawPictureFlags = + DisplayListAttributeFlags(kIgnoresPaint_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawPictureWithPaintFlags = + kSaveLayerWithPaintFlags; + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawDisplayListFlags = + DisplayListAttributeFlags(kIgnoresPaint_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawTextBlobFlags = + DisplayListAttributeFlags(PAINT_FLAGS | STROKE_OR_FILL_FLAGS | + kMayHaveJoins_) + .without(kUsesAntiAlias_); + +const DisplayListAttributeFlags DisplayListOpFlags::kDrawShadowFlags = + DisplayListAttributeFlags(kIgnoresPaint_); + +#undef PAINT_FLAGS +#undef STROKE_OR_FILL_FLAGS +#undef STROKE_FLAGS +#undef IMAGE_FLAGS_BASE + } // namespace flutter diff --git a/flow/display_list.h b/flow/display_list.h index 327ad9df2ea21..d7b439face475 100644 --- a/flow/display_list.h +++ b/flow/display_list.h @@ -5,17 +5,22 @@ #ifndef FLUTTER_FLOW_DISPLAY_LIST_H_ #define FLUTTER_FLOW_DISPLAY_LIST_H_ +#include + #include "third_party/skia/include/core/SkBlender.h" #include "third_party/skia/include/core/SkBlurTypes.h" #include "third_party/skia/include/core/SkCanvas.h" #include "third_party/skia/include/core/SkColorFilter.h" #include "third_party/skia/include/core/SkImage.h" #include "third_party/skia/include/core/SkImageFilter.h" +#include "third_party/skia/include/core/SkMaskFilter.h" #include "third_party/skia/include/core/SkPathEffect.h" #include "third_party/skia/include/core/SkPicture.h" #include "third_party/skia/include/core/SkShader.h" #include "third_party/skia/include/core/SkVertices.h" +#include "flutter/fml/logging.h" + // The Flutter DisplayList mechanism encapsulates a persistent sequence of // rendering operations. // @@ -463,6 +468,279 @@ class Dispatcher { SkScalar dpr) = 0; }; +/// The base class for the classes that maintain a list of +/// attributes that might be important for a number of operations +/// including which rendering attributes need to be set before +/// calling a rendering method (all |drawSomething| calls), +/// or for determining which exceptional conditions may need +/// to be accounted for in bounds calculations. +/// This class contains only protected definitions and helper methods +/// for the public classes |DisplayListAttributeFlags| and +/// |DisplayListSpecialGeometryFlags|. +class DisplayListFlags { + protected: + // A drawing operation that is not geometric in nature (but which + // may still apply a MaskFilter - see |kUsesMaskFilter_| below). + static constexpr int kIsNonGeometric_ = 0; + + // A geometric operation that is defined as a fill operation + // regardless of what the current paint Style is set to. + // This flag will automatically assume |kUsesMaskFilter_|. + static constexpr int kIsFilledGeometry_ = 1 << 0; + + // A geometric operation that is defined as a stroke operation + // regardless of what the current paint Style is set to. + // This flag will automatically assume |kUsesMaskFilter_|. + static constexpr int kIsStrokedGeometry_ = 1 << 1; + + // A geometric operation that may be a stroke or fill operation + // depending on the current state of the paint Style attribute. + // This flag will automatically assume |kUsesMaskFilter_|. + static constexpr int kIsDrawnGeometry_ = 1 << 2; + + static constexpr int kIsAnyGeometryMask_ = // + kIsFilledGeometry_ | // + kIsStrokedGeometry_ | // + kIsDrawnGeometry_; + + // A primitive that floods the surface (or clip) with no + // natural bounds, such as |drawColor| or |drawPaint|. + static constexpr int kFloodsSurface_ = 1 << 3; + + static constexpr int kMayHaveCaps_ = 1 << 4; + static constexpr int kMayHaveJoins_ = 1 << 5; + static constexpr int kButtCapIsSquare_ = 1 << 6; + + // A geometric operation which has a path that might have + // end caps that are not rectilinear which means that square + // end caps might project further than half the stroke width + // from the geometry bounds. + // A rectilinear path such as |drawRect| will not have + // diagonal end caps. |drawLine| might have diagonal end + // caps depending on the angle of the line, and more likely + // |drawPath| will often have such end caps. + static constexpr int kMayHaveDiagonalCaps_ = 1 << 7; + + // A geometric operation which has joined vertices that are + // not guaranteed to be smooth (angles of incoming and outgoing) + // segments at some joins may not have the same angle) or + // rectilinear (squares have right angles at the corners, but + // those corners will never extend past the bounding box of + // the geometry pre-transform). + // |drawRect|, |drawOval| and |drawRRect| all have well + // behaved joins, but |drawPath| might have joins that cause + // mitered extensions outside the pre-transformed bounding box. + static constexpr int kMayHaveAcuteJoins_ = 1 << 8; + + static constexpr int kAnySpecialGeometryMask_ = // + kMayHaveCaps_ | kMayHaveJoins_ | kButtCapIsSquare_ | // + kMayHaveDiagonalCaps_ | kMayHaveAcuteJoins_; + + // clang-format off + static constexpr int kUsesAntiAlias_ = 1 << 10; + static constexpr int kUsesDither_ = 1 << 11; + static constexpr int kUsesAlpha_ = 1 << 12; + static constexpr int kUsesColor_ = 1 << 13; + static constexpr int kUsesBlend_ = 1 << 14; + static constexpr int kUsesShader_ = 1 << 15; + static constexpr int kUsesColorFilter_ = 1 << 16; + static constexpr int kUsesPathEffect_ = 1 << 17; + static constexpr int kUsesMaskFilter_ = 1 << 18; + static constexpr int kUsesImageFilter_ = 1 << 19; + + static constexpr int kIgnoresPaint_ = 1 << 30; + // clang-format on + + static constexpr int kAnyAttributeMask_ = // + kUsesAntiAlias_ | kUsesDither_ | kUsesAlpha_ | kUsesColor_ | kUsesBlend_ | + kUsesShader_ | kUsesColorFilter_ | kUsesPathEffect_ | kUsesMaskFilter_ | + kUsesImageFilter_; +}; + +class DisplayListFlagsBase : protected DisplayListFlags { + protected: + explicit DisplayListFlagsBase(int flags) : flags_(flags) {} + + const int flags_; + + bool has_any(int qFlags) const { return (flags_ & qFlags) != 0; } + bool has_all(int qFlags) const { return (flags_ & qFlags) == qFlags; } + bool has_none(int qFlags) const { return (flags_ & qFlags) == 0; } +}; + +/// An attribute class for advertising specific properties of +/// a geometric attribute that can affect the computation of +/// the bounds of the primitive. +class DisplayListSpecialGeometryFlags : DisplayListFlagsBase { + public: + /// The geometry may have segments that end without closing the path. + bool may_have_end_caps() const { return has_any(kMayHaveCaps_); } + + /// The geometry may have segments connect non-continuously. + bool may_have_joins() const { return has_any(kMayHaveJoins_); } + + /// Mainly for drawPoints(PointMode) where Butt caps are rendered as squares. + bool butt_cap_becomes_square() const { return has_any(kButtCapIsSquare_); } + + /// The geometry may have segments that end on a diagonal + /// such that their end caps extend further than the default + /// |strokeWidth * 0.5| margin around the geometry. + bool may_have_diagonal_caps() const { return has_any(kMayHaveDiagonalCaps_); } + + /// The geometry may have segments that meet at vertices at + /// an acute angle such that the miter joins will extend + /// further than the default |strokeWidth * 0.5| margin around + /// the geometry. + bool may_have_acute_joins() const { return has_any(kMayHaveAcuteJoins_); } + + private: + explicit DisplayListSpecialGeometryFlags(int flags) + : DisplayListFlagsBase(flags) { + FML_DCHECK((flags & kAnySpecialGeometryMask_) == flags); + } + + const DisplayListSpecialGeometryFlags with(int extra) const { + return extra == 0 ? *this : DisplayListSpecialGeometryFlags(flags_ | extra); + } + + friend class DisplayListAttributeFlags; +}; + +class DisplayListAttributeFlags : DisplayListFlagsBase { + public: + const DisplayListSpecialGeometryFlags WithPathEffect( + sk_sp effect) const { + if (is_geometric() && effect) { + SkPathEffect::DashInfo info; + if (effect->asADash(&info) == SkPathEffect::kDash_DashType) { + // A dash effect has a very simple impact. It cannot introduce any + // miter joins that weren't already present in the original path + // and it does not grow the bounds of the path, but it can add + // end caps to areas that might not have had them before so all + // we need to do is to indicate the potential for diagonal + // end caps and move on. + return special_flags_.with(kMayHaveCaps_ | kMayHaveDiagonalCaps_); + } else { + // An arbitrary path effect can introduce joins at an arbitrary + // angle and may change the geometry of the end caps + return special_flags_.with(kMayHaveCaps_ | kMayHaveDiagonalCaps_ | + kMayHaveJoins_ | kMayHaveAcuteJoins_); + } + } + return special_flags_; + } + + bool ignores_paint() const { return has_any(kIgnoresPaint_); } + + bool applies_anti_alias() const { return has_any(kUsesAntiAlias_); } + bool applies_dither() const { return has_any(kUsesDither_); } + bool applies_color() const { return has_any(kUsesColor_); } + bool applies_alpha() const { return has_any(kUsesAlpha_); } + bool applies_alpha_or_color() const { + return has_any(kUsesAlpha_ | kUsesColor_); + } + + /// The primitive dynamically determines whether it is a stroke or fill + /// operation (or both) based on the setting of the |Style| attribute. + bool applies_style() const { return has_any(kIsDrawnGeometry_); } + /// The primitive can use any of the stroke attributes, such as + /// StrokeWidth, StrokeMiter, StrokeCap, or StrokeJoin. This + /// method will return if the primitive is defined as one that + /// strokes its geometry (such as |drawLine|) or if it is defined + /// as one that honors the Style attribute. If the Style attribute + /// is known then a more accurate answer can be returned from + /// the |is_stroked| method by supplying the actual setting of + /// the style. + // bool applies_stroke_attributes() const { return is_stroked(); } + + bool applies_shader() const { return has_any(kUsesShader_); } + /// The primitive honors the current SkColorFilter, including + /// the related attribute InvertColors + bool applies_color_filter() const { return has_any(kUsesColorFilter_); } + /// The primitive honors the SkBlendMode or SkBlender + bool applies_blend() const { return has_any(kUsesBlend_); } + bool applies_path_effect() const { return has_any(kUsesPathEffect_); } + /// The primitive honors the SkMaskFilter whether set using the + /// filter object or using the convenience method |setMaskBlurFilter| + bool applies_mask_filter() const { return has_any(kUsesMaskFilter_); } + bool applies_image_filter() const { return has_any(kUsesImageFilter_); } + + bool is_geometric() const { return has_any(kIsAnyGeometryMask_); } + bool always_stroked() const { return has_any(kIsStrokedGeometry_); } + bool is_stroked(SkPaint::Style style = SkPaint::Style::kStroke_Style) const { + return ( + has_any(kIsStrokedGeometry_) || + (style != SkPaint::Style::kFill_Style && has_any(kIsDrawnGeometry_))); + } + + bool is_flood() const { return has_any(kFloodsSurface_); } + + private: + explicit DisplayListAttributeFlags(int flags) + : DisplayListFlagsBase(flags), + special_flags_(flags & kAnySpecialGeometryMask_) { + FML_DCHECK((flags & kIsAnyGeometryMask_) == kIsNonGeometric_ || + (flags & kIsAnyGeometryMask_) == kIsFilledGeometry_ || + (flags & kIsAnyGeometryMask_) == kIsStrokedGeometry_ || + (flags & kIsAnyGeometryMask_) == kIsDrawnGeometry_); + FML_DCHECK(((flags & kAnyAttributeMask_) == 0) != + ((flags & kIgnoresPaint_) == 0)); + FML_DCHECK((flags & kIsAnyGeometryMask_) != 0 || + (flags & kAnySpecialGeometryMask_) == 0); + } + + const DisplayListAttributeFlags with(int extra) const { + return extra == 0 ? *this : DisplayListAttributeFlags(flags_ | extra); + } + + const DisplayListAttributeFlags without(int remove) const { + FML_DCHECK(has_all(remove)); + return DisplayListAttributeFlags(flags_ & ~remove); + } + + const DisplayListSpecialGeometryFlags special_flags_; + + friend class DisplayListOpFlags; +}; + +class DisplayListOpFlags : DisplayListFlags { + public: + static const DisplayListAttributeFlags kSaveLayerFlags; + static const DisplayListAttributeFlags kSaveLayerWithPaintFlags; + static const DisplayListAttributeFlags kDrawColorFlags; + static const DisplayListAttributeFlags kDrawPaintFlags; + static const DisplayListAttributeFlags kDrawLineFlags; + // Special case flags for horizonal and vertical lines + static const DisplayListAttributeFlags kDrawHVLineFlags; + static const DisplayListAttributeFlags kDrawRectFlags; + static const DisplayListAttributeFlags kDrawOvalFlags; + static const DisplayListAttributeFlags kDrawCircleFlags; + static const DisplayListAttributeFlags kDrawRRectFlags; + static const DisplayListAttributeFlags kDrawDRRectFlags; + static const DisplayListAttributeFlags kDrawPathFlags; + static const DisplayListAttributeFlags kDrawArcNoCenterFlags; + static const DisplayListAttributeFlags kDrawArcWithCenterFlags; + static const DisplayListAttributeFlags kDrawPointsAsPointsFlags; + static const DisplayListAttributeFlags kDrawPointsAsLinesFlags; + static const DisplayListAttributeFlags kDrawPointsAsPolygonFlags; + static const DisplayListAttributeFlags kDrawVerticesFlags; + static const DisplayListAttributeFlags kDrawImageFlags; + static const DisplayListAttributeFlags kDrawImageWithPaintFlags; + static const DisplayListAttributeFlags kDrawImageRectFlags; + static const DisplayListAttributeFlags kDrawImageRectWithPaintFlags; + static const DisplayListAttributeFlags kDrawImageNineFlags; + static const DisplayListAttributeFlags kDrawImageNineWithPaintFlags; + static const DisplayListAttributeFlags kDrawImageLatticeFlags; + static const DisplayListAttributeFlags kDrawImageLatticeWithPaintFlags; + static const DisplayListAttributeFlags kDrawAtlasFlags; + static const DisplayListAttributeFlags kDrawAtlasWithPaintFlags; + static const DisplayListAttributeFlags kDrawPictureFlags; + static const DisplayListAttributeFlags kDrawPictureWithPaintFlags; + static const DisplayListAttributeFlags kDrawDisplayListFlags; + static const DisplayListAttributeFlags kDrawTextBlobFlags; + static const DisplayListAttributeFlags kDrawShadowFlags; +}; + // The primary class used to build a display list. The list of methods // here matches the list of methods invoked on a |Dispatcher|. // If there is some code that already renders to an SkCanvas object, @@ -470,36 +748,148 @@ class Dispatcher { // the DisplayListCanvasRecorder class. class DisplayListBuilder final : public virtual Dispatcher, public SkRefCnt { public: - DisplayListBuilder(const SkRect& cull_rect = kMaxCullRect_); + explicit DisplayListBuilder(const SkRect& cull_rect = kMaxCullRect_); ~DisplayListBuilder(); - void setAntiAlias(bool aa) override; - void setDither(bool dither) override; - void setInvertColors(bool invert) override; - void setStrokeCap(SkPaint::Cap cap) override; - void setStrokeJoin(SkPaint::Join join) override; - void setStyle(SkPaint::Style style) override; - void setStrokeWidth(SkScalar width) override; - void setStrokeMiter(SkScalar limit) override; - void setColor(SkColor color) override; - void setBlendMode(SkBlendMode mode) override; - void setBlender(sk_sp blender) override; - void setShader(sk_sp shader) override; - void setImageFilter(sk_sp filter) override; - void setColorFilter(sk_sp filter) override; - void setPathEffect(sk_sp effect) override; - void setMaskFilter(sk_sp filter) override; - void setMaskBlurFilter(SkBlurStyle style, SkScalar sigma) override; + void setAntiAlias(bool aa) override { + if (current_anti_alias_ != aa) { + onSetAntiAlias(aa); + } + } + void setDither(bool dither) override { + if (current_dither_ != dither) { + onSetDither(dither); + } + } + void setInvertColors(bool invert) override { + if (current_invert_colors_ != invert) { + onSetInvertColors(invert); + } + } + void setStrokeCap(SkPaint::Cap cap) override { + if (current_stroke_cap_ != cap) { + onSetStrokeCap(cap); + } + } + void setStrokeJoin(SkPaint::Join join) override { + if (current_stroke_join_ != join) { + onSetStrokeJoin(join); + } + } + void setStyle(SkPaint::Style style) override { + if (current_style_ != style) { + onSetStyle(style); + } + } + void setStrokeWidth(SkScalar width) override { + if (current_stroke_width_ != width) { + onSetStrokeWidth(width); + } + } + void setStrokeMiter(SkScalar limit) override { + if (current_stroke_miter_ != limit) { + onSetStrokeMiter(limit); + } + } + void setColor(SkColor color) override { + if (current_color_ != color) { + onSetColor(color); + } + } + void setBlendMode(SkBlendMode mode) override { + if (current_blender_ || current_blend_mode_ != mode) { + onSetBlendMode(mode); + } + } + void setBlender(sk_sp blender) override { + if (!blender) { + setBlendMode(SkBlendMode::kSrcOver); + } else if (current_blender_ != blender) { + onSetBlender(std::move(blender)); + } + } + void setShader(sk_sp shader) override { + if (current_shader_ != shader) { + onSetShader(std::move(shader)); + } + } + void setImageFilter(sk_sp filter) override { + if (current_image_filter_ != filter) { + onSetImageFilter(std::move(filter)); + } + } + void setColorFilter(sk_sp filter) override { + if (current_color_filter_ != filter) { + onSetColorFilter(std::move(filter)); + } + } + void setPathEffect(sk_sp effect) override { + if (current_path_effect_ != effect) { + onSetPathEffect(std::move(effect)); + } + } + void setMaskFilter(sk_sp filter) override { + if (mask_sigma_valid(current_mask_sigma_) || + current_mask_filter_ != filter) { + onSetMaskFilter(std::move(filter)); + } + } + void setMaskBlurFilter(SkBlurStyle style, SkScalar sigma) override { + if (!mask_sigma_valid(sigma)) { + // SkMastFilter::MakeBlur(invalid sigma) returns a nullptr, so we + // reset the mask filter here rather than recording the invalid values. + setMaskFilter(nullptr); + } else if (current_mask_style_ != style || current_mask_sigma_ != sigma) { + onSetMaskBlurFilter(style, sigma); + } + } + + bool isAntiAlias() const { return current_anti_alias_; } + bool isDither() const { return current_dither_; } + SkPaint::Style getStyle() const { return current_style_; } + SkColor getColor() const { return current_color_; } + SkScalar getStrokeWidth() const { return current_stroke_width_; } + SkScalar getStrokeMiter() const { return current_stroke_miter_; } + SkPaint::Cap getStrokeCap() const { return current_stroke_cap_; } + SkPaint::Join getStrokeJoin() const { return current_stroke_join_; } + sk_sp getShader() const { return current_shader_; } + sk_sp getColorFilter() const { return current_color_filter_; } + bool isInvertColors() const { return current_invert_colors_; } + std::optional getBlendMode() const { + if (current_blender_) { + // The setters will turn "Mode" style blenders into "blend_mode"s + return {}; + } + return current_blend_mode_; + } + sk_sp getBlender() const { + return current_blender_ ? current_blender_ + : SkBlender::Mode(current_blend_mode_); + } + sk_sp getPathEffect() const { return current_path_effect_; } + sk_sp getMaskFilter() const { + return mask_sigma_valid(current_mask_sigma_) + ? SkMaskFilter::MakeBlur(current_mask_style_, + current_mask_sigma_) + : current_mask_filter_; + } + // No utility getter for the utility setter: + // void setMaskBlurFilter (SkBlurStyle style, SkScalar sigma) + sk_sp getImageFilter() const { return current_image_filter_; } void save() override; - void saveLayer(const SkRect* bounds, bool restoreWithPaint) override; + void saveLayer(const SkRect* bounds, bool restore_with_paint) override; void restore() override; + int getSaveCount() { return save_level_ + 1; } void translate(SkScalar tx, SkScalar ty) override; void scale(SkScalar sx, SkScalar sy) override; void rotate(SkScalar degrees) override; void skew(SkScalar sx, SkScalar sy) override; + void setAttributesFromPaint(const SkPaint& paint, + const DisplayListAttributeFlags flags); + // clang-format off // 2x3 2D affine subset of a 4x4 transform in row major order @@ -514,9 +904,9 @@ class DisplayListBuilder final : public virtual Dispatcher, public SkRefCnt { // clang-format on - void clipRect(const SkRect& rect, SkClipOp clip_op, bool isAA) override; - void clipRRect(const SkRRect& rrect, SkClipOp clip_op, bool isAA) override; - void clipPath(const SkPath& path, SkClipOp clip_op, bool isAA) override; + void clipRect(const SkRect& rect, SkClipOp clip_op, bool is_aa) override; + void clipRRect(const SkRRect& rrect, SkClipOp clip_op, bool is_aa) override; + void clipPath(const SkPath& path, SkClipOp clip_op, bool is_aa) override; void drawPaint() override; void drawColor(SkColor color, SkBlendMode mode) override; @@ -599,6 +989,51 @@ class DisplayListBuilder final : public virtual Dispatcher, public SkRefCnt { template void* Push(size_t extra, int op_inc, Args&&... args); + + // kInvalidSigma is used to indicate that no MaskBlur is currently set. + static constexpr SkScalar kInvalidSigma = 0.0; + static bool mask_sigma_valid(SkScalar sigma) { + return SkScalarIsFinite(sigma) && sigma > 0.0; + } + + void onSetAntiAlias(bool aa); + void onSetDither(bool dither); + void onSetInvertColors(bool invert); + void onSetStrokeCap(SkPaint::Cap cap); + void onSetStrokeJoin(SkPaint::Join join); + void onSetStyle(SkPaint::Style style); + void onSetStrokeWidth(SkScalar width); + void onSetStrokeMiter(SkScalar limit); + void onSetColor(SkColor color); + void onSetBlendMode(SkBlendMode mode); + void onSetBlender(sk_sp blender); + void onSetShader(sk_sp shader); + void onSetImageFilter(sk_sp filter); + void onSetColorFilter(sk_sp filter); + void onSetPathEffect(sk_sp effect); + void onSetMaskFilter(sk_sp filter); + void onSetMaskBlurFilter(SkBlurStyle style, SkScalar sigma); + + // These values should match the defaults of the Dart Paint object. + bool current_anti_alias_ = false; + bool current_dither_ = false; + bool current_invert_colors_ = false; + SkColor current_color_ = 0xFF000000; + SkPaint::Style current_style_ = SkPaint::Style::kFill_Style; + SkScalar current_stroke_width_ = 0.0; + SkScalar current_stroke_miter_ = 4.0; + SkPaint::Cap current_stroke_cap_ = SkPaint::Cap::kButt_Cap; + SkPaint::Join current_stroke_join_ = SkPaint::Join::kMiter_Join; + // If |current_blender_| is set then |current_blend_mode_| should be ignored + SkBlendMode current_blend_mode_ = SkBlendMode::kSrcOver; + sk_sp current_blender_; + sk_sp current_shader_; + sk_sp current_color_filter_; + sk_sp current_image_filter_; + sk_sp current_path_effect_; + sk_sp current_mask_filter_; + SkBlurStyle current_mask_style_; + SkScalar current_mask_sigma_ = kInvalidSigma; }; } // namespace flutter diff --git a/flow/display_list_canvas.cc b/flow/display_list_canvas.cc index 5b44c97d6071a..7f66a37515ec6 100644 --- a/flow/display_list_canvas.cc +++ b/flow/display_list_canvas.cc @@ -19,6 +19,7 @@ void DisplayListCanvasDispatcher::restore() { } void DisplayListCanvasDispatcher::saveLayer(const SkRect* bounds, bool restore_with_paint) { + TRACE_EVENT0("flutter", "Canvas::saveLayer"); canvas_->saveLayer(bounds, restore_with_paint ? &paint() : nullptr); } @@ -76,7 +77,14 @@ void DisplayListCanvasDispatcher::clipPath(const SkPath& path, } void DisplayListCanvasDispatcher::drawPaint() { - canvas_->drawPaint(paint()); + const SkPaint& sk_paint = paint(); + SkImageFilter* filter = sk_paint.getImageFilter(); + if (filter && !filter->asColorFilter(nullptr)) { + // drawPaint does an implicit saveLayer if an SkImageFilter is + // present that cannot be replaced by an SkColorFilter. + TRACE_EVENT0("flutter", "Canvas::saveLayer"); + } + canvas_->drawPaint(sk_paint); } void DisplayListCanvasDispatcher::drawColor(SkColor color, SkBlendMode mode) { canvas_->drawColor(color, mode); @@ -170,8 +178,13 @@ void DisplayListCanvasDispatcher::drawAtlas(const sk_sp atlas, void DisplayListCanvasDispatcher::drawPicture(const sk_sp picture, const SkMatrix* matrix, bool render_with_attributes) { - canvas_->drawPicture(picture, matrix, - render_with_attributes ? &paint() : nullptr); + if (render_with_attributes) { + // drawPicture does an implicit saveLayer if an SkPaint is supplied. + TRACE_EVENT0("flutter", "Canvas::saveLayer"); + canvas_->drawPicture(picture, matrix, &paint()); + } else { + canvas_->drawPicture(picture, matrix, nullptr); + } } void DisplayListCanvasDispatcher::drawDisplayList( const sk_sp display_list) { @@ -248,7 +261,7 @@ void DisplayListCanvasRecorder::willSave() { SkCanvas::SaveLayerStrategy DisplayListCanvasRecorder::getSaveLayerStrategy( const SaveLayerRec& rec) { if (rec.fPaint) { - RecordPaintAttributes(rec.fPaint, DrawType::kSaveLayerOpType); + builder_->setAttributesFromPaint(*rec.fPaint, kSaveLayerWithPaintFlags); builder_->saveLayer(rec.fBounds, true); } else { builder_->saveLayer(rec.fBounds, false); @@ -260,28 +273,28 @@ void DisplayListCanvasRecorder::didRestore() { } void DisplayListCanvasRecorder::onDrawPaint(const SkPaint& paint) { - RecordPaintAttributes(&paint, DrawType::kFillOpType); + builder_->setAttributesFromPaint(paint, kDrawPaintFlags); builder_->drawPaint(); } void DisplayListCanvasRecorder::onDrawRect(const SkRect& rect, const SkPaint& paint) { - RecordPaintAttributes(&paint, DrawType::kDrawOpType); + builder_->setAttributesFromPaint(paint, kDrawRectFlags); builder_->drawRect(rect); } void DisplayListCanvasRecorder::onDrawRRect(const SkRRect& rrect, const SkPaint& paint) { - RecordPaintAttributes(&paint, DrawType::kDrawOpType); + builder_->setAttributesFromPaint(paint, kDrawRRectFlags); builder_->drawRRect(rrect); } void DisplayListCanvasRecorder::onDrawDRRect(const SkRRect& outer, const SkRRect& inner, const SkPaint& paint) { - RecordPaintAttributes(&paint, DrawType::kDrawOpType); + builder_->setAttributesFromPaint(paint, kDrawDRRectFlags); builder_->drawDRRect(outer, inner); } void DisplayListCanvasRecorder::onDrawOval(const SkRect& rect, const SkPaint& paint) { - RecordPaintAttributes(&paint, DrawType::kDrawOpType); + builder_->setAttributesFromPaint(paint, kDrawOvalFlags); builder_->drawOval(rect); } void DisplayListCanvasRecorder::onDrawArc(const SkRect& rect, @@ -289,12 +302,15 @@ void DisplayListCanvasRecorder::onDrawArc(const SkRect& rect, SkScalar sweepAngle, bool useCenter, const SkPaint& paint) { - RecordPaintAttributes(&paint, DrawType::kDrawOpType); + builder_->setAttributesFromPaint(paint, + useCenter // + ? kDrawArcWithCenterFlags + : kDrawArcNoCenterFlags); builder_->drawArc(rect, startAngle, sweepAngle, useCenter); } void DisplayListCanvasRecorder::onDrawPath(const SkPath& path, const SkPaint& paint) { - RecordPaintAttributes(&paint, DrawType::kDrawOpType); + builder_->setAttributesFromPaint(paint, kDrawPathFlags); builder_->drawPath(path); } @@ -302,7 +318,17 @@ void DisplayListCanvasRecorder::onDrawPoints(SkCanvas::PointMode mode, size_t count, const SkPoint pts[], const SkPaint& paint) { - RecordPaintAttributes(&paint, DrawType::kStrokeOpType); + switch (mode) { + case SkCanvas::kPoints_PointMode: + builder_->setAttributesFromPaint(paint, kDrawPointsAsPointsFlags); + break; + case SkCanvas::kLines_PointMode: + builder_->setAttributesFromPaint(paint, kDrawPointsAsLinesFlags); + break; + case SkCanvas::kPolygon_PointMode: + builder_->setAttributesFromPaint(paint, kDrawPointsAsPolygonFlags); + break; + } if (mode == SkCanvas::PointMode::kLines_PointMode && count == 2) { builder_->drawLine(pts[0], pts[1]); } else { @@ -317,7 +343,7 @@ void DisplayListCanvasRecorder::onDrawPoints(SkCanvas::PointMode mode, void DisplayListCanvasRecorder::onDrawVerticesObject(const SkVertices* vertices, SkBlendMode mode, const SkPaint& paint) { - RecordPaintAttributes(&paint, DrawType::kDrawOpType); + builder_->setAttributesFromPaint(paint, kDrawVerticesFlags); builder_->drawVertices(sk_ref_sp(vertices), mode); } @@ -327,7 +353,7 @@ void DisplayListCanvasRecorder::onDrawImage2(const SkImage* image, const SkSamplingOptions& sampling, const SkPaint* paint) { if (paint != nullptr) { - RecordPaintAttributes(paint, DrawType::kImageOpType); + builder_->setAttributesFromPaint(*paint, kDrawImageWithPaintFlags); } builder_->drawImage(sk_ref_sp(image), SkPoint::Make(dx, dy), sampling, paint != nullptr); @@ -340,7 +366,7 @@ void DisplayListCanvasRecorder::onDrawImageRect2( const SkPaint* paint, SrcRectConstraint constraint) { if (paint != nullptr) { - RecordPaintAttributes(paint, DrawType::kImageRectOpType); + builder_->setAttributesFromPaint(*paint, kDrawImageRectWithPaintFlags); } builder_->drawImageRect(sk_ref_sp(image), src, dst, sampling, paint != nullptr, constraint); @@ -357,7 +383,7 @@ void DisplayListCanvasRecorder::onDrawImageLattice2(const SkImage* image, if (*paint == default_paint) { paint = nullptr; } else { - RecordPaintAttributes(paint, DrawType::kImageOpType); + builder_->setAttributesFromPaint(*paint, kDrawImageLatticeWithPaintFlags); } } builder_->drawImageLattice(sk_ref_sp(image), lattice, dst, filter, @@ -373,7 +399,7 @@ void DisplayListCanvasRecorder::onDrawAtlas2(const SkImage* image, const SkRect* cull, const SkPaint* paint) { if (paint != nullptr) { - RecordPaintAttributes(paint, DrawType::kImageOpType); + builder_->setAttributesFromPaint(*paint, kDrawAtlasWithPaintFlags); } builder_->drawAtlas(sk_ref_sp(image), xform, src, colors, count, mode, sampling, cull, paint != nullptr); @@ -383,7 +409,7 @@ void DisplayListCanvasRecorder::onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y, const SkPaint& paint) { - RecordPaintAttributes(&paint, DrawType::kDrawOpType); + builder_->setAttributesFromPaint(paint, kDrawTextBlobFlags); builder_->drawTextBlob(sk_ref_sp(blob), x, y); } void DisplayListCanvasRecorder::onDrawShadowRec(const SkPath& path, @@ -398,119 +424,9 @@ void DisplayListCanvasRecorder::onDrawPicture(const SkPicture* picture, const SkMatrix* matrix, const SkPaint* paint) { if (paint != nullptr) { - RecordPaintAttributes(paint, DrawType::kSaveLayerOpType); + builder_->setAttributesFromPaint(*paint, kDrawPictureWithPaintFlags); } builder_->drawPicture(sk_ref_sp(picture), matrix, paint != nullptr); } -void DisplayListCanvasRecorder::RecordPaintAttributes(const SkPaint* paint, - DrawType type) { - int dataNeeded; - switch (type) { - case DrawType::kDrawOpType: - dataNeeded = kDrawMask_; - break; - case DrawType::kFillOpType: - dataNeeded = kPaintMask_; - break; - case DrawType::kStrokeOpType: - dataNeeded = kStrokeMask_; - break; - case DrawType::kImageOpType: - dataNeeded = kImageMask_; - break; - case DrawType::kImageRectOpType: - dataNeeded = kImageRectMask_; - break; - case DrawType::kSaveLayerOpType: - dataNeeded = kSaveLayerMask_; - break; - default: - FML_DCHECK(false); - return; - } - if (paint == nullptr) { - paint = new SkPaint(); - } - if ((dataNeeded & kAaNeeded_) != 0 && current_aa_ != paint->isAntiAlias()) { - builder_->setAntiAlias(current_aa_ = paint->isAntiAlias()); - } - if ((dataNeeded & kDitherNeeded_) != 0 && - current_dither_ != paint->isDither()) { - builder_->setDither(current_dither_ = paint->isDither()); - } - if ((dataNeeded & kColorNeeded_) != 0 && - current_color_ != paint->getColor()) { - builder_->setColor(current_color_ = paint->getColor()); - } - if ((dataNeeded & kBlendNeeded_)) { - skstd::optional mode_optional = paint->asBlendMode(); - if (mode_optional) { - SkBlendMode mode = mode_optional.value(); - if (current_blender_ || current_blend_ != mode) { - builder_->setBlendMode(current_blend_ = mode); - current_blender_ = nullptr; - } - } else { - if (current_blender_.get() != paint->getBlender()) { - builder_->setBlender(current_blender_ = sk_ref_sp(paint->getBlender())); - } - } - } - // invert colors is a Flutter::Paint thing, not an SkPaint thing - // if ((dataNeeded & invertColorsNeeded_) != 0 && - // currentInvertColors_ != paint->???) { - // currentInvertColors_ = paint->invertColors; - // addOp_(currentInvertColors_ - // ? _CanvasOp.setInvertColors - // : _CanvasOp.clearInvertColors, 0); - // } - if ((dataNeeded & kPaintStyleNeeded_) != 0) { - if (current_style_ != paint->getStyle()) { - builder_->setStyle(current_style_ = paint->getStyle()); - } - if (current_style_ == SkPaint::Style::kStroke_Style) { - dataNeeded |= kStrokeStyleNeeded_; - } - } - if ((dataNeeded & kStrokeStyleNeeded_) != 0) { - if (current_stroke_width_ != paint->getStrokeWidth()) { - builder_->setStrokeWidth(current_stroke_width_ = paint->getStrokeWidth()); - } - if (current_cap_ != paint->getStrokeCap()) { - builder_->setStrokeCap(current_cap_ = paint->getStrokeCap()); - } - if (current_join_ != paint->getStrokeJoin()) { - builder_->setStrokeJoin(current_join_ = paint->getStrokeJoin()); - } - if (current_miter_limit_ != paint->getStrokeMiter()) { - builder_->setStrokeMiter(current_miter_limit_ = paint->getStrokeMiter()); - } - } - if ((dataNeeded & kShaderNeeded_) != 0 && - current_shader_.get() != paint->getShader()) { - builder_->setShader(current_shader_ = sk_ref_sp(paint->getShader())); - } - if ((dataNeeded & kColorFilterNeeded_) != 0 && - current_color_filter_.get() != paint->getColorFilter()) { - builder_->setColorFilter(current_color_filter_ = - sk_ref_sp(paint->getColorFilter())); - } - if ((dataNeeded & kImageFilterNeeded_) != 0 && - current_image_filter_.get() != paint->getImageFilter()) { - builder_->setImageFilter(current_image_filter_ = - sk_ref_sp(paint->getImageFilter())); - } - if ((dataNeeded & kPathEffectNeeded_) != 0 && - current_path_effect_.get() != paint->getPathEffect()) { - builder_->setPathEffect(current_path_effect_ = - sk_ref_sp(paint->getPathEffect())); - } - if ((dataNeeded & kMaskFilterNeeded_) != 0 && - current_mask_filter_.get() != paint->getMaskFilter()) { - builder_->setMaskFilter(current_mask_filter_ = - sk_ref_sp(paint->getMaskFilter())); - } -} - } // namespace flutter diff --git a/flow/display_list_canvas.h b/flow/display_list_canvas.h index a6ef6902f6b10..6c4d0f77275db 100644 --- a/flow/display_list_canvas.h +++ b/flow/display_list_canvas.h @@ -27,7 +27,7 @@ namespace flutter { class DisplayListCanvasDispatcher : public virtual Dispatcher, public SkPaintDispatchHelper { public: - DisplayListCanvasDispatcher(SkCanvas* canvas) : canvas_(canvas) {} + explicit DisplayListCanvasDispatcher(SkCanvas* canvas) : canvas_(canvas) {} void save() override; void restore() override; @@ -49,9 +49,9 @@ class DisplayListCanvasDispatcher : public virtual Dispatcher, SkScalar mwx, SkScalar mwy, SkScalar mwz, SkScalar mwt) override; // clang-format on - void clipRect(const SkRect& rect, SkClipOp clip_op, bool isAA) override; - void clipRRect(const SkRRect& rrect, SkClipOp clip_op, bool isAA) override; - void clipPath(const SkPath& path, SkClipOp clip_op, bool isAA) override; + void clipRect(const SkRect& rect, SkClipOp clip_op, bool is_aa) override; + void clipRRect(const SkRRect& rrect, SkClipOp clip_op, bool is_aa) override; + void clipPath(const SkPath& path, SkClipOp clip_op, bool is_aa) override; void drawPaint() override; void drawColor(SkColor color, SkBlendMode mode) override; @@ -120,9 +120,10 @@ class DisplayListCanvasDispatcher : public virtual Dispatcher, // Receives all methods on SkCanvas and sends them to a DisplayListBuilder class DisplayListCanvasRecorder : public SkCanvasVirtualEnforcer, - public SkRefCnt { + public SkRefCnt, + DisplayListOpFlags { public: - DisplayListCanvasRecorder(const SkRect& bounds); + explicit DisplayListCanvasRecorder(const SkRect& bounds); const sk_sp builder() { return builder_; } @@ -233,80 +234,8 @@ class DisplayListCanvasRecorder const SkMatrix* matrix, const SkPaint* paint) override; - enum class DrawType { - // The operation will be an image operation - kImageOpType, - // The operation will be an imageRect operation - kImageRectOpType, - // The operation will be a fill or stroke depending on the paint.style - kDrawOpType, - // The operation will be a fill (ignoring paint.style) - kFillOpType, - // The operation will be a stroke (ignoring paint.style) - kStrokeOpType, - // The operation will be a saveLayer with a paint object - kSaveLayerOpType, - }; - - void RecordPaintAttributes(const SkPaint* paint, DrawType type); - private: sk_sp builder_; - - // Mask bits for the various attributes that might be needed for a given - // operation. - // clang-format off - static constexpr int kAaNeeded_ = 1 << 0; - static constexpr int kColorNeeded_ = 1 << 1; - static constexpr int kBlendNeeded_ = 1 << 2; - static constexpr int kInvertColorsNeeded_ = 1 << 3; - static constexpr int kPaintStyleNeeded_ = 1 << 4; - static constexpr int kStrokeStyleNeeded_ = 1 << 5; - static constexpr int kShaderNeeded_ = 1 << 6; - static constexpr int kColorFilterNeeded_ = 1 << 7; - static constexpr int kImageFilterNeeded_ = 1 << 8; - static constexpr int kPathEffectNeeded_ = 1 << 9; - static constexpr int kMaskFilterNeeded_ = 1 << 10; - static constexpr int kDitherNeeded_ = 1 << 11; - // clang-format on - - // Combinations of the above mask bits that are common to typical "draw" - // calls. - // Note that the strokeStyle_ is handled conditionally depending on whether - // the paintStyle_ attribute value is synchronized. It can also be manually - // specified for operations that will be always stroking, like [drawLine]. - static constexpr int kPaintMask_ = kAaNeeded_ | kColorNeeded_ | - kBlendNeeded_ | kInvertColorsNeeded_ | - kColorFilterNeeded_ | kShaderNeeded_ | - kDitherNeeded_ | kImageFilterNeeded_; - static constexpr int kDrawMask_ = kPaintMask_ | kPaintStyleNeeded_ | - kMaskFilterNeeded_ | kPathEffectNeeded_; - static constexpr int kStrokeMask_ = kPaintMask_ | kStrokeStyleNeeded_ | - kMaskFilterNeeded_ | kPathEffectNeeded_; - static constexpr int kImageMask_ = kColorNeeded_ | kBlendNeeded_ | - kInvertColorsNeeded_ | - kColorFilterNeeded_ | kDitherNeeded_ | - kImageFilterNeeded_ | kMaskFilterNeeded_; - static constexpr int kImageRectMask_ = kImageMask_ | kAaNeeded_; - static constexpr int kSaveLayerMask_ = - kColorNeeded_ | kBlendNeeded_ | kInvertColorsNeeded_ | - kColorFilterNeeded_ | kImageFilterNeeded_; - - bool current_aa_ = false; - bool current_dither_ = false; - SkColor current_color_ = 0xFF000000; - SkBlendMode current_blend_ = SkBlendMode::kSrcOver; - SkPaint::Style current_style_ = SkPaint::Style::kFill_Style; - SkScalar current_stroke_width_ = 0.0; - SkScalar current_miter_limit_ = 4.0; - SkPaint::Cap current_cap_ = SkPaint::Cap::kButt_Cap; - SkPaint::Join current_join_ = SkPaint::Join::kMiter_Join; - sk_sp current_blender_; - sk_sp current_shader_; - sk_sp current_color_filter_; - sk_sp current_image_filter_; - sk_sp current_path_effect_; - sk_sp current_mask_filter_; }; } // namespace flutter diff --git a/flow/display_list_canvas_unittests.cc b/flow/display_list_canvas_unittests.cc index 9bb35ebd9c77d..4352c01008ad1 100644 --- a/flow/display_list_canvas_unittests.cc +++ b/flow/display_list_canvas_unittests.cc @@ -70,11 +70,13 @@ constexpr SkScalar Miter4DiamondOffsetY = RenderWidth * 0.14; // Render 3 vertical and horizontal diamonds each // designed to break at the tested miter limits // 0.0, 4.0 and 10.0 -constexpr SkScalar x_off_0 = RenderCenterX; -constexpr SkScalar x_off_l1 = RenderCenterX - Miter4DiamondOffsetX; +// Center is biased by 0.5 to include more pixel centers in the +// thin miters +constexpr SkScalar x_off_0 = RenderCenterX + 0.5; +constexpr SkScalar x_off_l1 = x_off_0 - Miter4DiamondOffsetX; constexpr SkScalar x_off_l2 = x_off_l1 - Miter10DiamondOffsetX; constexpr SkScalar x_off_l3 = x_off_l2 - Miter10DiamondOffsetX; -constexpr SkScalar x_off_r1 = RenderCenterX + Miter4DiamondOffsetX; +constexpr SkScalar x_off_r1 = x_off_0 + Miter4DiamondOffsetX; constexpr SkScalar x_off_r2 = x_off_r1 + MiterExtremeDiamondOffsetX; constexpr SkScalar x_off_r3 = x_off_r2 + MiterExtremeDiamondOffsetX; constexpr SkPoint VerticalMiterDiamondPoints[] = { @@ -104,11 +106,11 @@ constexpr SkPoint VerticalMiterDiamondPoints[] = { const int VerticalMiterDiamondPointCount = sizeof(VerticalMiterDiamondPoints) / sizeof(VerticalMiterDiamondPoints[0]); -constexpr SkScalar y_off_0 = RenderCenterY; -constexpr SkScalar y_off_u1 = RenderCenterY - Miter4DiamondOffsetY; +constexpr SkScalar y_off_0 = RenderCenterY + 0.5; +constexpr SkScalar y_off_u1 = x_off_0 - Miter4DiamondOffsetY; constexpr SkScalar y_off_u2 = y_off_u1 - Miter10DiamondOffsetY; constexpr SkScalar y_off_u3 = y_off_u2 - Miter10DiamondOffsetY; -constexpr SkScalar y_off_d1 = RenderCenterY + Miter4DiamondOffsetY; +constexpr SkScalar y_off_d1 = x_off_0 + Miter4DiamondOffsetY; constexpr SkScalar y_off_d2 = y_off_d1 + MiterExtremeDiamondOffsetY; constexpr SkScalar y_off_d3 = y_off_d2 + MiterExtremeDiamondOffsetY; const SkPoint HorizontalMiterDiamondPoints[] = { @@ -153,75 +155,67 @@ const int HorizontalMiterDiamondPointCount = // avoid false bounds overflow notifications. class BoundsTolerance { public: - BoundsTolerance() : BoundsTolerance(0, 0, 1, 1, 0, 0, 0) {} - BoundsTolerance(SkScalar bounds_pad_x, - SkScalar bounds_pad_y, - SkScalar scale_x, - SkScalar scale_y, - SkScalar absolute_pad_x, - SkScalar absolute_pad_y, - SkScalar discrete_offset) - : bounds_pad_x_(bounds_pad_x), - bounds_pad_y_(bounds_pad_y), - scale_x_(scale_x), - scale_y_(scale_y), - absolute_pad_x_(absolute_pad_x), - absolute_pad_y_(absolute_pad_y), - discrete_offset_(discrete_offset) {} + BoundsTolerance() = default; + BoundsTolerance(const BoundsTolerance&) = default; BoundsTolerance addBoundsPadding(SkScalar bounds_pad_x, SkScalar bounds_pad_y) const { - return {bounds_pad_x_ + bounds_pad_x, - bounds_pad_y_ + bounds_pad_y, - scale_x_, - scale_y_, - absolute_pad_x_, - absolute_pad_y_, - discrete_offset_}; + BoundsTolerance copy = BoundsTolerance(*this); + copy.bounds_pad_.offset(bounds_pad_x, bounds_pad_y); + return copy; } - BoundsTolerance addScale(SkScalar scale_x, SkScalar scale_y) const { - return {bounds_pad_x_, // - bounds_pad_y_, // - scale_x_ * scale_x, // - scale_y_ * scale_y, // - absolute_pad_x_, // - absolute_pad_y_, // - discrete_offset_}; + BoundsTolerance mulScale(SkScalar scale_x, SkScalar scale_y) const { + BoundsTolerance copy = BoundsTolerance(*this); + copy.scale_.fX *= scale_x; + copy.scale_.fY *= scale_y; + return copy; } BoundsTolerance addAbsolutePadding(SkScalar absolute_pad_x, SkScalar absolute_pad_y) const { - return {bounds_pad_x_, - bounds_pad_y_, - scale_x_, - scale_y_, - absolute_pad_x_ + absolute_pad_x, - absolute_pad_y_ + absolute_pad_y, - discrete_offset_}; + BoundsTolerance copy = BoundsTolerance(*this); + copy.absolute_pad_.offset(absolute_pad_x, absolute_pad_y); + return copy; } BoundsTolerance addDiscreteOffset(SkScalar discrete_offset) const { - return {bounds_pad_x_, - bounds_pad_y_, - scale_x_, - scale_y_, - absolute_pad_x_, - absolute_pad_y_, - discrete_offset_ + discrete_offset}; + BoundsTolerance copy = BoundsTolerance(*this); + copy.discrete_offset_ += discrete_offset; + return copy; } - bool overflows(SkISize pix_size, + BoundsTolerance clip(SkRect clip) const { + BoundsTolerance copy = BoundsTolerance(*this); + if (!copy.clip_.intersect(clip)) { + copy.clip_.setEmpty(); + } + return copy; + } + + static SkRect Scale(const SkRect& rect, const SkPoint& scales) { + SkScalar outset_x = rect.width() * (scales.fX - 1); + SkScalar outset_y = rect.height() * (scales.fY - 1); + return rect.makeOutset(outset_x, outset_y); + } + + bool overflows(SkIRect pix_bounds, int worst_bounds_pad_x, int worst_bounds_pad_y) const { - int scaled_bounds_pad_x = - std::ceil((pix_size.width() + bounds_pad_x_) * scale_x_); - int allowed_width = scaled_bounds_pad_x + absolute_pad_x_; - int scaled_bounds_pad_y = - std::ceil((pix_size.height() + bounds_pad_y_) * scale_y_); - int allowed_height = scaled_bounds_pad_y + absolute_pad_y_; - int allowed_pad_x = allowed_width - pix_size.width(); - int allowed_pad_y = allowed_height - pix_size.height(); + SkRect allowed = SkRect::Make(pix_bounds); + allowed.outset(bounds_pad_.fX, bounds_pad_.fY); + allowed = Scale(allowed, scale_); + allowed.outset(absolute_pad_.fX, absolute_pad_.fY); + if (!allowed.intersect(clip_)) { + allowed.setEmpty(); + } + SkIRect rounded = allowed.roundOut(); + int padLeft = std::max(0, pix_bounds.fLeft - rounded.fLeft); + int padTop = std::max(0, pix_bounds.fTop - rounded.fTop); + int padRight = std::max(0, pix_bounds.fRight - rounded.fRight); + int padBottom = std::max(0, pix_bounds.fBottom - rounded.fBottom); + int allowed_pad_x = std::max(padLeft, padRight); + int allowed_pad_y = std::max(padTop, padBottom); if (worst_bounds_pad_x > allowed_pad_x || worst_bounds_pad_y > allowed_pad_y) { FML_LOG(ERROR) << "allowed pad: " // @@ -234,18 +228,301 @@ class BoundsTolerance { SkScalar discrete_offset() const { return discrete_offset_; } private: - SkScalar bounds_pad_x_; - SkScalar bounds_pad_y_; - SkScalar scale_x_; - SkScalar scale_y_; - SkScalar absolute_pad_x_; - SkScalar absolute_pad_y_; - - SkScalar discrete_offset_; + SkPoint bounds_pad_ = {0, 0}; + SkPoint scale_ = {1, 1}; + SkPoint absolute_pad_ = {0, 0}; + SkRect clip_ = {-1E9, -1E9, 1E9, 1E9}; + + SkScalar discrete_offset_ = 0; }; -class CanvasCompareTester { +typedef const std::function CvSetup; +typedef const std::function CvRenderer; +typedef const std::function DlRenderer; +static void EmptyCvRenderer(SkCanvas*, const SkPaint&) {} +static void EmptyDlRenderer(DisplayListBuilder&) {} + +class RenderSurface { + public: + explicit RenderSurface(sk_sp surface) : surface_(surface) {} + ~RenderSurface() { sk_free(addr_); } + + SkCanvas* canvas() { return surface_->getCanvas(); } + + const SkPixmap* pixmap() { + if (!pixmap_.addr()) { + SkImageInfo info = surface_->imageInfo(); + if (info.colorType() != kN32_SkColorType || + !surface_->peekPixels(&pixmap_)) { + info = SkImageInfo::MakeN32Premul(info.dimensions()); + addr_ = malloc(info.computeMinByteSize() * info.height()); + pixmap_.reset(info, addr_, info.minRowBytes()); + EXPECT_TRUE(surface_->readPixels(pixmap_, 0, 0)); + } + } + return &pixmap_; + } + + private: + sk_sp surface_; + SkPixmap pixmap_; + void* addr_ = nullptr; +}; + +class RenderEnvironment { + public: + static RenderEnvironment Make565() { + return RenderEnvironment(SkImageInfo::Make({1, 1}, kRGB_565_SkColorType, + kOpaque_SkAlphaType, nullptr)); + } + + static RenderEnvironment MakeN32() { + return RenderEnvironment(SkImageInfo::MakeN32Premul(1, 1)); + } + + RenderSurface MakeSurface(const SkColor bg = SK_ColorTRANSPARENT, + int width = TestWidth, + int height = TestHeight) const { + sk_sp surface = + SkSurface::MakeRaster(info_.makeWH(width, height)); + surface->getCanvas()->clear(bg); + return RenderSurface(surface); + } + + void init_ref(CvRenderer& cv_renderer, SkColor bg = SK_ColorTRANSPARENT) { + init_ref([=](SkCanvas*, SkPaint&) {}, cv_renderer, bg); + } + + void init_ref(CvSetup& cv_setup, + CvRenderer& cv_renderer, + SkColor bg = SK_ColorTRANSPARENT) { + ref_canvas()->clear(bg); + cv_setup(ref_canvas(), ref_paint_); + ref_matrix_ = ref_canvas()->getTotalMatrix(); + ref_clip_ = ref_canvas()->getDeviceClipBounds(); + cv_renderer(ref_canvas(), ref_paint_); + ref_pixmap_ = ref_surface_.pixmap(); + } + + SkCanvas* ref_canvas() { return ref_surface_.canvas(); } + const SkPaint& ref_paint() const { return ref_paint_; } + const SkMatrix& ref_matrix() const { return ref_matrix_; } + const SkIRect& ref_clip_bounds() const { return ref_clip_; } + const SkPixmap* ref_pixmap() const { return ref_pixmap_; } + private: + explicit RenderEnvironment(const SkImageInfo& info) + : info_(info), ref_surface_(MakeSurface()) {} + + const SkImageInfo info_; + + SkPaint ref_paint_; + SkMatrix ref_matrix_; + SkIRect ref_clip_; + RenderSurface ref_surface_; + const SkPixmap* ref_pixmap_ = nullptr; +}; + +class TestParameters { + public: + TestParameters(const CvRenderer& cv_renderer, + const DlRenderer& dl_renderer, + const DisplayListAttributeFlags& flags) + : cv_renderer_(cv_renderer), dl_renderer_(dl_renderer), flags_(flags) {} + + bool uses_paint() const { return !flags_.ignores_paint(); } + + bool should_match(const RenderEnvironment& env, + const SkPaint& paint, + const SkMatrix& matrix, + const SkIRect& device_clip, + bool has_diff_clip, + bool has_mutating_save_layer) const { + if (has_mutating_save_layer) { + return false; + } + if (env.ref_clip_bounds() != device_clip || has_diff_clip) { + return false; + } + if (env.ref_matrix() != matrix && !flags_.is_flood()) { + return false; + } + if (flags_.ignores_paint()) { + return true; + } + const SkPaint& ref_paint = env.ref_paint(); + if (flags_.applies_anti_alias() && // + ref_paint.isAntiAlias() != paint.isAntiAlias()) { + return false; + } + if (flags_.applies_dither() && // + ref_paint.isDither() != paint.isDither()) { + return false; + } + if (flags_.applies_color() && // + ref_paint.getColor() != paint.getColor()) { + return false; + } + if (flags_.applies_alpha() && // + ref_paint.getAlpha() != paint.getAlpha()) { + return false; + } + if (flags_.applies_blend() && // + ref_paint.getBlender() != paint.getBlender()) { + return false; + } + if (flags_.applies_color_filter() && // + ref_paint.getColorFilter() != paint.getColorFilter()) { + return false; + } + if (flags_.applies_mask_filter() && // + ref_paint.getMaskFilter() != paint.getMaskFilter()) { + return false; + } + if (flags_.applies_image_filter() && // + ref_paint.getImageFilter() != paint.getImageFilter()) { + return false; + } + if (flags_.applies_shader() && // + ref_paint.getShader() != paint.getShader()) { + return false; + } + DisplayListSpecialGeometryFlags geo_flags = + flags_.WithPathEffect(paint.refPathEffect()); + if (flags_.applies_path_effect() && // + ref_paint.getPathEffect() != paint.getPathEffect()) { + SkPathEffect::DashInfo info; + if (paint.getPathEffect()->asADash(&info) != + SkPathEffect::kDash_DashType) { + return false; + } + if (!ignores_dashes()) { + return false; + } + } + bool is_stroked = flags_.is_stroked(ref_paint.getStyle()); + if (flags_.is_stroked(paint.getStyle()) != is_stroked) { + return false; + } + if (!is_stroked) { + return true; + } + if (ref_paint.getStrokeWidth() != paint.getStrokeWidth()) { + return false; + } + if (geo_flags.may_have_end_caps() && // + getCap(ref_paint, geo_flags) != getCap(paint, geo_flags)) { + return false; + } + if (geo_flags.may_have_joins()) { + if (ref_paint.getStrokeJoin() != paint.getStrokeJoin()) { + return false; + } + if (ref_paint.getStrokeJoin() == SkPaint::kMiter_Join) { + SkScalar ref_miter = ref_paint.getStrokeMiter(); + SkScalar test_miter = paint.getStrokeMiter(); + // miter limit < 1.4 affects right angles + if (geo_flags.may_have_acute_joins() || // + ref_miter < 1.4 || test_miter < 1.4) { + if (ref_miter != test_miter) { + return false; + } + } + } + } + return true; + } + + SkPaint::Cap getCap(const SkPaint& paint, + DisplayListSpecialGeometryFlags geo_flags) const { + SkPaint::Cap cap = paint.getStrokeCap(); + if (geo_flags.butt_cap_becomes_square() && cap == SkPaint::kButt_Cap) { + return SkPaint::kSquare_Cap; + } + return cap; + } + + const BoundsTolerance adjust(const BoundsTolerance& tolerance, + const SkPaint& paint, + const SkMatrix& matrix) const { + if (is_draw_text_blob() && tolerance.discrete_offset() > 0) { + // drawTextBlob needs just a little more leeway when using a + // discrete path effect. + return tolerance.addBoundsPadding(2, 2); + } + if (is_draw_line()) { + return lineAdjust(tolerance, paint, matrix); + } + if (is_draw_arc_center()) { + if (paint.getStyle() != SkPaint::kFill_Style && + paint.getStrokeJoin() == SkPaint::kMiter_Join) { + // the miter join at the center of an arc does not really affect + // its bounds in any of our test cases, but the bounds code needs + // to take it into account for the cases where it might, so we + // relax our tolerance to reflect the miter bounds padding. + SkScalar miter_pad = + paint.getStrokeMiter() * paint.getStrokeWidth() * 0.5f; + return tolerance.addBoundsPadding(miter_pad, miter_pad); + } + } + return tolerance; + } + + const BoundsTolerance lineAdjust(const BoundsTolerance& tolerance, + const SkPaint& paint, + const SkMatrix& matrix) const { + SkScalar adjust = 0.0; + SkScalar half_width = paint.getStrokeWidth() * 0.5f; + if (tolerance.discrete_offset() > 0) { + // When a discrete path effect is added, the bounds calculations must + // allow for miters in any direction, but a horizontal line will not + // have miters in the horizontal direction, similarly for vertical + // lines, and diagonal lines will have miters off at a "45 degree" + // angle that don't expand the bounds much at all. + // Also, the discrete offset will not move any points parallel with + // the line, so provide tolerance for both miters and offset. + adjust = + half_width * paint.getStrokeMiter() + tolerance.discrete_offset(); + } + DisplayListSpecialGeometryFlags geo_flags = + flags_.WithPathEffect(paint.refPathEffect()); + if (paint.getStrokeCap() == SkPaint::kButt_Cap && + !geo_flags.butt_cap_becomes_square()) { + adjust = std::max(adjust, half_width); + } + if (adjust == 0) { + return tolerance; + } + SkScalar hTolerance; + SkScalar vTolerance; + if (is_horizontal_line()) { + FML_DCHECK(!is_vertical_line()); + hTolerance = adjust; + vTolerance = 0; + } else if (is_vertical_line()) { + hTolerance = 0; + vTolerance = adjust; + } else { + // The perpendicular miters just do not impact the bounds of + // diagonal lines at all as they are aimed in the wrong direction + // to matter. So allow tolerance in both axes. + hTolerance = vTolerance = adjust; + } + BoundsTolerance new_tolerance = + tolerance.addBoundsPadding(hTolerance, vTolerance); + return new_tolerance; + } + + const CvRenderer& cv_renderer() const { return cv_renderer_; } + void render_to(SkCanvas* canvas, SkPaint& paint) const { + cv_renderer_(canvas, paint); + } + + const DlRenderer& dl_renderer() const { return dl_renderer_; } + void render_to(DisplayListBuilder& builder) const { // + dl_renderer_(builder); + } + // If a test is using any shadow operations then we cannot currently // record those in an SkCanvas and play it back into a DisplayList // because internally the operation gets encapsulated in a Skia @@ -253,334 +530,576 @@ class CanvasCompareTester { // that use shadows, we can perform a lot of tests, but not the tests // that require SkCanvas->DisplayList transfers. // See: https://bugs.chromium.org/p/skia/issues/detail?id=12125 - static bool TestingDrawShadows; + bool is_draw_shadows() const { return is_draw_shadows_; } // The CPU renders nothing for drawVertices with a Blender. // See: https://bugs.chromium.org/p/skia/issues/detail?id=12200 - static bool TestingDrawVertices; + bool is_draw_vertices() const { return is_draw_vertices_; } // The CPU renders nothing for drawAtlas with a Blender. // See: https://bugs.chromium.org/p/skia/issues/detail?id=12199 - static bool TestingDrawAtlas; + bool is_draw_atlas() const { return is_draw_atlas_; } + // Tests that call drawTextBlob with an sk_ref paint attribute will cause + // those attributes to be stored in an internal Skia cache so we need + // to expect that the |sk_ref.unique()| call will fail in those cases. + // See: (TBD(flar) - file Skia bug) + bool is_draw_text_blob() const { return is_draw_text_blob_; } + bool is_draw_display_list() const { return is_draw_display_list_; } + bool is_draw_line() const { return is_draw_line_; } + bool is_draw_arc_center() const { return is_draw_arc_center_; } + bool is_horizontal_line() const { return is_horizontal_line_; } + bool is_vertical_line() const { return is_vertical_line_; } + bool ignores_dashes() const { return ignores_dashes_; } + + TestParameters& set_draw_shadows() { + is_draw_shadows_ = true; + return *this; + } + TestParameters& set_draw_vertices() { + is_draw_vertices_ = true; + return *this; + } + TestParameters& set_draw_text_blob() { + is_draw_text_blob_ = true; + return *this; + } + TestParameters& set_draw_atlas() { + is_draw_atlas_ = true; + return *this; + } + TestParameters& set_draw_display_list() { + is_draw_display_list_ = true; + return *this; + } + TestParameters& set_draw_line() { + is_draw_line_ = true; + return *this; + } + TestParameters& set_draw_arc_center() { + is_draw_arc_center_ = true; + return *this; + } + TestParameters& set_ignores_dashes() { + ignores_dashes_ = true; + return *this; + } + TestParameters& set_horizontal_line() { + is_horizontal_line_ = true; + return *this; + } + TestParameters& set_vertical_line() { + is_vertical_line_ = true; + return *this; + } - public: - typedef const std::function CvRenderer; - typedef const std::function DlRenderer; - typedef const std::function - ToleranceAdjuster; + private: + const CvRenderer& cv_renderer_; + const DlRenderer& dl_renderer_; + const DisplayListAttributeFlags& flags_; + + bool is_draw_shadows_ = false; + bool is_draw_vertices_ = false; + bool is_draw_text_blob_ = false; + bool is_draw_atlas_ = false; + bool is_draw_display_list_ = false; + bool is_draw_line_ = false; + bool is_draw_arc_center_ = false; + bool ignores_dashes_ = false; + bool is_horizontal_line_ = false; + bool is_vertical_line_ = false; +}; - static BoundsTolerance DefaultTolerance; - static const BoundsTolerance DefaultAdjuster(const BoundsTolerance& tolerance, - const SkPaint& paint, - const SkMatrix& matrix) { - return tolerance; +class CaseParameters { + public: + explicit CaseParameters(std::string info) + : CaseParameters(info, EmptyCvRenderer, EmptyDlRenderer) {} + + CaseParameters(std::string info, CvSetup cv_setup, DlRenderer dl_setup) + : CaseParameters(info, + cv_setup, + dl_setup, + EmptyCvRenderer, + EmptyDlRenderer, + SK_ColorTRANSPARENT, + false, + false) {} + + CaseParameters(std::string info, + CvSetup cv_setup, + DlRenderer dl_setup, + CvRenderer cv_restore, + DlRenderer dl_restore, + SkColor bg, + bool has_diff_clip, + bool has_mutating_save_layer) + : info_(info), + bg_(bg), + cv_setup_(cv_setup), + dl_setup_(dl_setup), + cv_restore_(cv_restore), + dl_restore_(dl_restore), + has_diff_clip_(has_diff_clip), + has_mutating_save_layer_(has_mutating_save_layer) {} + + CaseParameters with_restore(CvRenderer cv_restore, + DlRenderer dl_restore, + bool mutating_layer) { + return CaseParameters(info_, cv_setup_, dl_setup_, cv_restore, dl_restore, + bg_, has_diff_clip_, mutating_layer); } - // All of the tests should eventually use this method except for the - // tests that call |RenderNoAttributes| because they do not use the - // SkPaint object. - // But there are a couple of conditions beyond our control which require - // the use of one of the variant methods below (|RenderShadows|, - // |RenderVertices|, |RenderAtlas|). - static void RenderAll(CvRenderer& cv_renderer, - DlRenderer& dl_renderer, - ToleranceAdjuster& adjuster = DefaultAdjuster, - const BoundsTolerance& tolerance = DefaultTolerance) { - RenderNoAttributes(cv_renderer, dl_renderer, adjuster, tolerance); - RenderWithAttributes(cv_renderer, dl_renderer, adjuster, tolerance); + CaseParameters with_bg(SkColor bg) { + return CaseParameters(info_, cv_setup_, dl_setup_, cv_restore_, dl_restore_, + bg, has_diff_clip_, has_mutating_save_layer_); } - // Used by the tests that render shadows to deal with a condition where - // we cannot recapture the shadow information from an SkCanvas stream - // due to the DrawShadowRec used by Skia is not properly exported. - // See: https://bugs.chromium.org/p/skia/issues/detail?id=12125 - static void RenderShadows( - CvRenderer& cv_renderer, - DlRenderer& dl_renderer, - ToleranceAdjuster& adjuster = DefaultAdjuster, - const BoundsTolerance& tolerance = DefaultTolerance) { - TestingDrawShadows = true; - RenderNoAttributes(cv_renderer, dl_renderer, adjuster, tolerance); - TestingDrawShadows = false; + CaseParameters with_diff_clip() { + return CaseParameters(info_, cv_setup_, dl_setup_, cv_restore_, dl_restore_, + bg_, true, has_mutating_save_layer_); } - // Used by the tests that call drawVertices to avoid using an SkBlender - // during testing because the CPU renderer appears not to render anything. - // See: https://bugs.chromium.org/p/skia/issues/detail?id=12200 - static void RenderVertices(CvRenderer& cv_renderer, DlRenderer& dl_renderer) { - TestingDrawVertices = true; - RenderAll(cv_renderer, dl_renderer); - TestingDrawVertices = false; + std::string info() const { return info_; } + SkColor bg() const { return bg_; } + bool has_diff_clip() const { return has_diff_clip_; } + bool has_mutating_save_layer() const { return has_mutating_save_layer_; } + + CvSetup cv_setup() const { return cv_setup_; } + DlRenderer dl_setup() const { return dl_setup_; } + CvRenderer cv_restore() const { return cv_restore_; } + DlRenderer dl_restore() const { return dl_restore_; } + + const SkPaint render_to(SkCanvas* canvas, // + const TestParameters& testP) const { + SkPaint paint; + cv_setup_(canvas, paint); + testP.render_to(canvas, paint); + cv_restore_(canvas, paint); + return paint; } - // Used by the tests that call drawAtlas to avoid using an SkBlender - // during testing because the CPU renderer appears not to render anything. - // See: https://bugs.chromium.org/p/skia/issues/detail?id=12199 - static void RenderAtlas(CvRenderer& cv_renderer, DlRenderer& dl_renderer) { - TestingDrawAtlas = true; - RenderAll(cv_renderer, dl_renderer); - TestingDrawAtlas = false; + void render_to(DisplayListBuilder& builder, + const TestParameters& testP) const { + dl_setup_(builder); + testP.render_to(builder); + dl_restore_(builder); } - // Used by the tests that call a draw method that does not take a paint - // call. Those tests could use |RenderAll| but there would be a lot of - // wasted test runs that prepare an SkPaint that is never used. - static void RenderNoAttributes( - CvRenderer& cv_renderer, - DlRenderer& dl_renderer, - ToleranceAdjuster& adjuster = DefaultAdjuster, - const BoundsTolerance& tolerance = DefaultTolerance) { - RenderWith([=](SkCanvas*, SkPaint& p) {}, // - [=](DisplayListBuilder& d) {}, // - cv_renderer, dl_renderer, adjuster, tolerance, "Base Test"); - RenderWithTransforms(cv_renderer, dl_renderer, adjuster, tolerance); - RenderWithClips(cv_renderer, dl_renderer, adjuster, tolerance); - RenderWithSaveRestore(cv_renderer, dl_renderer, adjuster, tolerance); + private: + const std::string info_; + const SkColor bg_; + const CvSetup cv_setup_; + const DlRenderer dl_setup_; + const CvRenderer cv_restore_; + const DlRenderer dl_restore_; + const bool has_diff_clip_; + const bool has_mutating_save_layer_; +}; + +class CanvasCompareTester { + public: + static BoundsTolerance DefaultTolerance; + + static void RenderAll(const TestParameters& params, + const BoundsTolerance& tolerance = DefaultTolerance) { + RenderEnvironment env = RenderEnvironment::MakeN32(); + env.init_ref(params.cv_renderer()); + RenderWithTransforms(params, env, tolerance); + RenderWithClips(params, env, tolerance); + RenderWithSaveRestore(params, env, tolerance); + // Only test attributes if the canvas version uses the paint object + if (params.uses_paint()) { + RenderWithAttributes(params, env, tolerance); + } } - static void RenderWithSaveRestore(CvRenderer& cv_renderer, - DlRenderer& dl_renderer, - ToleranceAdjuster& adjuster, + static void RenderWithSaveRestore(const TestParameters& testP, + const RenderEnvironment& env, const BoundsTolerance& tolerance) { - SkRect clip = SkRect::MakeLTRB(0, 0, 10, 10); - SkRect rect = SkRect::MakeLTRB(5, 5, 15, 15); + SkRect clip = SkRect::MakeXYWH(RenderCenterX - 1, RenderCenterY - 1, 2, 2); + SkRect rect = SkRect::MakeXYWH(RenderCenterX, RenderCenterY, 10, 10); SkColor alpha_layer_color = SkColorSetARGB(0x7f, 0x00, 0xff, 0xff); SkColor default_color = SkPaint().getColor(); - CvRenderer cv_restored = [=](SkCanvas* cv, SkPaint& p) { - // Draw more than one primitive to disable peephole optimizations - cv->drawRect(RenderBounds.makeOutset(5, 5), p); - cv_renderer(cv, p); + CvRenderer cv_restore = [=](SkCanvas* cv, const SkPaint& p) { + // Draw another primitive to disable peephole optimizations + cv->drawRect(RenderBounds.makeOffset(500, 500), p); cv->restore(); }; - DlRenderer dl_restored = [=](DisplayListBuilder& b) { - // Draw more than one primitive to disable peephole optimizations - b.drawRect(RenderBounds.makeOutset(5, 5)); - dl_renderer(b); + DlRenderer dl_restore = [=](DisplayListBuilder& b) { + // Draw another primitive to disable peephole optimizations + b.drawRect(RenderBounds.makeOffset(500, 500)); b.restore(); }; - RenderWith( - [=](SkCanvas* cv, SkPaint& p) { - cv->save(); - cv->clipRect(clip, SkClipOp::kIntersect, false); - cv->drawRect(rect, p); - cv->restore(); - }, - [=](DisplayListBuilder& b) { - b.save(); - b.clipRect(clip, SkClipOp::kIntersect, false); - b.drawRect(rect); - b.restore(); - }, - cv_renderer, dl_renderer, adjuster, tolerance, - "With prior save/clip/restore"); - RenderWith( - [=](SkCanvas* cv, SkPaint& p) { // - cv->saveLayer(nullptr, nullptr); - }, - [=](DisplayListBuilder& b) { // - b.saveLayer(nullptr, false); - }, - cv_restored, dl_restored, adjuster, tolerance, - "saveLayer no paint, no bounds"); - RenderWith( - [=](SkCanvas* cv, SkPaint& p) { // - cv->saveLayer(RenderBounds, nullptr); - }, - [=](DisplayListBuilder& b) { // - b.saveLayer(&RenderBounds, false); - }, - cv_restored, dl_restored, adjuster, tolerance, - "saveLayer no paint, with bounds"); - RenderWith( - [=](SkCanvas* cv, SkPaint& p) { - SkPaint save_p; - save_p.setColor(alpha_layer_color); - cv->saveLayer(nullptr, &save_p); - }, - [=](DisplayListBuilder& b) { - b.setColor(alpha_layer_color); - b.saveLayer(nullptr, true); - b.setColor(default_color); - }, - cv_restored, dl_restored, adjuster, tolerance, - "saveLayer with alpha, no bounds"); - RenderWith( - [=](SkCanvas* cv, SkPaint& p) { - SkPaint save_p; - save_p.setColor(alpha_layer_color); - cv->saveLayer(RenderBounds, &save_p); - }, - [=](DisplayListBuilder& b) { - b.setColor(alpha_layer_color); - b.saveLayer(&RenderBounds, true); - b.setColor(default_color); - }, - cv_restored, dl_restored, adjuster, tolerance, - "saveLayer with alpha and bounds"); + SkRect layer_bounds = RenderBounds.makeInset(15, 15); + RenderWith(testP, env, tolerance, + CaseParameters( + "With prior save/clip/restore", + [=](SkCanvas* cv, SkPaint& p) { + cv->save(); + cv->clipRect(clip, SkClipOp::kIntersect, false); + SkPaint p2; + cv->drawRect(rect, p2); + p2.setBlendMode(SkBlendMode::kClear); + cv->drawRect(rect, p2); + cv->restore(); + }, + [=](DisplayListBuilder& b) { + b.save(); + b.clipRect(clip, SkClipOp::kIntersect, false); + b.drawRect(rect); + b.setBlendMode(SkBlendMode::kClear); + b.drawRect(rect); + b.setBlendMode(SkBlendMode::kSrcOver); + b.restore(); + })); + RenderWith(testP, env, tolerance, + CaseParameters( + "saveLayer no paint, no bounds", + [=](SkCanvas* cv, SkPaint& p) { // + cv->saveLayer(nullptr, nullptr); + }, + [=](DisplayListBuilder& b) { // + b.saveLayer(nullptr, false); + }) + .with_restore(cv_restore, dl_restore, false)); + RenderWith(testP, env, tolerance, + CaseParameters( + "saveLayer no paint, with bounds", + [=](SkCanvas* cv, SkPaint& p) { // + cv->saveLayer(layer_bounds, nullptr); + }, + [=](DisplayListBuilder& b) { // + b.saveLayer(&layer_bounds, false); + }) + .with_restore(cv_restore, dl_restore, true)); + RenderWith(testP, env, tolerance, + CaseParameters( + "saveLayer with alpha, no bounds", + [=](SkCanvas* cv, SkPaint& p) { + SkPaint save_p; + save_p.setColor(alpha_layer_color); + cv->saveLayer(nullptr, &save_p); + }, + [=](DisplayListBuilder& b) { + b.setColor(alpha_layer_color); + b.saveLayer(nullptr, true); + b.setColor(default_color); + }) + .with_restore(cv_restore, dl_restore, true)); + RenderWith(testP, env, tolerance, + CaseParameters( + "saveLayer with alpha and bounds", + [=](SkCanvas* cv, SkPaint& p) { + SkPaint save_p; + save_p.setColor(alpha_layer_color); + cv->saveLayer(layer_bounds, &save_p); + }, + [=](DisplayListBuilder& b) { + b.setColor(alpha_layer_color); + b.saveLayer(&layer_bounds, true); + b.setColor(default_color); + }) + .with_restore(cv_restore, dl_restore, true)); { - sk_sp filter = - SkImageFilters::Blur(5.0, 5.0, SkTileMode::kDecal, nullptr, nullptr); - BoundsTolerance blur5Tolerance = tolerance.addBoundsPadding(4, 4); + // clang-format off + constexpr float rotate_alpha_color_matrix[20] = { + 0, 1, 0, 0 , 0, + 0, 0, 1, 0 , 0, + 1, 0, 0, 0 , 0, + 0, 0, 0, 0.5, 0, + }; + // clang-format on + sk_sp filter = + SkColorFilters::Matrix(rotate_alpha_color_matrix); + { + RenderWith(testP, env, tolerance, + CaseParameters( + "saveLayer ColorFilter, no bounds", + [=](SkCanvas* cv, SkPaint& p) { + SkPaint save_p; + save_p.setColorFilter(filter); + cv->saveLayer(nullptr, &save_p); + p.setStrokeWidth(5.0); + }, + [=](DisplayListBuilder& b) { + b.setColorFilter(filter); + b.saveLayer(nullptr, true); + b.setColorFilter(nullptr); + b.setStrokeWidth(5.0); + }) + .with_restore(cv_restore, dl_restore, true)); + } + EXPECT_TRUE(filter->unique()) + << "saveLayer ColorFilter, no bounds Cleanup"; + { + RenderWith(testP, env, tolerance, + CaseParameters( + "saveLayer ColorFilter and bounds", + [=](SkCanvas* cv, SkPaint& p) { + SkPaint save_p; + save_p.setColorFilter(filter); + cv->saveLayer(RenderBounds, &save_p); + p.setStrokeWidth(5.0); + }, + [=](DisplayListBuilder& b) { + b.setColorFilter(filter); + b.saveLayer(&RenderBounds, true); + b.setColorFilter(nullptr); + b.setStrokeWidth(5.0); + }) + .with_restore(cv_restore, dl_restore, true)); + } + EXPECT_TRUE(filter->unique()) + << "saveLayer ColorFilter and bounds Cleanup"; + } + { + sk_sp filter = SkImageFilters::Arithmetic( + 0.1, 0.1, 0.1, 0.25, true, nullptr, nullptr); { - RenderWith( - [=](SkCanvas* cv, SkPaint& p) { - SkPaint save_p; - save_p.setImageFilter(filter); - cv->saveLayer(nullptr, &save_p); - p.setStrokeWidth(5.0); - }, - [=](DisplayListBuilder& b) { - b.setImageFilter(filter); - b.saveLayer(nullptr, true); - b.setImageFilter(nullptr); - b.setStrokeWidth(5.0); - }, - cv_restored, dl_restored, adjuster, blur5Tolerance, - "saveLayer ImageFilter, no bounds"); + RenderWith(testP, env, tolerance, + CaseParameters( + "saveLayer ImageFilter, no bounds", + [=](SkCanvas* cv, SkPaint& p) { + SkPaint save_p; + save_p.setImageFilter(filter); + cv->saveLayer(nullptr, &save_p); + p.setStrokeWidth(5.0); + }, + [=](DisplayListBuilder& b) { + b.setImageFilter(filter); + b.saveLayer(nullptr, true); + b.setImageFilter(nullptr); + b.setStrokeWidth(5.0); + }) + .with_restore(cv_restore, dl_restore, true)); } - ASSERT_TRUE(filter->unique()) + EXPECT_TRUE(filter->unique()) << "saveLayer ImageFilter, no bounds Cleanup"; { - RenderWith( - [=](SkCanvas* cv, SkPaint& p) { - SkPaint save_p; - save_p.setImageFilter(filter); - cv->saveLayer(RenderBounds, &save_p); - p.setStrokeWidth(5.0); - }, - [=](DisplayListBuilder& b) { - b.setImageFilter(filter); - b.saveLayer(&RenderBounds, true); - b.setImageFilter(nullptr); - b.setStrokeWidth(5.0); - }, - cv_restored, dl_restored, adjuster, blur5Tolerance, - "saveLayer ImageFilter and bounds"); + RenderWith(testP, env, tolerance, + CaseParameters( + "saveLayer ImageFilter and bounds", + [=](SkCanvas* cv, SkPaint& p) { + SkPaint save_p; + save_p.setImageFilter(filter); + cv->saveLayer(RenderBounds, &save_p); + p.setStrokeWidth(5.0); + }, + [=](DisplayListBuilder& b) { + b.setImageFilter(filter); + b.saveLayer(&RenderBounds, true); + b.setImageFilter(nullptr); + b.setStrokeWidth(5.0); + }) + .with_restore(cv_restore, dl_restore, true)); } - ASSERT_TRUE(filter->unique()) + EXPECT_TRUE(filter->unique()) << "saveLayer ImageFilter and bounds Cleanup"; } } - static void RenderWithAttributes(CvRenderer& cv_renderer, - DlRenderer& dl_renderer, - ToleranceAdjuster& adjuster, + static void RenderWithAttributes(const TestParameters& testP, + const RenderEnvironment& env, const BoundsTolerance& tolerance) { - RenderWith([=](SkCanvas*, SkPaint& p) {}, // - [=](DisplayListBuilder& d) {}, // - cv_renderer, dl_renderer, adjuster, tolerance, "Base Test"); - - RenderWith([=](SkCanvas*, SkPaint& p) { p.setAntiAlias(true); }, // - [=](DisplayListBuilder& b) { b.setAntiAlias(true); }, // - cv_renderer, dl_renderer, adjuster, tolerance, - "AntiAlias == True"); - RenderWith([=](SkCanvas*, SkPaint& p) { p.setAntiAlias(false); }, // - [=](DisplayListBuilder& b) { b.setAntiAlias(false); }, // - cv_renderer, dl_renderer, adjuster, tolerance, - "AntiAlias == False"); - - RenderWith([=](SkCanvas*, SkPaint& p) { p.setDither(true); }, // - [=](DisplayListBuilder& b) { b.setDither(true); }, // - cv_renderer, dl_renderer, adjuster, tolerance, "Dither == True"); - RenderWith([=](SkCanvas*, SkPaint& p) { p.setDither(false); }, // - [=](DisplayListBuilder& b) { b.setDither(false); }, // - cv_renderer, dl_renderer, adjuster, tolerance, "Dither = False"); - - RenderWith([=](SkCanvas*, SkPaint& p) { p.setColor(SK_ColorBLUE); }, // - [=](DisplayListBuilder& b) { b.setColor(SK_ColorBLUE); }, // - cv_renderer, dl_renderer, adjuster, tolerance, "Color == Blue"); - RenderWith([=](SkCanvas*, SkPaint& p) { p.setColor(SK_ColorGREEN); }, // - [=](DisplayListBuilder& b) { b.setColor(SK_ColorGREEN); }, // - cv_renderer, dl_renderer, adjuster, tolerance, "Color == Green"); - - RenderWithStrokes(cv_renderer, dl_renderer, adjuster, tolerance); + RenderWith(testP, env, tolerance, CaseParameters("Defaults Test")); + + { + // CPU renderer with default line width of 0 does not show antialiasing + // for stroked primitives, so we make a new reference with a non-trivial + // stroke width to demonstrate the differences + RenderEnvironment aa_env = RenderEnvironment::MakeN32(); + // Tweak the bounds tolerance for the displacement of 1/10 of a pixel + const BoundsTolerance aa_tolerance = tolerance.addBoundsPadding(1, 1); + CvSetup cv_aa_setup = [=](SkCanvas* cv, SkPaint& p) { + cv->translate(0.1, 0.1); + p.setStrokeWidth(5.0); + }; + DlRenderer dl_aa_setup = [=](DisplayListBuilder& b) { + b.translate(0.1, 0.1); + b.setStrokeWidth(5.0); + }; + aa_env.init_ref(cv_aa_setup, testP.cv_renderer()); + RenderWith(testP, aa_env, aa_tolerance, + CaseParameters( + "AntiAlias == True", + [=](SkCanvas* cv, SkPaint& p) { + cv_aa_setup(cv, p); + p.setAntiAlias(true); + }, + [=](DisplayListBuilder& b) { + dl_aa_setup(b); + b.setAntiAlias(true); + })); + RenderWith(testP, aa_env, aa_tolerance, + CaseParameters( + "AntiAlias == False", + [=](SkCanvas* cv, SkPaint& p) { + cv_aa_setup(cv, p); + p.setAntiAlias(false); + }, + [=](DisplayListBuilder& b) { + dl_aa_setup(b); + b.setAntiAlias(false); + })); + } + + { + // The CPU renderer does not always dither for solid colors and we + // need to use a non-default color (default is black) on an opaque + // surface, so we use a shader instead of a color. Also, thin stroked + // primitives (mainly drawLine and drawPoints) do not show much + // dithering so we use a non-trivial stroke width as well. + RenderEnvironment dither_env = RenderEnvironment::Make565(); + SkColor dither_bg = SK_ColorBLACK; + CvSetup cv_dither_setup = [=](SkCanvas*, SkPaint& p) { + p.setShader(testImageShader); + p.setAlpha(0xf0); + p.setStrokeWidth(5.0); + }; + DlRenderer dl_dither_setup = [=](DisplayListBuilder& b) { + b.setShader(testImageShader); + b.setColor(SkColor(0xf0000000)); + b.setStrokeWidth(5.0); + }; + dither_env.init_ref(cv_dither_setup, testP.cv_renderer(), dither_bg); + RenderWith(testP, dither_env, tolerance, + CaseParameters( + "Dither == True", + [=](SkCanvas* cv, SkPaint& p) { + cv_dither_setup(cv, p); + p.setDither(true); + }, + [=](DisplayListBuilder& b) { + dl_dither_setup(b); + b.setDither(true); + }) + .with_bg(dither_bg)); + RenderWith(testP, dither_env, tolerance, + CaseParameters( + "Dither = False", + [=](SkCanvas* cv, SkPaint& p) { + cv_dither_setup(cv, p); + p.setDither(false); + }, + [=](DisplayListBuilder& b) { + dl_dither_setup(b); + b.setDither(false); + }) + .with_bg(dither_bg)); + } + EXPECT_TRUE(testImageShader->unique()) << "Dither Cleanup"; + + RenderWith(testP, env, tolerance, + CaseParameters( + "Color == Blue", + [=](SkCanvas*, SkPaint& p) { p.setColor(SK_ColorBLUE); }, + [=](DisplayListBuilder& b) { b.setColor(SK_ColorBLUE); })); + RenderWith(testP, env, tolerance, + CaseParameters( + "Color == Green", + [=](SkCanvas*, SkPaint& p) { p.setColor(SK_ColorGREEN); }, + [=](DisplayListBuilder& b) { b.setColor(SK_ColorGREEN); })); + + RenderWithStrokes(testP, env, tolerance); { // half opaque cyan SkColor blendableColor = SkColorSetARGB(0x7f, 0x00, 0xff, 0xff); SkColor bg = SK_ColorWHITE; - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setBlendMode(SkBlendMode::kSrcIn); - p.setColor(blendableColor); - }, - [=](DisplayListBuilder& b) { - b.setBlendMode(SkBlendMode::kSrcIn); - b.setColor(blendableColor); - }, - cv_renderer, dl_renderer, adjuster, tolerance, "Blend == SrcIn", &bg); - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setBlendMode(SkBlendMode::kDstIn); - p.setColor(blendableColor); - }, - [=](DisplayListBuilder& b) { - b.setBlendMode(SkBlendMode::kDstIn); - b.setColor(blendableColor); - }, - cv_renderer, dl_renderer, adjuster, tolerance, "Blend == DstIn", &bg); + RenderWith(testP, env, tolerance, + CaseParameters( + "Blend == SrcIn", + [=](SkCanvas*, SkPaint& p) { + p.setBlendMode(SkBlendMode::kSrcIn); + p.setColor(blendableColor); + }, + [=](DisplayListBuilder& b) { + b.setBlendMode(SkBlendMode::kSrcIn); + b.setColor(blendableColor); + }) + .with_bg(bg)); + RenderWith(testP, env, tolerance, + CaseParameters( + "Blend == DstIn", + [=](SkCanvas*, SkPaint& p) { + p.setBlendMode(SkBlendMode::kDstIn); + p.setColor(blendableColor); + }, + [=](DisplayListBuilder& b) { + b.setBlendMode(SkBlendMode::kDstIn); + b.setColor(blendableColor); + }) + .with_bg(bg)); } - if (!(TestingDrawAtlas || TestingDrawVertices)) { + if (!(testP.is_draw_atlas() || testP.is_draw_vertices())) { sk_sp blender = SkBlenders::Arithmetic(0.25, 0.25, 0.25, 0.25, false); { - RenderWith([=](SkCanvas*, SkPaint& p) { p.setBlender(blender); }, - [=](DisplayListBuilder& b) { b.setBlender(blender); }, - cv_renderer, dl_renderer, adjuster, tolerance, - "ImageFilter == Blender Arithmetic 0.25-false"); + RenderWith(testP, env, tolerance, + CaseParameters( + "ImageFilter == Blender Arithmetic 0.25-false", + [=](SkCanvas*, SkPaint& p) { p.setBlender(blender); }, + [=](DisplayListBuilder& b) { b.setBlender(blender); })); } - ASSERT_TRUE(blender->unique()) << "Blender Cleanup"; + EXPECT_TRUE(blender->unique()) << "Blender Cleanup"; blender = SkBlenders::Arithmetic(0.25, 0.25, 0.25, 0.25, true); { - RenderWith([=](SkCanvas*, SkPaint& p) { p.setBlender(blender); }, - [=](DisplayListBuilder& b) { b.setBlender(blender); }, - cv_renderer, dl_renderer, adjuster, tolerance, - "ImageFilter == Blender Arithmetic 0.25-true"); + RenderWith(testP, env, tolerance, + CaseParameters( + "ImageFilter == Blender Arithmetic 0.25-true", + [=](SkCanvas*, SkPaint& p) { p.setBlender(blender); }, + [=](DisplayListBuilder& b) { b.setBlender(blender); })); } - ASSERT_TRUE(blender->unique()) << "Blender Cleanup"; + EXPECT_TRUE(blender->unique()) << "Blender Cleanup"; } { + // Being able to see a blur requires some non-default attributes, + // like a non-trivial stroke width and a shader rather than a color + // (for drawPaint) so we create a new environment for these tests. + RenderEnvironment blur_env = RenderEnvironment::MakeN32(); + CvSetup cv_blur_setup = [=](SkCanvas*, SkPaint& p) { + p.setShader(testImageShader); + p.setStrokeWidth(5.0); + }; + DlRenderer dl_blur_setup = [=](DisplayListBuilder& b) { + b.setShader(testImageShader); + b.setStrokeWidth(5.0); + }; + blur_env.init_ref(cv_blur_setup, testP.cv_renderer()); sk_sp filter = SkImageFilters::Blur(5.0, 5.0, SkTileMode::kDecal, nullptr, nullptr); BoundsTolerance blur5Tolerance = tolerance.addBoundsPadding(4, 4); { - RenderWith( - [=](SkCanvas*, SkPaint& p) { - // Provide some non-trivial stroke size to get blurred - p.setStrokeWidth(5.0); - p.setImageFilter(filter); - }, - [=](DisplayListBuilder& b) { - // Provide some non-trivial stroke size to get blurred - b.setStrokeWidth(5.0); - b.setImageFilter(filter); - }, - cv_renderer, dl_renderer, adjuster, blur5Tolerance, - "ImageFilter == Decal Blur 5"); + RenderWith(testP, blur_env, blur5Tolerance, + CaseParameters( + "ImageFilter == Decal Blur 5", + [=](SkCanvas* cv, SkPaint& p) { + cv_blur_setup(cv, p); + p.setImageFilter(filter); + }, + [=](DisplayListBuilder& b) { + dl_blur_setup(b); + b.setImageFilter(filter); + })); } - ASSERT_TRUE(filter->unique()) << "ImageFilter Cleanup"; + EXPECT_TRUE(filter->unique()) << "ImageFilter Cleanup"; filter = SkImageFilters::Blur(5.0, 5.0, SkTileMode::kClamp, nullptr, nullptr); { - RenderWith( - [=](SkCanvas*, SkPaint& p) { - // Provide some non-trivial stroke size to get blurred - p.setStrokeWidth(5.0); - p.setImageFilter(filter); - }, - [=](DisplayListBuilder& b) { - // Provide some non-trivial stroke size to get blurred - b.setStrokeWidth(5.0); - b.setImageFilter(filter); - }, - cv_renderer, dl_renderer, adjuster, blur5Tolerance, - "ImageFilter == Clamp Blur 5"); + RenderWith(testP, blur_env, blur5Tolerance, + CaseParameters( + "ImageFilter == Clamp Blur 5", + [=](SkCanvas* cv, SkPaint& p) { + cv_blur_setup(cv, p); + p.setImageFilter(filter); + }, + [=](DisplayListBuilder& b) { + dl_blur_setup(b); + b.setImageFilter(filter); + })); } - ASSERT_TRUE(filter->unique()) << "ImageFilter Cleanup"; + EXPECT_TRUE(filter->unique()) << "ImageFilter Cleanup"; } { @@ -601,35 +1120,37 @@ class CanvasCompareTester { sk_sp filter = SkColorFilters::Matrix(rotate_color_matrix); { SkColor bg = SK_ColorWHITE; - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setColor(SK_ColorYELLOW); - p.setColorFilter(filter); - }, - [=](DisplayListBuilder& b) { - b.setColor(SK_ColorYELLOW); - b.setColorFilter(filter); - }, - cv_renderer, dl_renderer, adjuster, tolerance, - "ColorFilter == RotateRGB", &bg); + RenderWith(testP, env, tolerance, + CaseParameters( + "ColorFilter == RotateRGB", + [=](SkCanvas*, SkPaint& p) { + p.setColor(SK_ColorYELLOW); + p.setColorFilter(filter); + }, + [=](DisplayListBuilder& b) { + b.setColor(SK_ColorYELLOW); + b.setColorFilter(filter); + }) + .with_bg(bg)); } - ASSERT_TRUE(filter->unique()) << "ColorFilter == RotateRGB Cleanup"; + EXPECT_TRUE(filter->unique()) << "ColorFilter == RotateRGB Cleanup"; filter = SkColorFilters::Matrix(invert_color_matrix); { SkColor bg = SK_ColorWHITE; - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setColor(SK_ColorYELLOW); - p.setColorFilter(filter); - }, - [=](DisplayListBuilder& b) { - b.setColor(SK_ColorYELLOW); - b.setInvertColors(true); - }, - cv_renderer, dl_renderer, adjuster, tolerance, - "ColorFilter == Invert", &bg); + RenderWith(testP, env, tolerance, + CaseParameters( + "ColorFilter == Invert", + [=](SkCanvas*, SkPaint& p) { + p.setColor(SK_ColorYELLOW); + p.setColorFilter(filter); + }, + [=](DisplayListBuilder& b) { + b.setColor(SK_ColorYELLOW); + b.setInvertColors(true); + }) + .with_bg(bg)); } - ASSERT_TRUE(filter->unique()) << "ColorFilter == Invert Cleanup"; + EXPECT_TRUE(filter->unique()) << "ColorFilter == Invert Cleanup"; } { @@ -637,58 +1158,60 @@ class CanvasCompareTester { { // Discrete path effects need a stroke width for drawPointsAsPoints // to do something realistic - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setStrokeWidth(5.0); - // A Discrete(3, 5) effect produces miters that are near - // maximal for a miter limit of 3.0. - p.setStrokeMiter(3.0); - p.setPathEffect(effect); - }, - [=](DisplayListBuilder& b) { - b.setStrokeWidth(5.0); - // A Discrete(3, 5) effect produces miters that are near - // maximal for a miter limit of 3.0. - b.setStrokeMiter(3.0); - b.setPathEffect(effect); - }, - cv_renderer, dl_renderer, adjuster, + // And a Discrete(3, 5) effect produces miters that are near + // maximal for a miter limit of 3.0. + BoundsTolerance discrete_tolerance = tolerance // register the discrete offset so adjusters can compensate .addDiscreteOffset(5) // the miters in the 3-5 discrete effect don't always fill // their conservative bounds, so tolerate a couple of pixels - .addBoundsPadding(2, 2), - "PathEffect == Discrete-3-5"); + .addBoundsPadding(2, 2); + RenderWith(testP, env, discrete_tolerance, + CaseParameters( + "PathEffect == Discrete-3-5", + [=](SkCanvas*, SkPaint& p) { + p.setStrokeWidth(5.0); + p.setStrokeMiter(3.0); + p.setPathEffect(effect); + }, + [=](DisplayListBuilder& b) { + b.setStrokeWidth(5.0); + b.setStrokeMiter(3.0); + b.setPathEffect(effect); + })); } - ASSERT_TRUE(effect->unique()) << "PathEffect == Discrete-3-5 Cleanup"; + EXPECT_TRUE(testP.is_draw_text_blob() || effect->unique()) + << "PathEffect == Discrete-3-5 Cleanup"; effect = SkDiscretePathEffect::Make(2, 3); { - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setStrokeWidth(5.0); - // A Discrete(2, 3) effect produces miters that are near - // maximal for a miter limit of 2.5. - p.setStrokeMiter(2.5); - p.setPathEffect(effect); - }, - [=](DisplayListBuilder& b) { - b.setStrokeWidth(5.0); - // A Discrete(2, 3) effect produces miters that are near - // maximal for a miter limit of 2.5. - b.setStrokeMiter(2.5); - b.setPathEffect(effect); - }, - cv_renderer, dl_renderer, adjuster, + // Discrete path effects need a stroke width for drawPointsAsPoints + // to do something realistic + // And a Discrete(2, 3) effect produces miters that are near + // maximal for a miter limit of 2.5. + BoundsTolerance discrete_tolerance = tolerance // register the discrete offset so adjusters can compensate .addDiscreteOffset(3) // the miters in the 3-5 discrete effect don't always fill // their conservative bounds, so tolerate a couple of pixels - .addBoundsPadding(2, 2), - "PathEffect == Discrete-2-3"); + .addBoundsPadding(2, 2); + RenderWith(testP, env, discrete_tolerance, + CaseParameters( + "PathEffect == Discrete-2-3", + [=](SkCanvas*, SkPaint& p) { + p.setStrokeWidth(5.0); + p.setStrokeMiter(2.5); + p.setPathEffect(effect); + }, + [=](DisplayListBuilder& b) { + b.setStrokeWidth(5.0); + b.setStrokeMiter(2.5); + b.setPathEffect(effect); + })); } - ASSERT_TRUE(effect->unique()) << "PathEffect == Discrete-2-3 Cleanup"; + EXPECT_TRUE(testP.is_draw_text_blob() || effect->unique()) + << "PathEffect == Discrete-2-3 Cleanup"; } { @@ -696,37 +1219,35 @@ class CanvasCompareTester { SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, 5.0); BoundsTolerance blur5Tolerance = tolerance.addBoundsPadding(4, 4); { - RenderWith( - [=](SkCanvas*, SkPaint& p) { - // Provide some non-trivial stroke size to get blurred - p.setStrokeWidth(5.0); - p.setMaskFilter(filter); - }, - [=](DisplayListBuilder& b) { - // Provide some non-trivial stroke size to get blurred - b.setStrokeWidth(5.0); - b.setMaskFilter(filter); - }, - cv_renderer, dl_renderer, adjuster, blur5Tolerance, - "MaskFilter == Blur 5"); + // Stroked primitives need some non-trivial stroke size to be blurred + RenderWith(testP, env, blur5Tolerance, + CaseParameters( + "MaskFilter == Blur 5", + [=](SkCanvas*, SkPaint& p) { + p.setStrokeWidth(5.0); + p.setMaskFilter(filter); + }, + [=](DisplayListBuilder& b) { + b.setStrokeWidth(5.0); + b.setMaskFilter(filter); + })); } - ASSERT_TRUE(filter->unique()) << "MaskFilter == Blur 5 Cleanup"; + EXPECT_TRUE(testP.is_draw_text_blob() || filter->unique()) + << "MaskFilter == Blur 5 Cleanup"; { - RenderWith( - [=](SkCanvas*, SkPaint& p) { - // Provide some non-trivial stroke size to get blurred - p.setStrokeWidth(5.0); - p.setMaskFilter(filter); - }, - [=](DisplayListBuilder& b) { - // Provide some non-trivial stroke size to get blurred - b.setStrokeWidth(5.0); - b.setMaskBlurFilter(kNormal_SkBlurStyle, 5.0); - }, - cv_renderer, dl_renderer, adjuster, blur5Tolerance, - "MaskFilter == Blur(Normal, 5.0)"); + RenderWith(testP, env, blur5Tolerance, + CaseParameters( + "MaskFilter == Blur(Normal, 5.0)", + [=](SkCanvas*, SkPaint& p) { + p.setStrokeWidth(5.0); + p.setMaskFilter(filter); + }, + [=](DisplayListBuilder& b) { + b.setStrokeWidth(5.0); + b.setMaskBlurFilter(kNormal_SkBlurStyle, 5.0); + })); } - ASSERT_TRUE(filter->unique()) + EXPECT_TRUE(testP.is_draw_text_blob() || filter->unique()) << "MaskFilter == Blur(Normal, 5.0) Cleanup"; } @@ -737,7 +1258,7 @@ class CanvasCompareTester { }; SkColor colors[] = { SK_ColorGREEN, - SK_ColorYELLOW, + SkColorSetA(SK_ColorYELLOW, 0x7f), SK_ColorBLUE, }; float stops[] = { @@ -748,241 +1269,260 @@ class CanvasCompareTester { sk_sp shader = SkGradientShader::MakeLinear( end_points, colors, stops, 3, SkTileMode::kMirror, 0, nullptr); { - RenderWith([=](SkCanvas*, SkPaint& p) { p.setShader(shader); }, - [=](DisplayListBuilder& b) { b.setShader(shader); }, - cv_renderer, dl_renderer, adjuster, tolerance, - "LinearGradient GYB"); + RenderWith(testP, env, tolerance, + CaseParameters( + "LinearGradient GYB", + [=](SkCanvas*, SkPaint& p) { p.setShader(shader); }, + [=](DisplayListBuilder& b) { b.setShader(shader); })); } - ASSERT_TRUE(shader->unique()) << "LinearGradient GYB Cleanup"; + EXPECT_TRUE(shader->unique()) << "LinearGradient GYB Cleanup"; } } - static void RenderWithStrokes(CvRenderer& cv_renderer, - DlRenderer& dl_renderer, - ToleranceAdjuster& adjuster, + static void RenderWithStrokes(const TestParameters& testP, + const RenderEnvironment env, const BoundsTolerance& tolerance_in) { // The test cases were generated with geometry that will try to fill // out the various miter limits used for testing, but they can be off // by a couple of pixels so we will relax bounds testing for strokes by // a couple of pixels. BoundsTolerance tolerance = tolerance_in.addBoundsPadding(2, 2); - RenderWith( // - [=](SkCanvas*, SkPaint& p) { p.setStyle(SkPaint::kFill_Style); }, - [=](DisplayListBuilder& b) { b.setStyle(SkPaint::kFill_Style); }, - cv_renderer, dl_renderer, adjuster, tolerance, "Fill"); - RenderWith( - [=](SkCanvas*, SkPaint& p) { p.setStyle(SkPaint::kStroke_Style); }, - [=](DisplayListBuilder& b) { b.setStyle(SkPaint::kStroke_Style); }, - cv_renderer, dl_renderer, adjuster, tolerance, "Stroke + defaults"); - - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setStyle(SkPaint::kFill_Style); - p.setStrokeWidth(10.0); - }, - [=](DisplayListBuilder& b) { - b.setStyle(SkPaint::kFill_Style); - b.setStrokeWidth(10.0); - }, - cv_renderer, dl_renderer, adjuster, tolerance, - "Fill + unnecessary StrokeWidth 10"); - - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setStyle(SkPaint::kStroke_Style); - p.setStrokeWidth(10.0); - }, - [=](DisplayListBuilder& b) { - b.setStyle(SkPaint::kStroke_Style); - b.setStrokeWidth(10.0); - }, - cv_renderer, dl_renderer, adjuster, tolerance, "Stroke Width 10"); - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setStyle(SkPaint::kStroke_Style); - p.setStrokeWidth(5.0); - }, - [=](DisplayListBuilder& b) { - b.setStyle(SkPaint::kStroke_Style); - b.setStrokeWidth(5.0); - }, - cv_renderer, dl_renderer, adjuster, tolerance, "Stroke Width 5"); - - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setStyle(SkPaint::kStroke_Style); - p.setStrokeWidth(5.0); - p.setStrokeCap(SkPaint::kButt_Cap); - }, - [=](DisplayListBuilder& b) { - b.setStyle(SkPaint::kStroke_Style); - b.setStrokeWidth(5.0); - b.setStrokeCap(SkPaint::kButt_Cap); - }, - cv_renderer, dl_renderer, adjuster, tolerance, - "Stroke Width 5, Butt Cap"); - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setStyle(SkPaint::kStroke_Style); - p.setStrokeWidth(5.0); - p.setStrokeCap(SkPaint::kRound_Cap); - }, - [=](DisplayListBuilder& b) { - b.setStyle(SkPaint::kStroke_Style); - b.setStrokeWidth(5.0); - b.setStrokeCap(SkPaint::kRound_Cap); - }, - cv_renderer, dl_renderer, adjuster, tolerance, - "Stroke Width 5, Round Cap"); - - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setStyle(SkPaint::kStroke_Style); - p.setStrokeWidth(5.0); - p.setStrokeJoin(SkPaint::kBevel_Join); - }, - [=](DisplayListBuilder& b) { - b.setStyle(SkPaint::kStroke_Style); - b.setStrokeWidth(5.0); - b.setStrokeJoin(SkPaint::kBevel_Join); - }, - cv_renderer, dl_renderer, adjuster, tolerance, - "Stroke Width 5, Bevel Join"); - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setStyle(SkPaint::kStroke_Style); - p.setStrokeWidth(5.0); - p.setStrokeJoin(SkPaint::kRound_Join); - }, - [=](DisplayListBuilder& b) { - b.setStyle(SkPaint::kStroke_Style); - b.setStrokeWidth(5.0); - b.setStrokeJoin(SkPaint::kRound_Join); - }, - cv_renderer, dl_renderer, adjuster, tolerance, - "Stroke Width 5, Round Join"); - - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setStyle(SkPaint::kStroke_Style); - p.setStrokeWidth(5.0); - p.setStrokeMiter(10.0); - p.setStrokeJoin(SkPaint::kMiter_Join); - // AA helps fill in the peaks of the really thin miters better - // for bounds accuracy testing - p.setAntiAlias(true); - }, - [=](DisplayListBuilder& b) { - b.setStyle(SkPaint::kStroke_Style); - b.setStrokeWidth(5.0); - b.setStrokeMiter(10.0); - b.setStrokeJoin(SkPaint::kMiter_Join); - // AA helps fill in the peaks of the really thin miters better - // for bounds accuracy testing - b.setAntiAlias(true); - }, - cv_renderer, dl_renderer, adjuster, tolerance, - "Stroke Width 5, Miter 10"); - - RenderWith( - [=](SkCanvas*, SkPaint& p) { - p.setStyle(SkPaint::kStroke_Style); - p.setStrokeWidth(5.0); - p.setStrokeMiter(0.0); - p.setStrokeJoin(SkPaint::kMiter_Join); - }, - [=](DisplayListBuilder& b) { - b.setStyle(SkPaint::kStroke_Style); - b.setStrokeWidth(5.0); - b.setStrokeMiter(0.0); - b.setStrokeJoin(SkPaint::kMiter_Join); - }, - cv_renderer, dl_renderer, adjuster, tolerance, - "Stroke Width 5, Miter 0"); + RenderWith(testP, env, tolerance, + CaseParameters( + "Fill", + [=](SkCanvas*, SkPaint& p) { // + p.setStyle(SkPaint::kFill_Style); + }, + [=](DisplayListBuilder& b) { // + b.setStyle(SkPaint::kFill_Style); + })); + RenderWith(testP, env, tolerance, + CaseParameters( + "Stroke + defaults", + [=](SkCanvas*, SkPaint& p) { // + p.setStyle(SkPaint::kStroke_Style); + }, + [=](DisplayListBuilder& b) { // + b.setStyle(SkPaint::kStroke_Style); + })); + + RenderWith(testP, env, tolerance, + CaseParameters( + "Fill + unnecessary StrokeWidth 10", + [=](SkCanvas*, SkPaint& p) { + p.setStyle(SkPaint::kFill_Style); + p.setStrokeWidth(10.0); + }, + [=](DisplayListBuilder& b) { + b.setStyle(SkPaint::kFill_Style); + b.setStrokeWidth(10.0); + })); + + RenderEnvironment stroke_base_env = RenderEnvironment::MakeN32(); + CvSetup cv_stroke_setup = [=](SkCanvas*, SkPaint& p) { + p.setStyle(SkPaint::kStroke_Style); + p.setStrokeWidth(5.0); + }; + stroke_base_env.init_ref(cv_stroke_setup, testP.cv_renderer()); + + RenderWith(testP, stroke_base_env, tolerance, + CaseParameters( + "Stroke Width 10", + [=](SkCanvas*, SkPaint& p) { + p.setStyle(SkPaint::kStroke_Style); + p.setStrokeWidth(10.0); + }, + [=](DisplayListBuilder& b) { + b.setStyle(SkPaint::kStroke_Style); + b.setStrokeWidth(10.0); + })); + RenderWith(testP, stroke_base_env, tolerance, + CaseParameters( + "Stroke Width 5", + [=](SkCanvas*, SkPaint& p) { + p.setStyle(SkPaint::kStroke_Style); + p.setStrokeWidth(5.0); + }, + [=](DisplayListBuilder& b) { + b.setStyle(SkPaint::kStroke_Style); + b.setStrokeWidth(5.0); + })); + + RenderWith(testP, stroke_base_env, tolerance, + CaseParameters( + "Stroke Width 5, Square Cap", + [=](SkCanvas*, SkPaint& p) { + p.setStyle(SkPaint::kStroke_Style); + p.setStrokeWidth(5.0); + p.setStrokeCap(SkPaint::kSquare_Cap); + }, + [=](DisplayListBuilder& b) { + b.setStyle(SkPaint::kStroke_Style); + b.setStrokeWidth(5.0); + b.setStrokeCap(SkPaint::kSquare_Cap); + })); + RenderWith(testP, stroke_base_env, tolerance, + CaseParameters( + "Stroke Width 5, Round Cap", + [=](SkCanvas*, SkPaint& p) { + p.setStyle(SkPaint::kStroke_Style); + p.setStrokeWidth(5.0); + p.setStrokeCap(SkPaint::kRound_Cap); + }, + [=](DisplayListBuilder& b) { + b.setStyle(SkPaint::kStroke_Style); + b.setStrokeWidth(5.0); + b.setStrokeCap(SkPaint::kRound_Cap); + })); + + RenderWith(testP, stroke_base_env, tolerance, + CaseParameters( + "Stroke Width 5, Bevel Join", + [=](SkCanvas*, SkPaint& p) { + p.setStyle(SkPaint::kStroke_Style); + p.setStrokeWidth(5.0); + p.setStrokeJoin(SkPaint::kBevel_Join); + }, + [=](DisplayListBuilder& b) { + b.setStyle(SkPaint::kStroke_Style); + b.setStrokeWidth(5.0); + b.setStrokeJoin(SkPaint::kBevel_Join); + })); + RenderWith(testP, stroke_base_env, tolerance, + CaseParameters( + "Stroke Width 5, Round Join", + [=](SkCanvas*, SkPaint& p) { + p.setStyle(SkPaint::kStroke_Style); + p.setStrokeWidth(5.0); + p.setStrokeJoin(SkPaint::kRound_Join); + }, + [=](DisplayListBuilder& b) { + b.setStyle(SkPaint::kStroke_Style); + b.setStrokeWidth(5.0); + b.setStrokeJoin(SkPaint::kRound_Join); + })); + + RenderWith(testP, stroke_base_env, tolerance, + CaseParameters( + "Stroke Width 5, Miter 10", + [=](SkCanvas*, SkPaint& p) { + p.setStyle(SkPaint::kStroke_Style); + p.setStrokeWidth(5.0); + p.setStrokeMiter(10.0); + p.setStrokeJoin(SkPaint::kMiter_Join); + }, + [=](DisplayListBuilder& b) { + b.setStyle(SkPaint::kStroke_Style); + b.setStrokeWidth(5.0); + b.setStrokeMiter(10.0); + b.setStrokeJoin(SkPaint::kMiter_Join); + })); + + RenderWith(testP, stroke_base_env, tolerance, + CaseParameters( + "Stroke Width 5, Miter 0", + [=](SkCanvas*, SkPaint& p) { + p.setStyle(SkPaint::kStroke_Style); + p.setStrokeWidth(5.0); + p.setStrokeMiter(0.0); + p.setStrokeJoin(SkPaint::kMiter_Join); + }, + [=](DisplayListBuilder& b) { + b.setStyle(SkPaint::kStroke_Style); + b.setStrokeWidth(5.0); + b.setStrokeMiter(0.0); + b.setStrokeJoin(SkPaint::kMiter_Join); + })); { const SkScalar TestDashes1[] = {29.0, 2.0}; const SkScalar TestDashes2[] = {17.0, 1.5}; sk_sp effect = SkDashPathEffect::Make(TestDashes1, 2, 0.0f); { - RenderWith( - [=](SkCanvas*, SkPaint& p) { - // Need stroke style to see dashing properly - p.setStyle(SkPaint::kStroke_Style); - // Provide some non-trivial stroke size to get dashed - p.setStrokeWidth(5.0); - p.setPathEffect(effect); - }, - [=](DisplayListBuilder& b) { - // Need stroke style to see dashing properly - b.setStyle(SkPaint::kStroke_Style); - // Provide some non-trivial stroke size to get dashed - b.setStrokeWidth(5.0); - b.setPathEffect(effect); - }, - cv_renderer, dl_renderer, adjuster, tolerance, - "PathEffect == Dash-29-2"); + RenderWith(testP, stroke_base_env, tolerance, + CaseParameters( + "PathEffect == Dash-29-2", + [=](SkCanvas*, SkPaint& p) { + // Need stroke style to see dashing properly + p.setStyle(SkPaint::kStroke_Style); + // Provide some non-trivial stroke size to get dashed + p.setStrokeWidth(5.0); + p.setPathEffect(effect); + }, + [=](DisplayListBuilder& b) { + // Need stroke style to see dashing properly + b.setStyle(SkPaint::kStroke_Style); + // Provide some non-trivial stroke size to get dashed + b.setStrokeWidth(5.0); + b.setPathEffect(effect); + })); } - ASSERT_TRUE(effect->unique()) << "PathEffect == Dash-29-2 Cleanup"; + EXPECT_TRUE(testP.is_draw_text_blob() || effect->unique()) + << "PathEffect == Dash-29-2 Cleanup"; effect = SkDashPathEffect::Make(TestDashes2, 2, 0.0f); { - RenderWith( - [=](SkCanvas*, SkPaint& p) { - // Need stroke style to see dashing properly - p.setStyle(SkPaint::kStroke_Style); - // Provide some non-trivial stroke size to get dashed - p.setStrokeWidth(5.0); - p.setPathEffect(effect); - }, - [=](DisplayListBuilder& b) { - // Need stroke style to see dashing properly - b.setStyle(SkPaint::kStroke_Style); - // Provide some non-trivial stroke size to get dashed - b.setStrokeWidth(5.0); - b.setPathEffect(effect); - }, - cv_renderer, dl_renderer, adjuster, tolerance, - "PathEffect == Dash-17-1.5"); + RenderWith(testP, stroke_base_env, tolerance, + CaseParameters( + "PathEffect == Dash-17-1.5", + [=](SkCanvas*, SkPaint& p) { + // Need stroke style to see dashing properly + p.setStyle(SkPaint::kStroke_Style); + // Provide some non-trivial stroke size to get dashed + p.setStrokeWidth(5.0); + p.setPathEffect(effect); + }, + [=](DisplayListBuilder& b) { + // Need stroke style to see dashing properly + b.setStyle(SkPaint::kStroke_Style); + // Provide some non-trivial stroke size to get dashed + b.setStrokeWidth(5.0); + b.setPathEffect(effect); + })); } - ASSERT_TRUE(effect->unique()) << "PathEffect == Dash-17-1.5 Cleanup"; + EXPECT_TRUE(testP.is_draw_text_blob() || effect->unique()) + << "PathEffect == Dash-17-1.5 Cleanup"; } } - static void RenderWithTransforms(CvRenderer& cv_renderer, - DlRenderer& dl_renderer, - ToleranceAdjuster& adjuster, + static void RenderWithTransforms(const TestParameters& testP, + const RenderEnvironment& env, const BoundsTolerance& tolerance) { - // If there is bounds padding for some conservative bounds overestimate - // then that padding will be even more pronounced in rotated or skewed - // coordinate systems so we scale the padding by about 5% to compensate. - BoundsTolerance skewed_tolerance = tolerance.addScale(1.05, 1.05); - RenderWith([=](SkCanvas* c, SkPaint&) { c->translate(5, 10); }, // - [=](DisplayListBuilder& b) { b.translate(5, 10); }, // - cv_renderer, dl_renderer, adjuster, tolerance, - "Translate 5, 10"); - RenderWith([=](SkCanvas* c, SkPaint&) { c->scale(1.05, 1.05); }, // - [=](DisplayListBuilder& b) { b.scale(1.05, 1.05); }, // - cv_renderer, dl_renderer, adjuster, tolerance, // - "Scale +5%"); - RenderWith([=](SkCanvas* c, SkPaint&) { c->rotate(5); }, // - [=](DisplayListBuilder& b) { b.rotate(5); }, // - cv_renderer, dl_renderer, adjuster, skewed_tolerance, - "Rotate 5 degrees"); - RenderWith([=](SkCanvas* c, SkPaint&) { c->skew(0.05, 0.05); }, // - [=](DisplayListBuilder& b) { b.skew(0.05, 0.05); }, // - cv_renderer, dl_renderer, adjuster, skewed_tolerance, // - "Skew 5%"); + // If the rendering method does not fill the corners of the original + // bounds, then the estimate under rotation or skewing will be off + // so we scale the padding by about 5% to compensate. + BoundsTolerance skewed_tolerance = tolerance.mulScale(1.05, 1.05); + RenderWith(testP, env, tolerance, + CaseParameters( + "Translate 5, 10", // + [=](SkCanvas* c, SkPaint&) { c->translate(5, 10); }, + [=](DisplayListBuilder& b) { b.translate(5, 10); })); + RenderWith(testP, env, tolerance, + CaseParameters( + "Scale +5%", // + [=](SkCanvas* c, SkPaint&) { c->scale(1.05, 1.05); }, + [=](DisplayListBuilder& b) { b.scale(1.05, 1.05); })); + RenderWith(testP, env, skewed_tolerance, + CaseParameters( + "Rotate 5 degrees", // + [=](SkCanvas* c, SkPaint&) { c->rotate(5); }, + [=](DisplayListBuilder& b) { b.rotate(5); })); + RenderWith(testP, env, skewed_tolerance, + CaseParameters( + "Skew 5%", // + [=](SkCanvas* c, SkPaint&) { c->skew(0.05, 0.05); }, + [=](DisplayListBuilder& b) { b.skew(0.05, 0.05); })); { SkMatrix tx = SkMatrix::MakeAll(1.10, 0.10, 5, // 0.05, 1.05, 10, // 0, 0, 1); - RenderWith([=](SkCanvas* c, SkPaint&) { c->concat(tx); }, // - [=](DisplayListBuilder& b) { - b.transform2DAffine(tx[0], tx[1], tx[2], // - tx[3], tx[4], tx[5]); - }, // - cv_renderer, dl_renderer, adjuster, skewed_tolerance, - "Transform 2D Affine"); + RenderWith(testP, env, skewed_tolerance, + CaseParameters( + "Transform 2D Affine", + [=](SkCanvas* c, SkPaint&) { c->concat(tx); }, + [=](DisplayListBuilder& b) { + b.transform2DAffine(tx[0], tx[1], tx[2], // + tx[3], tx[4], tx[5]); + })); } { SkM44 m44 = SkM44(1, 0, 0, RenderCenterX, // @@ -992,207 +1532,222 @@ class CanvasCompareTester { m44.preConcat(SkM44::Rotate({1, 0, 0}, M_PI / 60)); // 3 degrees around X m44.preConcat(SkM44::Rotate({0, 1, 0}, M_PI / 45)); // 4 degrees around Y m44.preTranslate(-RenderCenterX, -RenderCenterY); - RenderWith([=](SkCanvas* c, SkPaint&) { c->concat(m44); }, // - [=](DisplayListBuilder& b) { - b.transformFullPerspective( - m44.rc(0, 0), m44.rc(0, 1), m44.rc(0, 2), m44.rc(0, 3), - m44.rc(1, 0), m44.rc(1, 1), m44.rc(1, 2), m44.rc(1, 3), - m44.rc(2, 0), m44.rc(2, 1), m44.rc(2, 2), m44.rc(2, 3), - m44.rc(3, 0), m44.rc(3, 1), m44.rc(3, 2), m44.rc(3, 3)); - }, // - cv_renderer, dl_renderer, adjuster, skewed_tolerance, - "Transform Full Perspective"); + RenderWith( + testP, env, skewed_tolerance, + CaseParameters( + "Transform Full Perspective", + [=](SkCanvas* c, SkPaint&) { c->concat(m44); }, // + [=](DisplayListBuilder& b) { + b.transformFullPerspective( + m44.rc(0, 0), m44.rc(0, 1), m44.rc(0, 2), m44.rc(0, 3), + m44.rc(1, 0), m44.rc(1, 1), m44.rc(1, 2), m44.rc(1, 3), + m44.rc(2, 0), m44.rc(2, 1), m44.rc(2, 2), m44.rc(2, 3), + m44.rc(3, 0), m44.rc(3, 1), m44.rc(3, 2), m44.rc(3, 3)); + })); } } - static void RenderWithClips(CvRenderer& cv_renderer, - DlRenderer& dl_renderer, - ToleranceAdjuster& diff_adjuster, + static void RenderWithClips(const TestParameters& testP, + const RenderEnvironment& env, const BoundsTolerance& diff_tolerance) { SkRect r_clip = RenderBounds.makeInset(15.5, 15.5); - // For kIntersect clips we can be really strict on tolerance - ToleranceAdjuster& intersect_adjuster = DefaultAdjuster; - BoundsTolerance& intersect_tolerance = DefaultTolerance; - RenderWith( - [=](SkCanvas* c, SkPaint&) { - c->clipRect(r_clip, SkClipOp::kIntersect, false); - }, - [=](DisplayListBuilder& b) { - b.clipRect(r_clip, SkClipOp::kIntersect, false); - }, - cv_renderer, dl_renderer, intersect_adjuster, intersect_tolerance, - "Hard ClipRect inset by 15.5"); - RenderWith( - [=](SkCanvas* c, SkPaint&) { - c->clipRect(r_clip, SkClipOp::kIntersect, true); - }, - [=](DisplayListBuilder& b) { - b.clipRect(r_clip, SkClipOp::kIntersect, true); - }, - cv_renderer, dl_renderer, intersect_adjuster, intersect_tolerance, - "AntiAlias ClipRect inset by 15.5"); - RenderWith( - [=](SkCanvas* c, SkPaint&) { - c->clipRect(r_clip, SkClipOp::kDifference, false); - }, - [=](DisplayListBuilder& b) { - b.clipRect(r_clip, SkClipOp::kDifference, false); - }, - cv_renderer, dl_renderer, diff_adjuster, diff_tolerance, - "Hard ClipRect Diff, inset by 15.5"); + BoundsTolerance intersect_tolerance = diff_tolerance.clip(r_clip); + RenderWith(testP, env, intersect_tolerance, + CaseParameters( + "Hard ClipRect inset by 15.5", + [=](SkCanvas* c, SkPaint&) { + c->clipRect(r_clip, SkClipOp::kIntersect, false); + }, + [=](DisplayListBuilder& b) { + b.clipRect(r_clip, SkClipOp::kIntersect, false); + })); + RenderWith(testP, env, intersect_tolerance, + CaseParameters( + "AntiAlias ClipRect inset by 15.5", + [=](SkCanvas* c, SkPaint&) { + c->clipRect(r_clip, SkClipOp::kIntersect, true); + }, + [=](DisplayListBuilder& b) { + b.clipRect(r_clip, SkClipOp::kIntersect, true); + })); + RenderWith(testP, env, diff_tolerance, + CaseParameters( + "Hard ClipRect Diff, inset by 15.5", + [=](SkCanvas* c, SkPaint&) { + c->clipRect(r_clip, SkClipOp::kDifference, false); + }, + [=](DisplayListBuilder& b) { + b.clipRect(r_clip, SkClipOp::kDifference, false); + }) + .with_diff_clip()); SkRRect rr_clip = SkRRect::MakeRectXY(r_clip, 1.8, 2.7); - RenderWith( - [=](SkCanvas* c, SkPaint&) { - c->clipRRect(rr_clip, SkClipOp::kIntersect, false); - }, - [=](DisplayListBuilder& b) { - b.clipRRect(rr_clip, SkClipOp::kIntersect, false); - }, - cv_renderer, dl_renderer, intersect_adjuster, intersect_tolerance, - "Hard ClipRRect inset by 15.5"); - RenderWith( - [=](SkCanvas* c, SkPaint&) { - c->clipRRect(rr_clip, SkClipOp::kIntersect, true); - }, - [=](DisplayListBuilder& b) { - b.clipRRect(rr_clip, SkClipOp::kIntersect, true); - }, - cv_renderer, dl_renderer, intersect_adjuster, intersect_tolerance, - "AntiAlias ClipRRect inset by 15.5"); - RenderWith( - [=](SkCanvas* c, SkPaint&) { - c->clipRRect(rr_clip, SkClipOp::kDifference, false); - }, - [=](DisplayListBuilder& b) { - b.clipRRect(rr_clip, SkClipOp::kDifference, false); - }, - cv_renderer, dl_renderer, diff_adjuster, diff_tolerance, - "Hard ClipRRect Diff, inset by 15.5"); + RenderWith(testP, env, intersect_tolerance, + CaseParameters( + "Hard ClipRRect inset by 15.5", + [=](SkCanvas* c, SkPaint&) { + c->clipRRect(rr_clip, SkClipOp::kIntersect, false); + }, + [=](DisplayListBuilder& b) { + b.clipRRect(rr_clip, SkClipOp::kIntersect, false); + })); + RenderWith(testP, env, intersect_tolerance, + CaseParameters( + "AntiAlias ClipRRect inset by 15.5", + [=](SkCanvas* c, SkPaint&) { + c->clipRRect(rr_clip, SkClipOp::kIntersect, true); + }, + [=](DisplayListBuilder& b) { + b.clipRRect(rr_clip, SkClipOp::kIntersect, true); + })); + RenderWith(testP, env, diff_tolerance, + CaseParameters( + "Hard ClipRRect Diff, inset by 15.5", + [=](SkCanvas* c, SkPaint&) { + c->clipRRect(rr_clip, SkClipOp::kDifference, false); + }, + [=](DisplayListBuilder& b) { + b.clipRRect(rr_clip, SkClipOp::kDifference, false); + }) + .with_diff_clip()); SkPath path_clip = SkPath(); path_clip.setFillType(SkPathFillType::kEvenOdd); path_clip.addRect(r_clip); path_clip.addCircle(RenderCenterX, RenderCenterY, 1.0); - RenderWith( - [=](SkCanvas* c, SkPaint&) { - c->clipPath(path_clip, SkClipOp::kIntersect, false); - }, - [=](DisplayListBuilder& b) { - b.clipPath(path_clip, SkClipOp::kIntersect, false); - }, - cv_renderer, dl_renderer, intersect_adjuster, intersect_tolerance, - "Hard ClipPath inset by 15.5"); - RenderWith( - [=](SkCanvas* c, SkPaint&) { - c->clipPath(path_clip, SkClipOp::kIntersect, true); - }, - [=](DisplayListBuilder& b) { - b.clipPath(path_clip, SkClipOp::kIntersect, true); - }, - cv_renderer, dl_renderer, intersect_adjuster, intersect_tolerance, - "AntiAlias ClipPath inset by 15.5"); - RenderWith( - [=](SkCanvas* c, SkPaint&) { - c->clipPath(path_clip, SkClipOp::kDifference, false); - }, - [=](DisplayListBuilder& b) { - b.clipPath(path_clip, SkClipOp::kDifference, false); - }, - cv_renderer, dl_renderer, diff_adjuster, diff_tolerance, - "Hard ClipPath Diff, inset by 15.5"); + RenderWith(testP, env, intersect_tolerance, + CaseParameters( + "Hard ClipPath inset by 15.5", + [=](SkCanvas* c, SkPaint&) { + c->clipPath(path_clip, SkClipOp::kIntersect, false); + }, + [=](DisplayListBuilder& b) { + b.clipPath(path_clip, SkClipOp::kIntersect, false); + })); + RenderWith(testP, env, intersect_tolerance, + CaseParameters( + "AntiAlias ClipPath inset by 15.5", + [=](SkCanvas* c, SkPaint&) { + c->clipPath(path_clip, SkClipOp::kIntersect, true); + }, + [=](DisplayListBuilder& b) { + b.clipPath(path_clip, SkClipOp::kIntersect, true); + })); + RenderWith(testP, env, diff_tolerance, + CaseParameters( + "Hard ClipPath Diff, inset by 15.5", + [=](SkCanvas* c, SkPaint&) { + c->clipPath(path_clip, SkClipOp::kDifference, false); + }, + [=](DisplayListBuilder& b) { + b.clipPath(path_clip, SkClipOp::kDifference, false); + }) + .with_diff_clip()); } - static sk_sp getSkPicture(CvRenderer& cv_setup, - CvRenderer& cv_render) { + static sk_sp getSkPicture(const TestParameters& testP, + const CaseParameters& caseP) { SkPictureRecorder recorder; SkRTreeFactory rtree_factory; SkCanvas* cv = recorder.beginRecording(TestBounds, &rtree_factory); - SkPaint p; - cv_setup(cv, p); - cv_render(cv, p); + caseP.render_to(cv, testP); return recorder.finishRecordingAsPicture(); } - static void RenderWith(CvRenderer& cv_setup, - DlRenderer& dl_setup, - CvRenderer& cv_render, - DlRenderer& dl_render, - ToleranceAdjuster& adjuster, + static void RenderWith(const TestParameters& testP, + const RenderEnvironment& env, const BoundsTolerance& tolerance_in, - const std::string info, - const SkColor* bg = nullptr) { - // surface1 is direct rendering via SkCanvas to SkSurface + const CaseParameters& caseP) { + // sk_surface is a direct rendering via SkCanvas to SkSurface // DisplayList mechanisms are not involved in this operation - sk_sp ref_surface = makeSurface(bg); - SkPaint paint1; - cv_setup(ref_surface->getCanvas(), paint1); - const BoundsTolerance tolerance = adjuster( - tolerance_in, paint1, ref_surface->getCanvas()->getTotalMatrix()); - cv_render(ref_surface->getCanvas(), paint1); - sk_sp ref_picture = getSkPicture(cv_setup, cv_render); - SkRect ref_bounds = ref_picture->cullRect(); - SkPixmap ref_pixels; - ASSERT_TRUE(ref_surface->peekPixels(&ref_pixels)) << info; - ASSERT_EQ(ref_pixels.width(), TestWidth) << info; - ASSERT_EQ(ref_pixels.height(), TestHeight) << info; - ASSERT_EQ(ref_pixels.info().bytesPerPixel(), 4) << info; - checkPixels(&ref_pixels, ref_bounds, info + " (Skia reference)", bg); + const std::string info = caseP.info(); + const SkColor bg = caseP.bg(); + RenderSurface sk_surface = env.MakeSurface(bg); + SkCanvas* sk_canvas = sk_surface.canvas(); + SkPaint sk_paint; + caseP.cv_setup()(sk_canvas, sk_paint); + SkMatrix sk_matrix = sk_canvas->getTotalMatrix(); + SkIRect sk_clip = sk_canvas->getDeviceClipBounds(); + const BoundsTolerance tolerance = + testP.adjust(tolerance_in, sk_paint, sk_canvas->getTotalMatrix()); + testP.render_to(sk_canvas, sk_paint); + caseP.cv_restore()(sk_canvas, sk_paint); + const sk_sp sk_picture = getSkPicture(testP, caseP); + SkRect sk_bounds = sk_picture->cullRect(); + const SkPixmap* sk_pixels = sk_surface.pixmap(); + ASSERT_EQ(sk_pixels->width(), TestWidth) << info; + ASSERT_EQ(sk_pixels->height(), TestHeight) << info; + ASSERT_EQ(sk_pixels->info().bytesPerPixel(), 4) << info; + checkPixels(sk_pixels, sk_bounds, info + " (Skia reference)", bg); + + if (testP.should_match(env, sk_paint, sk_matrix, sk_clip, + caseP.has_diff_clip(), + caseP.has_mutating_save_layer())) { + quickCompareToReference(env.ref_pixmap(), sk_pixels, true, + info + " (attribute has no effect)"); + } else { + quickCompareToReference(env.ref_pixmap(), sk_pixels, false, + info + " (attribute affects rendering)"); + } { // This sequence plays the provided equivalently constructed // DisplayList onto the SkCanvas of the surface // DisplayList => direct rendering - sk_sp test_surface = makeSurface(bg); + RenderSurface dl_surface = env.MakeSurface(bg); DisplayListBuilder builder(TestBounds); - dl_setup(builder); - dl_render(builder); + caseP.render_to(builder, testP); sk_sp display_list = builder.Build(); SkRect dl_bounds = display_list->bounds(); - if (!ref_bounds.roundOut().contains(dl_bounds)) { + if (!sk_bounds.roundOut().contains(dl_bounds)) { FML_LOG(ERROR) << "For " << info; - FML_LOG(ERROR) << "ref: " // - << ref_bounds.fLeft << ", " << ref_bounds.fTop << " => " - << ref_bounds.fRight << ", " << ref_bounds.fBottom; + FML_LOG(ERROR) << "sk ref: " // + << sk_bounds.fLeft << ", " << sk_bounds.fTop << " => " + << sk_bounds.fRight << ", " << sk_bounds.fBottom; FML_LOG(ERROR) << "dl: " // << dl_bounds.fLeft << ", " << dl_bounds.fTop << " => " << dl_bounds.fRight << ", " << dl_bounds.fBottom; - if (!dl_bounds.contains(ref_bounds)) { + if (!dl_bounds.contains(sk_bounds)) { FML_LOG(ERROR) << "DisplayList bounds are too small!"; } - if (!ref_bounds.roundOut().contains(dl_bounds.roundOut())) { + if (!sk_bounds.roundOut().contains(dl_bounds.roundOut())) { FML_LOG(ERROR) << "###### DisplayList bounds larger than reference!"; } } - // This sometimes triggers, but when it triggers and I examine - // the ref_bounds, they are always unnecessarily large and - // since the pixel OOB tests in the compare method do not - // trigger, we will trust the DL bounds. + // This EXPECT sometimes triggers, but when it triggers and I examine + // the ref_bounds, they are always unnecessarily large and since the + // pixel OOB tests in the compare method do not trigger, we will trust + // the DL bounds. // EXPECT_TRUE(dl_bounds.contains(ref_bounds)) << info; - EXPECT_EQ(display_list->op_count(), ref_picture->approximateOpCount()) - << info; + // When we are drawing a DisplayList, the display_list built above + // will contain just a single drawDisplayList call plus the case + // attribute. The sk_picture will, however, contain a list of all + // of the embedded calls in the display list and so the op counts + // will not be equal between the two. + if (!testP.is_draw_display_list()) { + EXPECT_EQ(display_list->op_count(), sk_picture->approximateOpCount()) + << info; + } - display_list->RenderTo(test_surface->getCanvas()); - compareToReference(test_surface.get(), &ref_pixels, + display_list->RenderTo(dl_surface.canvas()); + compareToReference(dl_surface.pixmap(), sk_pixels, info + " (DisplayList built directly -> surface)", &dl_bounds, &tolerance, bg); } // This test cannot work if the rendering is using shadows until // we can access the Skia ShadowRec via public headers. - if (!TestingDrawShadows) { + if (!testP.is_draw_shadows()) { // This sequence renders SkCanvas calls to a DisplayList and then // plays them back on SkCanvas to SkSurface // SkCanvas calls => DisplayList => rendering - sk_sp test_surface = makeSurface(bg); + RenderSurface cv_dl_surface = env.MakeSurface(bg); DisplayListCanvasRecorder dl_recorder(TestBounds); - SkPaint test_paint; - cv_setup(&dl_recorder, test_paint); - cv_render(&dl_recorder, test_paint); - dl_recorder.builder()->Build()->RenderTo(test_surface->getCanvas()); - compareToReference(test_surface.get(), &ref_pixels, + caseP.render_to(&dl_recorder, testP); + dl_recorder.builder()->Build()->RenderTo(cv_dl_surface.canvas()); + compareToReference(cv_dl_surface.pixmap(), sk_pixels, info + " (Skia calls -> DisplayList -> surface)", - nullptr, nullptr, nullptr); + nullptr, nullptr, bg); } { @@ -1205,41 +1760,41 @@ class CanvasCompareTester { const int TestHeight2 = TestHeight * 2; const SkScalar TestScale = 2.0; - SkPictureRecorder sk_recorder; - SkCanvas* ref_canvas = sk_recorder.beginRecording(TestBounds); + SkPictureRecorder sk_x2_recorder; + SkCanvas* ref_canvas = sk_x2_recorder.beginRecording(TestBounds); SkPaint ref_paint; - cv_setup(ref_canvas, ref_paint); - cv_render(ref_canvas, ref_paint); - sk_sp ref_picture = sk_recorder.finishRecordingAsPicture(); - sk_sp ref_surface2 = makeSurface(bg, TestWidth2, TestHeight2); - SkCanvas* ref_canvas2 = ref_surface2->getCanvas(); - ref_canvas2->scale(TestScale, TestScale); - ref_picture->playback(ref_canvas2); - SkPixmap ref_pixels2; - ASSERT_TRUE(ref_surface2->peekPixels(&ref_pixels2)) << info; - ASSERT_EQ(ref_pixels2.width(), TestWidth2) << info; - ASSERT_EQ(ref_pixels2.height(), TestHeight2) << info; - ASSERT_EQ(ref_pixels2.info().bytesPerPixel(), 4) << info; - - DisplayListBuilder builder(TestBounds); - dl_setup(builder); - dl_render(builder); - sk_sp display_list = builder.Build(); - sk_sp test_surface = makeSurface(bg, TestWidth2, TestHeight2); - SkCanvas* test_canvas = test_surface->getCanvas(); - test_canvas->scale(TestScale, TestScale); - display_list->RenderTo(test_canvas); - compareToReference(test_surface.get(), &ref_pixels2, + caseP.render_to(ref_canvas, testP); + sk_sp ref_x2_picture = + sk_x2_recorder.finishRecordingAsPicture(); + RenderSurface ref_x2_surface = + env.MakeSurface(bg, TestWidth2, TestHeight2); + SkCanvas* ref_x2_canvas = ref_x2_surface.canvas(); + ref_x2_canvas->scale(TestScale, TestScale); + ref_x2_picture->playback(ref_x2_canvas); + const SkPixmap* ref_x2_pixels = ref_x2_surface.pixmap(); + ASSERT_EQ(ref_x2_pixels->width(), TestWidth2) << info; + ASSERT_EQ(ref_x2_pixels->height(), TestHeight2) << info; + ASSERT_EQ(ref_x2_pixels->info().bytesPerPixel(), 4) << info; + + DisplayListBuilder builder_x2(TestBounds); + caseP.render_to(builder_x2, testP); + sk_sp display_list_x2 = builder_x2.Build(); + RenderSurface test_x2_surface = + env.MakeSurface(bg, TestWidth2, TestHeight2); + SkCanvas* test_x2_canvas = test_x2_surface.canvas(); + test_x2_canvas->scale(TestScale, TestScale); + display_list_x2->RenderTo(test_x2_canvas); + compareToReference(test_x2_surface.pixmap(), ref_x2_pixels, info + " (Both rendered scaled 2x)", nullptr, nullptr, - nullptr, TestWidth2, TestHeight2, false); + bg, TestWidth2, TestHeight2, false); } } - static void checkPixels(SkPixmap* ref_pixels, + static void checkPixels(const SkPixmap* ref_pixels, SkRect ref_bounds, const std::string info, - const SkColor* bg) { - SkPMColor untouched = (bg) ? SkPreMultiplyColor(*bg) : 0; + const SkColor bg) { + SkPMColor untouched = SkPreMultiplyColor(bg); int pixels_touched = 0; int pixels_oob = 0; SkIRect i_bounds = ref_bounds.roundOut(); @@ -1258,21 +1813,45 @@ class CanvasCompareTester { ASSERT_GT(pixels_touched, 0) << info; } - static void compareToReference(SkSurface* test_surface, - SkPixmap* reference, + static void quickCompareToReference(const SkPixmap* ref_pixels, + const SkPixmap* test_pixels, + bool should_match, + const std::string info) { + ASSERT_EQ(test_pixels->width(), ref_pixels->width()) << info; + ASSERT_EQ(test_pixels->height(), ref_pixels->height()) << info; + ASSERT_EQ(test_pixels->info().bytesPerPixel(), 4) << info; + ASSERT_EQ(ref_pixels->info().bytesPerPixel(), 4) << info; + int pixels_different = 0; + for (int y = 0; y < test_pixels->height(); y++) { + const uint32_t* ref_row = ref_pixels->addr32(0, y); + const uint32_t* test_row = test_pixels->addr32(0, y); + for (int x = 0; x < test_pixels->width(); x++) { + if (ref_row[x] != test_row[x]) { + pixels_different++; + } + } + } + if (should_match) { + ASSERT_EQ(pixels_different, 0) << info; + } else { + ASSERT_NE(pixels_different, 0) << info; + } + } + + static void compareToReference(const SkPixmap* test_pixels, + const SkPixmap* ref_pixels, const std::string info, SkRect* bounds, const BoundsTolerance* tolerance, - const SkColor* bg, + const SkColor bg, int width = TestWidth, int height = TestHeight, bool printMismatches = false) { - SkPMColor untouched = (bg) ? SkPreMultiplyColor(*bg) : 0; - SkPixmap test_pixels; - ASSERT_TRUE(test_surface->peekPixels(&test_pixels)) << info; - ASSERT_EQ(test_pixels.width(), width) << info; - ASSERT_EQ(test_pixels.height(), height) << info; - ASSERT_EQ(test_pixels.info().bytesPerPixel(), 4) << info; + SkPMColor untouched = SkPreMultiplyColor(bg); + ASSERT_EQ(test_pixels->width(), width) << info; + ASSERT_EQ(test_pixels->height(), height) << info; + ASSERT_EQ(test_pixels->info().bytesPerPixel(), 4) << info; + ASSERT_EQ(ref_pixels->info().bytesPerPixel(), 4) << info; SkIRect i_bounds = bounds ? bounds->roundOut() : SkIRect::MakeWH(width, height); @@ -1283,18 +1862,22 @@ class CanvasCompareTester { int maxX = 0; int maxY = 0; for (int y = 0; y < height; y++) { - const uint32_t* ref_row = reference->addr32(0, y); - const uint32_t* test_row = test_pixels.addr32(0, y); + const uint32_t* ref_row = ref_pixels->addr32(0, y); + const uint32_t* test_row = test_pixels->addr32(0, y); for (int x = 0; x < width; x++) { if (bounds && test_row[x] != untouched) { - if (minX > x) + if (minX > x) { minX = x; - if (minY > y) + } + if (minY > y) { minY = y; - if (maxX <= x) + } + if (maxX <= x) { maxX = x + 1; - if (maxY <= y) + } + if (maxY <= y) { maxY = y + 1; + } if (!i_bounds.contains(x, y)) { pixels_oob++; } @@ -1336,12 +1919,14 @@ class CanvasCompareTester { int pad_top = std::max(0, pixTop - bounds.fTop); int pad_right = std::max(0, bounds.fRight - pixRight); int pad_bottom = std::max(0, bounds.fBottom - pixBottom); - int pixWidth = pixRight - pixLeft; - int pixHeight = pixBottom - pixTop; - SkISize pixSize = SkISize::Make(pixWidth, pixHeight); + SkIRect pix_bounds = + SkIRect::MakeLTRB(pixLeft, pixTop, pixRight, pixBottom); + SkISize pix_size = pix_bounds.size(); + int pixWidth = pix_size.width(); + int pixHeight = pix_size.height(); int worst_pad_x = std::max(pad_left, pad_right); int worst_pad_y = std::max(pad_top, pad_bottom); - if (tolerance->overflows(pixSize, worst_pad_x, worst_pad_y)) { + if (tolerance->overflows(pix_bounds, worst_pad_x, worst_pad_y)) { FML_LOG(ERROR) << "Overflow for " << info; FML_LOG(ERROR) << "pix bounds[" // << pixLeft << ", " << pixTop << " => " // @@ -1356,7 +1941,7 @@ class CanvasCompareTester { << worst_pad_x << ", " << worst_pad_y // << " (" << (worst_pad_x * 100.0 / pixWidth) // << "%, " << (worst_pad_y * 100.0 / pixHeight) << "%)"; - int pix_area = pixSize.area(); + int pix_area = pix_size.area(); int dl_area = bounds.width() * bounds.height(); FML_LOG(ERROR) << "Total overflow area: " << (dl_area - pix_area) // << " (+" << (dl_area * 100.0 / pix_area - 100.0) << "%)"; @@ -1364,16 +1949,6 @@ class CanvasCompareTester { } } - static sk_sp makeSurface(const SkColor* bg, - int width = TestWidth, - int height = TestHeight) { - sk_sp surface = SkSurface::MakeRasterN32Premul(width, height); - if (bg) { - surface->getCanvas()->drawColor(*bg); - } - return surface; - } - static const sk_sp testImage; static const sk_sp makeTestImage() { sk_sp surface = @@ -1381,7 +1956,7 @@ class CanvasCompareTester { SkCanvas* canvas = surface->getCanvas(); SkPaint p0, p1; p0.setStyle(SkPaint::kFill_Style); - p0.setColor(SK_ColorGREEN); + p0.setColor(SkColorSetARGB(0xff, 0x00, 0xfe, 0x00)); // off-green p1.setStyle(SkPaint::kFill_Style); p1.setColor(SK_ColorBLUE); // Some pixels need some transparency for DstIn testing @@ -1396,6 +1971,8 @@ class CanvasCompareTester { return surface->makeImageSnapshot(); } + static const sk_sp testImageShader; + static sk_sp MakeTextBlob(std::string string, SkScalar font_height) { SkFont font(SkTypeface::MakeFromName("ahem", SkFontStyle::Normal()), @@ -1405,230 +1982,203 @@ class CanvasCompareTester { } }; -bool CanvasCompareTester::TestingDrawShadows = false; -bool CanvasCompareTester::TestingDrawVertices = false; -bool CanvasCompareTester::TestingDrawAtlas = false; BoundsTolerance CanvasCompareTester::DefaultTolerance = BoundsTolerance().addAbsolutePadding(1, 1); -const sk_sp CanvasCompareTester::testImage = - CanvasCompareTester::makeTestImage(); - -TEST(DisplayListCanvas, DrawPaint) { - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawPaint(paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawPaint(); - }); -} - -TEST(DisplayListCanvas, DrawColor) { - CanvasCompareTester::RenderNoAttributes( // - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawColor(SK_ColorMAGENTA); - }, - [=](DisplayListBuilder& builder) { // - builder.drawColor(SK_ColorMAGENTA, SkBlendMode::kSrcOver); - }); -} - -BoundsTolerance lineTolerance(const BoundsTolerance& tolerance, - const SkPaint& paint, - const SkMatrix& matrix, - bool is_horizontal, - bool is_vertical, - bool ignores_butt_cap) { - SkScalar adjust = 0.0; - SkScalar half_width = paint.getStrokeWidth() * 0.5f; - if (tolerance.discrete_offset() > 0) { - // When a discrete path effect is added, the bounds calculations must allow - // for miters in any direction, but a horizontal line will not have - // miters in the horizontal direction, similarly for vertical - // lines, and diagonal lines will have miters off at a "45 degree" angle - // that don't expand the bounds much at all. - // Also, the discrete offset will not move any points parallel with - // the line, so provide tolerance for both miters and offset. - adjust = half_width * paint.getStrokeMiter() + tolerance.discrete_offset(); - } - if (paint.getStrokeCap() == SkPaint::kButt_Cap && !ignores_butt_cap) { - adjust = std::max(adjust, half_width); - } - if (adjust == 0) { - return CanvasCompareTester::DefaultAdjuster(tolerance, paint, matrix); - } - SkScalar hTolerance; - SkScalar vTolerance; - if (is_horizontal) { - FML_DCHECK(!is_vertical); - hTolerance = adjust; - vTolerance = 0; - } else if (is_vertical) { - hTolerance = 0; - vTolerance = adjust; - } else { - // The perpendicular miters just do not impact the bounds of - // diagonal lines at all as they are aimed in the wrong direction - // to matter. So allow tolerance in both axes. - hTolerance = vTolerance = adjust; - } - BoundsTolerance new_tolerance = - tolerance.addBoundsPadding(hTolerance, vTolerance); - return CanvasCompareTester::DefaultAdjuster(new_tolerance, paint, matrix); -} - -// For drawing horizontal lines -BoundsTolerance hLineTolerance(const BoundsTolerance& tolerance, - const SkPaint& paint, - const SkMatrix& matrix) { - return lineTolerance(tolerance, paint, matrix, true, false, false); -} +const sk_sp CanvasCompareTester::testImage = makeTestImage(); +const sk_sp CanvasCompareTester::testImageShader = + makeTestImage()->makeShader(SkTileMode::kRepeat, + SkTileMode::kRepeat, + SkSamplingOptions()); + +// Eventually this bare bones testing::Test fixture will subsume the +// CanvasCompareTester and the TestParameters could then become just +// configuration calls made upon the fixture. +template +class DisplayListCanvasTestBase : public BaseT, protected DisplayListOpFlags { + public: + DisplayListCanvasTestBase() = default; -// For drawing vertical lines -BoundsTolerance vLineTolerance(const BoundsTolerance& tolerance, - const SkPaint& paint, - const SkMatrix& matrix) { - return lineTolerance(tolerance, paint, matrix, false, true, false); -} + private: + FML_DISALLOW_COPY_AND_ASSIGN(DisplayListCanvasTestBase); +}; +using DisplayListCanvas = DisplayListCanvasTestBase<::testing::Test>; -// For drawing diagonal lines -BoundsTolerance dLineTolerance(const BoundsTolerance& tolerance, - const SkPaint& paint, - const SkMatrix& matrix) { - return lineTolerance(tolerance, paint, matrix, false, false, false); +TEST_F(DisplayListCanvas, DrawPaint) { + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawPaint(paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawPaint(); + }, + kDrawPaintFlags)); } -// For drawing individual points (drawPoints(Point_Mode)) -BoundsTolerance pointsTolerance(const BoundsTolerance& tolerance, - const SkPaint& paint, - const SkMatrix& matrix) { - return lineTolerance(tolerance, paint, matrix, false, false, true); +TEST_F(DisplayListCanvas, DrawColor) { + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { + canvas->drawColor(SK_ColorMAGENTA); + }, + [=](DisplayListBuilder& builder) { + builder.drawColor(SK_ColorMAGENTA, SkBlendMode::kSrcOver); + }, + kDrawColorFlags)); } -TEST(DisplayListCanvas, DrawDiagonalLines) { +TEST_F(DisplayListCanvas, DrawDiagonalLines) { SkPoint p1 = SkPoint::Make(RenderLeft, RenderTop); SkPoint p2 = SkPoint::Make(RenderRight, RenderBottom); SkPoint p3 = SkPoint::Make(RenderLeft, RenderBottom); SkPoint p4 = SkPoint::Make(RenderRight, RenderTop); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - // Skia requires kStroke style on horizontal and vertical - // lines to get the bounds correct. - // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 - SkPaint p = paint; - p.setStyle(SkPaint::kStroke_Style); - canvas->drawLine(p1, p2, p); - canvas->drawLine(p3, p4, p); - }, - [=](DisplayListBuilder& builder) { // - builder.drawLine(p1, p2); - builder.drawLine(p3, p4); - }, - dLineTolerance); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + // Skia requires kStroke style on horizontal and vertical + // lines to get the bounds correct. + // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 + SkPaint p = paint; + p.setStyle(SkPaint::kStroke_Style); + canvas->drawLine(p1, p2, p); + canvas->drawLine(p3, p4, p); + }, + [=](DisplayListBuilder& builder) { // + builder.drawLine(p1, p2); + builder.drawLine(p3, p4); + }, + kDrawLineFlags) + .set_draw_line()); } -TEST(DisplayListCanvas, DrawHorizontalLine) { +TEST_F(DisplayListCanvas, DrawHorizontalLine) { SkPoint p1 = SkPoint::Make(RenderLeft, RenderCenterY); SkPoint p2 = SkPoint::Make(RenderRight, RenderCenterY); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - // Skia requires kStroke style on horizontal and vertical - // lines to get the bounds correct. - // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 - SkPaint p = paint; - p.setStyle(SkPaint::kStroke_Style); - canvas->drawLine(p1, p2, p); - }, - [=](DisplayListBuilder& builder) { // - builder.drawLine(p1, p2); - }, - hLineTolerance); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + // Skia requires kStroke style on horizontal and vertical + // lines to get the bounds correct. + // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 + SkPaint p = paint; + p.setStyle(SkPaint::kStroke_Style); + canvas->drawLine(p1, p2, p); + }, + [=](DisplayListBuilder& builder) { // + builder.drawLine(p1, p2); + }, + kDrawHVLineFlags) + .set_draw_line() + .set_horizontal_line()); } -TEST(DisplayListCanvas, DrawVerticalLine) { +TEST_F(DisplayListCanvas, DrawVerticalLine) { SkPoint p1 = SkPoint::Make(RenderCenterX, RenderTop); SkPoint p2 = SkPoint::Make(RenderCenterY, RenderBottom); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - // Skia requires kStroke style on horizontal and vertical - // lines to get the bounds correct. - // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 - SkPaint p = paint; - p.setStyle(SkPaint::kStroke_Style); - canvas->drawLine(p1, p2, p); - }, - [=](DisplayListBuilder& builder) { // - builder.drawLine(p1, p2); - }, - vLineTolerance); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + // Skia requires kStroke style on horizontal and vertical + // lines to get the bounds correct. + // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 + SkPaint p = paint; + p.setStyle(SkPaint::kStroke_Style); + canvas->drawLine(p1, p2, p); + }, + [=](DisplayListBuilder& builder) { // + builder.drawLine(p1, p2); + }, + kDrawHVLineFlags) + .set_draw_line() + .set_vertical_line()); } -TEST(DisplayListCanvas, DrawRect) { - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawRect(RenderBounds, paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawRect(RenderBounds); - }); +TEST_F(DisplayListCanvas, DrawRect) { + // Bounds are offset by 0.5 pixels to induce AA + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawRect(RenderBounds.makeOffset(0.5, 0.5), paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawRect(RenderBounds.makeOffset(0.5, 0.5)); + }, + kDrawRectFlags)); } -TEST(DisplayListCanvas, DrawOval) { +TEST_F(DisplayListCanvas, DrawOval) { SkRect rect = RenderBounds.makeInset(0, 10); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawOval(rect, paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawOval(rect); - }); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawOval(rect, paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawOval(rect); + }, + kDrawOvalFlags)); } -TEST(DisplayListCanvas, DrawCircle) { - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawCircle(TestCenter, RenderRadius, paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawCircle(TestCenter, RenderRadius); - }); +TEST_F(DisplayListCanvas, DrawCircle) { + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawCircle(TestCenter, RenderRadius, paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawCircle(TestCenter, RenderRadius); + }, + kDrawCircleFlags)); } -TEST(DisplayListCanvas, DrawRRect) { +TEST_F(DisplayListCanvas, DrawRRect) { SkRRect rrect = SkRRect::MakeRectXY(RenderBounds, RenderCornerRadius, RenderCornerRadius); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawRRect(rrect, paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawRRect(rrect); - }); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawRRect(rrect, paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawRRect(rrect); + }, + kDrawRRectFlags)); } -TEST(DisplayListCanvas, DrawDRRect) { +TEST_F(DisplayListCanvas, DrawDRRect) { SkRRect outer = SkRRect::MakeRectXY(RenderBounds, RenderCornerRadius, RenderCornerRadius); SkRect innerBounds = RenderBounds.makeInset(30.0, 30.0); SkRRect inner = SkRRect::MakeRectXY(innerBounds, RenderCornerRadius, RenderCornerRadius); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawDRRect(outer, inner, paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawDRRect(outer, inner); - }); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawDRRect(outer, inner, paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawDRRect(outer, inner); + }, + kDrawDRRectFlags)); } -TEST(DisplayListCanvas, DrawPath) { +TEST_F(DisplayListCanvas, DrawPath) { SkPath path; + + // unclosed lines to show some caps + path.moveTo(RenderLeft + 15, RenderTop + 15); + path.lineTo(RenderRight - 15, RenderBottom - 15); + path.moveTo(RenderLeft + 15, RenderBottom - 15); + path.lineTo(RenderRight - 15, RenderTop + 15); + path.addRect(RenderBounds); + + // miter diamonds horizontally and vertically to show miters path.moveTo(VerticalMiterDiamondPoints[0]); for (int i = 1; i < VerticalMiterDiamondPointCount; i++) { path.lineTo(VerticalMiterDiamondPoints[i]); @@ -1639,43 +2189,61 @@ TEST(DisplayListCanvas, DrawPath) { path.lineTo(HorizontalMiterDiamondPoints[i]); } path.close(); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawPath(path, paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawPath(path); - }); + + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawPath(path, paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawPath(path); + }, + kDrawPathFlags)); } -TEST(DisplayListCanvas, DrawArc) { - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawArc(RenderBounds, 60, 330, false, paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawArc(RenderBounds, 60, 330, false); - }); +TEST_F(DisplayListCanvas, DrawArc) { + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawArc(RenderBounds, 60, 330, false, paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawArc(RenderBounds, 60, 330, false); + }, + kDrawArcNoCenterFlags)); } -TEST(DisplayListCanvas, DrawArcCenter) { - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawArc(RenderBounds, 60, 330, true, paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawArc(RenderBounds, 60, 330, true); - }); +TEST_F(DisplayListCanvas, DrawArcCenter) { + // Center arcs that inscribe nearly a whole circle except for a small + // arc extent gap have 2 angles that may appear or disappear at the + // various miter limits tested (0, 4, and 10). + // The center angle here is 12 degrees which shows a miter + // at limit=10, but not 0 or 4. + // The arcs at the corners where it turns in towards the + // center show miters at 4 and 10, but not 0. + // Limit == 0, neither corner does a miter + // Limit == 4, only the edge "turn-in" corners miter + // Limit == 10, edge and center corners all miter + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawArc(RenderBounds, 60, 360 - 12, true, paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawArc(RenderBounds, 60, 360 - 12, true); + }, + kDrawArcWithCenterFlags) + .set_draw_arc_center()); } -TEST(DisplayListCanvas, DrawPointsAsPoints) { +TEST_F(DisplayListCanvas, DrawPointsAsPoints) { // The +/- 16 points are designed to fall just inside the clips // that are tested against so we avoid lots of undrawn pixels // in the accumulated bounds. const SkScalar x0 = RenderLeft; const SkScalar x1 = RenderLeft + 16; const SkScalar x2 = (RenderLeft + RenderCenterX) * 0.5; - const SkScalar x3 = RenderCenterX; + const SkScalar x3 = RenderCenterX + 0.1; const SkScalar x4 = (RenderRight + RenderCenterX) * 0.5; const SkScalar x5 = RenderRight - 16; const SkScalar x6 = RenderRight; @@ -1683,7 +2251,7 @@ TEST(DisplayListCanvas, DrawPointsAsPoints) { const SkScalar y0 = RenderTop; const SkScalar y1 = RenderTop + 16; const SkScalar y2 = (RenderTop + RenderCenterY) * 0.5; - const SkScalar y3 = RenderCenterY; + const SkScalar y3 = RenderCenterY + 0.1; const SkScalar y4 = (RenderBottom + RenderCenterY) * 0.5; const SkScalar y5 = RenderBottom - 16; const SkScalar y6 = RenderBottom; @@ -1701,22 +2269,25 @@ TEST(DisplayListCanvas, DrawPointsAsPoints) { // clang-format on const int count = sizeof(points) / sizeof(points[0]); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - // Skia requires kStroke style on horizontal and vertical - // lines to get the bounds correct. - // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 - SkPaint p = paint; - p.setStyle(SkPaint::kStroke_Style); - canvas->drawPoints(SkCanvas::kPoints_PointMode, count, points, p); - }, - [=](DisplayListBuilder& builder) { // - builder.drawPoints(SkCanvas::kPoints_PointMode, count, points); - }, - pointsTolerance); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + // Skia requires kStroke style on horizontal and vertical + // lines to get the bounds correct. + // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 + SkPaint p = paint; + p.setStyle(SkPaint::kStroke_Style); + canvas->drawPoints(SkCanvas::kPoints_PointMode, count, points, p); + }, + [=](DisplayListBuilder& builder) { // + builder.drawPoints(SkCanvas::kPoints_PointMode, count, points); + }, + kDrawPointsAsPointsFlags) + .set_draw_line() + .set_ignores_dashes()); } -TEST(DisplayListCanvas, DrawPointsAsLines) { +TEST_F(DisplayListCanvas, DrawPointsAsLines) { const SkScalar x0 = RenderLeft + 1; const SkScalar x1 = RenderLeft + 16; const SkScalar x2 = RenderRight - 16; @@ -1748,21 +2319,23 @@ TEST(DisplayListCanvas, DrawPointsAsLines) { const int count = sizeof(points) / sizeof(points[0]); ASSERT_TRUE((count & 1) == 0); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - // Skia requires kStroke style on horizontal and vertical - // lines to get the bounds correct. - // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 - SkPaint p = paint; - p.setStyle(SkPaint::kStroke_Style); - canvas->drawPoints(SkCanvas::kLines_PointMode, count, points, p); - }, - [=](DisplayListBuilder& builder) { // - builder.drawPoints(SkCanvas::kLines_PointMode, count, points); - }); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + // Skia requires kStroke style on horizontal and vertical + // lines to get the bounds correct. + // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 + SkPaint p = paint; + p.setStyle(SkPaint::kStroke_Style); + canvas->drawPoints(SkCanvas::kLines_PointMode, count, points, p); + }, + [=](DisplayListBuilder& builder) { // + builder.drawPoints(SkCanvas::kLines_PointMode, count, points); + }, + kDrawPointsAsLinesFlags)); } -TEST(DisplayListCanvas, DrawPointsAsPolygon) { +TEST_F(DisplayListCanvas, DrawPointsAsPolygon) { const SkPoint points1[] = { // RenderBounds box with a diagonal SkPoint::Make(RenderLeft, RenderTop), @@ -1774,21 +2347,24 @@ TEST(DisplayListCanvas, DrawPointsAsPolygon) { }; const int count1 = sizeof(points1) / sizeof(points1[0]); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - // Skia requires kStroke style on horizontal and vertical - // lines to get the bounds correct. - // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 - SkPaint p = paint; - p.setStyle(SkPaint::kStroke_Style); - canvas->drawPoints(SkCanvas::kPolygon_PointMode, count1, points1, p); - }, - [=](DisplayListBuilder& builder) { // - builder.drawPoints(SkCanvas::kPolygon_PointMode, count1, points1); - }); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + // Skia requires kStroke style on horizontal and vertical + // lines to get the bounds correct. + // See https://bugs.chromium.org/p/skia/issues/detail?id=12446 + SkPaint p = paint; + p.setStyle(SkPaint::kStroke_Style); + canvas->drawPoints(SkCanvas::kPolygon_PointMode, count1, points1, + p); + }, + [=](DisplayListBuilder& builder) { // + builder.drawPoints(SkCanvas::kPolygon_PointMode, count1, points1); + }, + kDrawPointsAsPolygonFlags)); } -TEST(DisplayListCanvas, DrawVerticesWithColors) { +TEST_F(DisplayListCanvas, DrawVerticesWithColors) { // Cover as many sides of the box with only 6 vertices: // +----------+ // |xxxxxxxxxx| @@ -1814,17 +2390,21 @@ TEST(DisplayListCanvas, DrawVerticesWithColors) { }; const sk_sp vertices = SkVertices::MakeCopy( SkVertices::kTriangles_VertexMode, 6, pts, nullptr, colors); - CanvasCompareTester::RenderVertices( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawVertices(vertices.get(), SkBlendMode::kSrcOver, paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawVertices(vertices, SkBlendMode::kSrcOver); - }); - ASSERT_TRUE(vertices->unique()); + + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawVertices(vertices.get(), SkBlendMode::kSrcOver, paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawVertices(vertices, SkBlendMode::kSrcOver); + }, + kDrawVerticesFlags) + .set_draw_vertices()); + EXPECT_TRUE(vertices->unique()); } -TEST(DisplayListCanvas, DrawVerticesWithImage) { +TEST_F(DisplayListCanvas, DrawVerticesWithImage) { // Cover as many sides of the box with only 6 vertices: // +----------+ // |xxxxxxxxxx| @@ -1854,226 +2434,267 @@ TEST(DisplayListCanvas, DrawVerticesWithImage) { }; const sk_sp vertices = SkVertices::MakeCopy( SkVertices::kTriangles_VertexMode, 6, pts, tex, nullptr); - const sk_sp shader = CanvasCompareTester::testImage->makeShader( - SkTileMode::kRepeat, SkTileMode::kRepeat, SkSamplingOptions()); - CanvasCompareTester::RenderVertices( - [=](SkCanvas* canvas, SkPaint& paint) { // - paint.setShader(shader); - canvas->drawVertices(vertices.get(), SkBlendMode::kSrcOver, paint); - }, - [=](DisplayListBuilder& builder) { // - builder.setShader(shader); - builder.drawVertices(vertices, SkBlendMode::kSrcOver); - }); - ASSERT_TRUE(vertices->unique()); - ASSERT_TRUE(shader->unique()); + + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + SkPaint v_paint = paint; + if (v_paint.getShader() == nullptr) { + v_paint.setShader(CanvasCompareTester::testImageShader); + } + canvas->drawVertices(vertices.get(), SkBlendMode::kSrcOver, + v_paint); + }, + [=](DisplayListBuilder& builder) { // + if (builder.getShader() == nullptr) { + builder.setShader(CanvasCompareTester::testImageShader); + } + builder.drawVertices(vertices, SkBlendMode::kSrcOver); + }, + kDrawVerticesFlags) + .set_draw_vertices()); + + EXPECT_TRUE(vertices->unique()); + EXPECT_TRUE(CanvasCompareTester::testImageShader->unique()); } -TEST(DisplayListCanvas, DrawImageNearest) { - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawImage(CanvasCompareTester::testImage, RenderLeft, RenderTop, - DisplayList::NearestSampling, &paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawImage(CanvasCompareTester::testImage, - SkPoint::Make(RenderLeft, RenderTop), - DisplayList::NearestSampling, true); - }); +TEST_F(DisplayListCanvas, DrawImageNearest) { + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawImage(CanvasCompareTester::testImage, // + RenderLeft, RenderTop, + DisplayList::NearestSampling, &paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawImage(CanvasCompareTester::testImage, + SkPoint::Make(RenderLeft, RenderTop), + DisplayList::NearestSampling, true); + }, + kDrawImageWithPaintFlags)); } -TEST(DisplayListCanvas, DrawImageNearestNoPaint) { - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawImage(CanvasCompareTester::testImage, RenderLeft, RenderTop, - DisplayList::NearestSampling, nullptr); - }, - [=](DisplayListBuilder& builder) { // - builder.drawImage(CanvasCompareTester::testImage, - SkPoint::Make(RenderLeft, RenderTop), - DisplayList::NearestSampling, false); - }); +TEST_F(DisplayListCanvas, DrawImageNearestNoPaint) { + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawImage(CanvasCompareTester::testImage, // + RenderLeft, RenderTop, + DisplayList::NearestSampling, nullptr); + }, + [=](DisplayListBuilder& builder) { // + builder.drawImage(CanvasCompareTester::testImage, + SkPoint::Make(RenderLeft, RenderTop), + DisplayList::NearestSampling, false); + }, + kDrawImageFlags)); } -TEST(DisplayListCanvas, DrawImageLinear) { - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawImage(CanvasCompareTester::testImage, RenderLeft, RenderTop, - DisplayList::LinearSampling, &paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawImage(CanvasCompareTester::testImage, - SkPoint::Make(RenderLeft, RenderTop), - DisplayList::LinearSampling, true); - }); +TEST_F(DisplayListCanvas, DrawImageLinear) { + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawImage(CanvasCompareTester::testImage, // + RenderLeft, RenderTop, + DisplayList::LinearSampling, &paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawImage(CanvasCompareTester::testImage, + SkPoint::Make(RenderLeft, RenderTop), + DisplayList::LinearSampling, true); + }, + kDrawImageWithPaintFlags)); } -TEST(DisplayListCanvas, DrawImageRectNearest) { +TEST_F(DisplayListCanvas, DrawImageRectNearest) { SkRect src = SkRect::MakeIWH(RenderWidth, RenderHeight).makeInset(5, 5); - SkRect dst = RenderBounds.makeInset(15.5, 10.5); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawImageRect(CanvasCompareTester::testImage, src, dst, - DisplayList::NearestSampling, &paint, - SkCanvas::kFast_SrcRectConstraint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawImageRect(CanvasCompareTester::testImage, src, dst, - DisplayList::NearestSampling, true); - }); + SkRect dst = RenderBounds.makeInset(10.5, 10.5); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawImageRect(CanvasCompareTester::testImage, src, dst, + DisplayList::NearestSampling, &paint, + SkCanvas::kFast_SrcRectConstraint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawImageRect(CanvasCompareTester::testImage, src, dst, + DisplayList::NearestSampling, true); + }, + kDrawImageRectWithPaintFlags)); } -TEST(DisplayListCanvas, DrawImageRectNearestNoPaint) { +TEST_F(DisplayListCanvas, DrawImageRectNearestNoPaint) { SkRect src = SkRect::MakeIWH(RenderWidth, RenderHeight).makeInset(5, 5); - SkRect dst = RenderBounds.makeInset(15.5, 10.5); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawImageRect(CanvasCompareTester::testImage, src, dst, - DisplayList::NearestSampling, nullptr, - SkCanvas::kFast_SrcRectConstraint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawImageRect(CanvasCompareTester::testImage, src, dst, - DisplayList::NearestSampling, false); - }); + SkRect dst = RenderBounds.makeInset(10.5, 10.5); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawImageRect(CanvasCompareTester::testImage, src, dst, + DisplayList::NearestSampling, nullptr, + SkCanvas::kFast_SrcRectConstraint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawImageRect(CanvasCompareTester::testImage, src, dst, + DisplayList::NearestSampling, false); + }, + kDrawImageRectFlags)); } -TEST(DisplayListCanvas, DrawImageRectLinear) { +TEST_F(DisplayListCanvas, DrawImageRectLinear) { SkRect src = SkRect::MakeIWH(RenderWidth, RenderHeight).makeInset(5, 5); - SkRect dst = RenderBounds.makeInset(15.5, 10.5); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawImageRect(CanvasCompareTester::testImage, src, dst, - DisplayList::LinearSampling, &paint, - SkCanvas::kFast_SrcRectConstraint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawImageRect(CanvasCompareTester::testImage, src, dst, - DisplayList::LinearSampling, true); - }); + SkRect dst = RenderBounds.makeInset(10.5, 10.5); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawImageRect(CanvasCompareTester::testImage, src, dst, + DisplayList::LinearSampling, &paint, + SkCanvas::kFast_SrcRectConstraint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawImageRect(CanvasCompareTester::testImage, src, dst, + DisplayList::LinearSampling, true); + }, + kDrawImageRectWithPaintFlags)); } -TEST(DisplayListCanvas, DrawImageNineNearest) { - SkIRect src = SkIRect::MakeWH(RenderWidth, RenderHeight).makeInset(5, 5); - SkRect dst = RenderBounds.makeInset(15.5, 10.5); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawImageNine(CanvasCompareTester::testImage.get(), src, dst, - SkFilterMode::kNearest, &paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawImageNine(CanvasCompareTester::testImage, src, dst, - SkFilterMode::kNearest, true); - }); +TEST_F(DisplayListCanvas, DrawImageNineNearest) { + SkIRect src = SkIRect::MakeWH(RenderWidth, RenderHeight).makeInset(25, 25); + SkRect dst = RenderBounds.makeInset(10.5, 10.5); + sk_sp image = CanvasCompareTester::testImage; + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { + canvas->drawImageNine(image.get(), src, dst, SkFilterMode::kNearest, + &paint); + }, + [=](DisplayListBuilder& builder) { + builder.drawImageNine(image, src, dst, SkFilterMode::kNearest, + true); + }, + kDrawImageNineWithPaintFlags)); } -TEST(DisplayListCanvas, DrawImageNineNearestNoPaint) { - SkIRect src = SkIRect::MakeWH(RenderWidth, RenderHeight).makeInset(5, 5); - SkRect dst = RenderBounds.makeInset(15.5, 10.5); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawImageNine(CanvasCompareTester::testImage.get(), src, dst, - SkFilterMode::kNearest, nullptr); - }, - [=](DisplayListBuilder& builder) { // - builder.drawImageNine(CanvasCompareTester::testImage, src, dst, - SkFilterMode::kNearest, false); - }); +TEST_F(DisplayListCanvas, DrawImageNineNearestNoPaint) { + SkIRect src = SkIRect::MakeWH(RenderWidth, RenderHeight).makeInset(25, 25); + SkRect dst = RenderBounds.makeInset(10.5, 10.5); + sk_sp image = CanvasCompareTester::testImage; + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { + canvas->drawImageNine(image.get(), src, dst, SkFilterMode::kNearest, + nullptr); + }, + [=](DisplayListBuilder& builder) { + builder.drawImageNine(image, src, dst, SkFilterMode::kNearest, + false); + }, + kDrawImageNineFlags)); } -TEST(DisplayListCanvas, DrawImageNineLinear) { - SkIRect src = SkIRect::MakeWH(RenderWidth, RenderHeight).makeInset(5, 5); - SkRect dst = RenderBounds.makeInset(15.5, 10.5); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawImageNine(CanvasCompareTester::testImage.get(), src, dst, - SkFilterMode::kLinear, &paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawImageNine(CanvasCompareTester::testImage, src, dst, - SkFilterMode::kLinear, true); - }); +TEST_F(DisplayListCanvas, DrawImageNineLinear) { + SkIRect src = SkIRect::MakeWH(RenderWidth, RenderHeight).makeInset(25, 25); + SkRect dst = RenderBounds.makeInset(10.5, 10.5); + sk_sp image = CanvasCompareTester::testImage; + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { + canvas->drawImageNine(image.get(), src, dst, SkFilterMode::kLinear, + &paint); + }, + [=](DisplayListBuilder& builder) { + builder.drawImageNine(image, src, dst, SkFilterMode::kLinear, true); + }, + kDrawImageNineWithPaintFlags)); } -TEST(DisplayListCanvas, DrawImageLatticeNearest) { - const SkRect dst = RenderBounds.makeInset(15.5, 10.5); +TEST_F(DisplayListCanvas, DrawImageLatticeNearest) { + const SkRect dst = RenderBounds.makeInset(10.5, 10.5); const int divX[] = { - (RenderLeft + RenderCenterX) / 2, - RenderCenterX, - (RenderRight + RenderCenterX) / 2, + RenderWidth * 1 / 4, + RenderWidth * 2 / 4, + RenderWidth * 3 / 4, }; const int divY[] = { - (RenderTop + RenderCenterY) / 2, - RenderCenterY, - (RenderBottom + RenderCenterY) / 2, + RenderHeight * 1 / 4, + RenderHeight * 2 / 4, + RenderHeight * 3 / 4, }; SkCanvas::Lattice lattice = { divX, divY, nullptr, 3, 3, nullptr, nullptr, }; - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawImageLattice(CanvasCompareTester::testImage.get(), lattice, - dst, SkFilterMode::kNearest, &paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawImageLattice(CanvasCompareTester::testImage, lattice, // - dst, SkFilterMode::kNearest, true); - }); + sk_sp image = CanvasCompareTester::testImage; + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { + canvas->drawImageLattice(image.get(), lattice, dst, + SkFilterMode::kNearest, &paint); + }, + [=](DisplayListBuilder& builder) { + builder.drawImageLattice(image, lattice, dst, + SkFilterMode::kNearest, true); + }, + kDrawImageLatticeWithPaintFlags)); } -TEST(DisplayListCanvas, DrawImageLatticeNearestNoPaint) { - const SkRect dst = RenderBounds.makeInset(15.5, 10.5); +TEST_F(DisplayListCanvas, DrawImageLatticeNearestNoPaint) { + const SkRect dst = RenderBounds.makeInset(10.5, 10.5); const int divX[] = { - (RenderLeft + RenderCenterX) / 2, - RenderCenterX, - (RenderRight + RenderCenterX) / 2, + RenderWidth * 1 / 4, + RenderWidth * 2 / 4, + RenderWidth * 3 / 4, }; const int divY[] = { - (RenderTop + RenderCenterY) / 2, - RenderCenterY, - (RenderBottom + RenderCenterY) / 2, + RenderHeight * 1 / 4, + RenderHeight * 2 / 4, + RenderHeight * 3 / 4, }; SkCanvas::Lattice lattice = { divX, divY, nullptr, 3, 3, nullptr, nullptr, }; - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawImageLattice(CanvasCompareTester::testImage.get(), lattice, - dst, SkFilterMode::kNearest, nullptr); - }, - [=](DisplayListBuilder& builder) { // - builder.drawImageLattice(CanvasCompareTester::testImage, lattice, // - dst, SkFilterMode::kNearest, false); - }); + sk_sp image = CanvasCompareTester::testImage; + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { + canvas->drawImageLattice(image.get(), lattice, dst, + SkFilterMode::kNearest, nullptr); + }, + [=](DisplayListBuilder& builder) { + builder.drawImageLattice(image, lattice, dst, + SkFilterMode::kNearest, false); + }, + kDrawImageLatticeFlags)); } -TEST(DisplayListCanvas, DrawImageLatticeLinear) { - const SkRect dst = RenderBounds.makeInset(15.5, 10.5); +TEST_F(DisplayListCanvas, DrawImageLatticeLinear) { + const SkRect dst = RenderBounds.makeInset(10.5, 10.5); const int divX[] = { - (RenderLeft + RenderCenterX) / 2, - RenderCenterX, - (RenderRight + RenderCenterX) / 2, + RenderWidth / 4, + RenderWidth / 2, + RenderWidth * 3 / 4, }; const int divY[] = { - (RenderTop + RenderCenterY) / 2, - RenderCenterY, - (RenderBottom + RenderCenterY) / 2, + RenderHeight / 4, + RenderHeight / 2, + RenderHeight * 3 / 4, }; SkCanvas::Lattice lattice = { divX, divY, nullptr, 3, 3, nullptr, nullptr, }; - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawImageLattice(CanvasCompareTester::testImage.get(), lattice, - dst, SkFilterMode::kLinear, &paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawImageLattice(CanvasCompareTester::testImage, lattice, // - dst, SkFilterMode::kLinear, true); - }); + sk_sp image = CanvasCompareTester::testImage; + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { + canvas->drawImageLattice(image.get(), lattice, dst, + SkFilterMode::kLinear, &paint); + }, + [=](DisplayListBuilder& builder) { + builder.drawImageLattice(image, lattice, dst, SkFilterMode::kLinear, + true); + }, + kDrawImageLatticeWithPaintFlags)); } -TEST(DisplayListCanvas, DrawAtlasNearest) { +TEST_F(DisplayListCanvas, DrawAtlasNearest) { const SkRSXform xform[] = { // clang-format off { 1.2f, 0.0f, RenderLeft, RenderTop}, @@ -2097,20 +2718,22 @@ TEST(DisplayListCanvas, DrawAtlasNearest) { SK_ColorMAGENTA, }; const sk_sp image = CanvasCompareTester::testImage; - CanvasCompareTester::RenderAtlas( - [=](SkCanvas* canvas, SkPaint& paint) { - canvas->drawAtlas(image.get(), xform, tex, colors, 4, - SkBlendMode::kSrcOver, DisplayList::NearestSampling, - nullptr, &paint); - }, - [=](DisplayListBuilder& builder) { - builder.drawAtlas(image, xform, tex, colors, 4, // - SkBlendMode::kSrcOver, DisplayList::NearestSampling, - nullptr, true); - }); + const SkSamplingOptions sampling = DisplayList::NearestSampling; + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { + canvas->drawAtlas(image.get(), xform, tex, colors, 4, + SkBlendMode::kSrcOver, sampling, nullptr, &paint); + }, + [=](DisplayListBuilder& builder) { + builder.drawAtlas(image, xform, tex, colors, 4, // + SkBlendMode::kSrcOver, sampling, nullptr, true); + }, + kDrawAtlasWithPaintFlags) + .set_draw_atlas()); } -TEST(DisplayListCanvas, DrawAtlasNearestNoPaint) { +TEST_F(DisplayListCanvas, DrawAtlasNearestNoPaint) { const SkRSXform xform[] = { // clang-format off { 1.2f, 0.0f, RenderLeft, RenderTop}, @@ -2134,20 +2757,24 @@ TEST(DisplayListCanvas, DrawAtlasNearestNoPaint) { SK_ColorMAGENTA, }; const sk_sp image = CanvasCompareTester::testImage; - CanvasCompareTester::RenderAtlas( - [=](SkCanvas* canvas, SkPaint& paint) { - canvas->drawAtlas(image.get(), xform, tex, colors, 4, - SkBlendMode::kSrcOver, DisplayList::NearestSampling, - nullptr, nullptr); - }, - [=](DisplayListBuilder& builder) { - builder.drawAtlas(image, xform, tex, colors, 4, // - SkBlendMode::kSrcOver, DisplayList::NearestSampling, - nullptr, false); - }); + const SkSamplingOptions sampling = DisplayList::NearestSampling; + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { + canvas->drawAtlas(image.get(), xform, tex, colors, 4, + SkBlendMode::kSrcOver, sampling, // + nullptr, nullptr); + }, + [=](DisplayListBuilder& builder) { + builder.drawAtlas(image, xform, tex, colors, 4, // + SkBlendMode::kSrcOver, sampling, // + nullptr, false); + }, + kDrawAtlasFlags) + .set_draw_atlas()); } -TEST(DisplayListCanvas, DrawAtlasLinear) { +TEST_F(DisplayListCanvas, DrawAtlasLinear) { const SkRSXform xform[] = { // clang-format off { 1.2f, 0.0f, RenderLeft, RenderTop}, @@ -2171,17 +2798,19 @@ TEST(DisplayListCanvas, DrawAtlasLinear) { SK_ColorMAGENTA, }; const sk_sp image = CanvasCompareTester::testImage; - CanvasCompareTester::RenderAtlas( - [=](SkCanvas* canvas, SkPaint& paint) { - canvas->drawAtlas(image.get(), xform, tex, colors, 2, // - SkBlendMode::kSrcOver, DisplayList::LinearSampling, - nullptr, &paint); - }, - [=](DisplayListBuilder& builder) { - builder.drawAtlas(image, xform, tex, colors, 2, // - SkBlendMode::kSrcOver, DisplayList::LinearSampling, - nullptr, true); - }); + const SkSamplingOptions sampling = DisplayList::LinearSampling; + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { + canvas->drawAtlas(image.get(), xform, tex, colors, 2, // + SkBlendMode::kSrcOver, sampling, nullptr, &paint); + }, + [=](DisplayListBuilder& builder) { + builder.drawAtlas(image, xform, tex, colors, 2, // + SkBlendMode::kSrcOver, sampling, nullptr, true); + }, + kDrawAtlasWithPaintFlags) + .set_draw_atlas()); } sk_sp makeTestPicture() { @@ -2189,91 +2818,86 @@ sk_sp makeTestPicture() { SkCanvas* cv = recorder.beginRecording(RenderBounds); SkPaint p; p.setStyle(SkPaint::kFill_Style); - SkScalar x_coords[] = { - RenderLeft, - RenderCenterX, - RenderRight, - }; - SkScalar y_coords[] = { - RenderTop, - RenderCenterY, - RenderBottom, - }; - SkColor colors[][2] = { - { - SK_ColorRED, - SK_ColorBLUE, - }, - { - SK_ColorGREEN, - SK_ColorYELLOW, - }, - }; - for (int j = 0; j < 2; j++) { - for (int i = 0; i < 2; i++) { - SkRect rect = { - x_coords[i], - y_coords[j], - x_coords[i + 1], - y_coords[j + 1], - }; - p.setColor(colors[i][j]); - cv->drawOval(rect, p); - } - } + p.setColor(SK_ColorRED); + cv->drawRect({RenderLeft, RenderTop, RenderCenterX, RenderCenterY}, p); + p.setColor(SK_ColorBLUE); + cv->drawRect({RenderCenterX, RenderTop, RenderRight, RenderCenterY}, p); + p.setColor(SK_ColorGREEN); + cv->drawRect({RenderLeft, RenderCenterY, RenderCenterX, RenderBottom}, p); + p.setColor(SK_ColorYELLOW); + cv->drawRect({RenderCenterX, RenderCenterY, RenderRight, RenderBottom}, p); return recorder.finishRecordingAsPicture(); } -TEST(DisplayListCanvas, DrawPicture) { +TEST_F(DisplayListCanvas, DrawPicture) { sk_sp picture = makeTestPicture(); - CanvasCompareTester::RenderNoAttributes( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawPicture(picture, nullptr, nullptr); - }, - [=](DisplayListBuilder& builder) { // - builder.drawPicture(picture, nullptr, false); - }); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawPicture(picture, nullptr, nullptr); + }, + [=](DisplayListBuilder& builder) { // + builder.drawPicture(picture, nullptr, false); + }, + kDrawPictureFlags)); } -TEST(DisplayListCanvas, DrawPictureWithMatrix) { +TEST_F(DisplayListCanvas, DrawPictureWithMatrix) { sk_sp picture = makeTestPicture(); SkMatrix matrix = SkMatrix::Scale(0.95, 0.95); - CanvasCompareTester::RenderNoAttributes( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawPicture(picture, &matrix, nullptr); - }, - [=](DisplayListBuilder& builder) { // - builder.drawPicture(picture, &matrix, false); - }); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawPicture(picture, &matrix, nullptr); + }, + [=](DisplayListBuilder& builder) { // + builder.drawPicture(picture, &matrix, false); + }, + kDrawPictureFlags)); } -TEST(DisplayListCanvas, DrawPictureWithPaint) { +TEST_F(DisplayListCanvas, DrawPictureWithPaint) { sk_sp picture = makeTestPicture(); - CanvasCompareTester::RenderAll( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawPicture(picture, nullptr, &paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawPicture(picture, nullptr, true); - }); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawPicture(picture, nullptr, &paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawPicture(picture, nullptr, true); + }, + kDrawPictureWithPaintFlags)); } -TEST(DisplayListCanvas, DrawDisplayList) { +sk_sp makeTestDisplayList() { DisplayListBuilder builder; builder.setStyle(SkPaint::kFill_Style); + builder.setColor(SK_ColorRED); + builder.drawRect({RenderLeft, RenderTop, RenderCenterX, RenderCenterY}); builder.setColor(SK_ColorBLUE); - builder.drawOval(RenderBounds); - sk_sp display_list = builder.Build(); - CanvasCompareTester::RenderNoAttributes( - [=](SkCanvas* canvas, SkPaint& paint) { // - display_list->RenderTo(canvas); - }, - [=](DisplayListBuilder& builder) { // - builder.drawDisplayList(display_list); - }); + builder.drawRect({RenderCenterX, RenderTop, RenderRight, RenderCenterY}); + builder.setColor(SK_ColorGREEN); + builder.drawRect({RenderLeft, RenderCenterY, RenderCenterX, RenderBottom}); + builder.setColor(SK_ColorYELLOW); + builder.drawRect({RenderCenterX, RenderCenterY, RenderRight, RenderBottom}); + return builder.Build(); } -TEST(DisplayListCanvas, DrawTextBlob) { +TEST_F(DisplayListCanvas, DrawDisplayList) { + sk_sp display_list = makeTestDisplayList(); + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + display_list->RenderTo(canvas); + }, + [=](DisplayListBuilder& builder) { // + builder.drawDisplayList(display_list); + }, + kDrawDisplayListFlags) + .set_draw_display_list()); +} + +TEST_F(DisplayListCanvas, DrawTextBlob) { // TODO(https://github.com/flutter/flutter/issues/82202): Remove once the // performance overlay can use Fuchsia's font manager instead of the empty // default. @@ -2282,38 +2906,31 @@ TEST(DisplayListCanvas, DrawTextBlob) { #endif // OS_FUCHSIA sk_sp blob = CanvasCompareTester::MakeTextBlob("Testing", RenderHeight * 0.33f); - SkScalar RenderY1_3 = RenderTop + RenderHeight * 0.33; - SkScalar RenderY2_3 = RenderTop + RenderHeight * 0.66; - CanvasCompareTester::RenderNoAttributes( - [=](SkCanvas* canvas, SkPaint& paint) { // - canvas->drawTextBlob(blob, RenderLeft, RenderY1_3, paint); - canvas->drawTextBlob(blob, RenderLeft, RenderY2_3, paint); - canvas->drawTextBlob(blob, RenderLeft, RenderBottom, paint); - }, - [=](DisplayListBuilder& builder) { // - builder.drawTextBlob(blob, RenderLeft, RenderY1_3); - builder.drawTextBlob(blob, RenderLeft, RenderY2_3); - builder.drawTextBlob(blob, RenderLeft, RenderBottom); - }, - CanvasCompareTester::DefaultAdjuster, + SkScalar RenderY1_3 = RenderTop + RenderHeight * 0.3; + SkScalar RenderY2_3 = RenderTop + RenderHeight * 0.6; + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + canvas->drawTextBlob(blob, RenderLeft, RenderY1_3, paint); + canvas->drawTextBlob(blob, RenderLeft, RenderY2_3, paint); + canvas->drawTextBlob(blob, RenderLeft, RenderBottom, paint); + }, + [=](DisplayListBuilder& builder) { // + builder.drawTextBlob(blob, RenderLeft, RenderY1_3); + builder.drawTextBlob(blob, RenderLeft, RenderY2_3); + builder.drawTextBlob(blob, RenderLeft, RenderBottom); + }, + kDrawTextBlobFlags) + .set_draw_text_blob(), // From examining the bounds differential for the "Default" case, the - // SkTextBlob adds a padding of ~31 on the left, ~30 on the right, - // ~12 on top and ~8 on the bottom, so we add 32h & 13v allowed + // SkTextBlob adds a padding of ~32 on the left, ~30 on the right, + // ~12 on top and ~8 on the bottom, so we add 33h & 13v allowed // padding to the tolerance - CanvasCompareTester::DefaultTolerance.addBoundsPadding(32, 13)); -} - -const BoundsTolerance shadowTolerance(const BoundsTolerance& tolerance, - const SkPaint& paint, - const SkMatrix& matrix) { - // Shadow primitives could use just a little more horizontal bounds - // tolerance when drawn with a perspective transform. - return CanvasCompareTester::DefaultAdjuster( - matrix.hasPerspective() ? tolerance.addScale(1.04, 1.0) : tolerance, - paint, matrix); + CanvasCompareTester::DefaultTolerance.addBoundsPadding(33, 13)); + EXPECT_TRUE(blob->unique()); } -TEST(DisplayListCanvas, DrawShadow) { +TEST_F(DisplayListCanvas, DrawShadow) { SkPath path; path.addRoundRect( { @@ -2326,19 +2943,21 @@ TEST(DisplayListCanvas, DrawShadow) { const SkColor color = SK_ColorDKGRAY; const SkScalar elevation = 5; - CanvasCompareTester::RenderShadows( - [=](SkCanvas* canvas, SkPaint& paint) { // - PhysicalShapeLayer::DrawShadow(canvas, path, color, elevation, false, - 1.0); - }, - [=](DisplayListBuilder& builder) { // - builder.drawShadow(path, color, elevation, false, 1.0); - }, - shadowTolerance, + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + PhysicalShapeLayer::DrawShadow(canvas, path, color, elevation, + false, 1.0); + }, + [=](DisplayListBuilder& builder) { // + builder.drawShadow(path, color, elevation, false, 1.0); + }, + kDrawShadowFlags) + .set_draw_shadows(), CanvasCompareTester::DefaultTolerance.addBoundsPadding(3, 3)); } -TEST(DisplayListCanvas, DrawShadowTransparentOccluder) { +TEST_F(DisplayListCanvas, DrawShadowTransparentOccluder) { SkPath path; path.addRoundRect( { @@ -2351,19 +2970,21 @@ TEST(DisplayListCanvas, DrawShadowTransparentOccluder) { const SkColor color = SK_ColorDKGRAY; const SkScalar elevation = 5; - CanvasCompareTester::RenderShadows( - [=](SkCanvas* canvas, SkPaint& paint) { // - PhysicalShapeLayer::DrawShadow(canvas, path, color, elevation, true, - 1.0); - }, - [=](DisplayListBuilder& builder) { // - builder.drawShadow(path, color, elevation, true, 1.0); - }, - shadowTolerance, + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + PhysicalShapeLayer::DrawShadow(canvas, path, color, elevation, true, + 1.0); + }, + [=](DisplayListBuilder& builder) { // + builder.drawShadow(path, color, elevation, true, 1.0); + }, + kDrawShadowFlags) + .set_draw_shadows(), CanvasCompareTester::DefaultTolerance.addBoundsPadding(3, 3)); } -TEST(DisplayListCanvas, DrawShadowDpr) { +TEST_F(DisplayListCanvas, DrawShadowDpr) { SkPath path; path.addRoundRect( { @@ -2376,15 +2997,17 @@ TEST(DisplayListCanvas, DrawShadowDpr) { const SkColor color = SK_ColorDKGRAY; const SkScalar elevation = 5; - CanvasCompareTester::RenderShadows( - [=](SkCanvas* canvas, SkPaint& paint) { // - PhysicalShapeLayer::DrawShadow(canvas, path, color, elevation, false, - 1.5); - }, - [=](DisplayListBuilder& builder) { // - builder.drawShadow(path, color, elevation, false, 1.5); - }, - shadowTolerance, + CanvasCompareTester::RenderAll( // + TestParameters( + [=](SkCanvas* canvas, const SkPaint& paint) { // + PhysicalShapeLayer::DrawShadow(canvas, path, color, elevation, + false, 1.5); + }, + [=](DisplayListBuilder& builder) { // + builder.drawShadow(path, color, elevation, false, 1.5); + }, + kDrawShadowFlags) + .set_draw_shadows(), CanvasCompareTester::DefaultTolerance.addBoundsPadding(3, 3)); } diff --git a/flow/display_list_unittests.cc b/flow/display_list_unittests.cc index 9e1409a8b1362..6228440a0f228 100644 --- a/flow/display_list_unittests.cc +++ b/flow/display_list_unittests.cc @@ -272,97 +272,99 @@ struct DisplayListInvocationGroup { std::vector allGroups = { { "SetAntiAlias", { - {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setAntiAlias(false);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setAntiAlias(true);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setAntiAlias(false);}}, } }, { "SetDither", { - {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setDither(false);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setDither(true);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setDither(false);}}, } }, { "SetInvertColors", { - {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setInvertColors(false);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setInvertColors(true);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setInvertColors(false);}}, } }, { "SetStrokeCap", { - {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStrokeCap(SkPaint::kButt_Cap);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStrokeCap(SkPaint::kRound_Cap);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStrokeCap(SkPaint::kSquare_Cap);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setStrokeCap(SkPaint::kButt_Cap);}}, } }, { "SetStrokeJoin", { {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStrokeJoin(SkPaint::kBevel_Join);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStrokeJoin(SkPaint::kRound_Join);}}, - {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStrokeJoin(SkPaint::kMiter_Join);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setStrokeJoin(SkPaint::kMiter_Join);}}, } }, { "SetStyle", { - {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStyle(SkPaint::kFill_Style);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStyle(SkPaint::kStroke_Style);}}, + {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStyle(SkPaint::kStrokeAndFill_Style);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setStyle(SkPaint::kFill_Style);}}, } }, { "SetStrokeWidth", { - {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStrokeWidth(0.0);}}, + {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStrokeWidth(1.0);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStrokeWidth(5.0);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setStrokeWidth(0.0);}}, } }, { "SetStrokeMiter", { {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStrokeMiter(0.0);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setStrokeMiter(5.0);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setStrokeMiter(4.0);}}, } }, { "SetColor", { {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setColor(SK_ColorGREEN);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setColor(SK_ColorBLUE);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setColor(SK_ColorBLACK);}}, } }, - { "SetBlendMode", { + { "SetBlendModeOrBlender", { {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setBlendMode(SkBlendMode::kSrcIn);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setBlendMode(SkBlendMode::kDstIn);}}, - } - }, - { "SetBlender", { - {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setBlender(nullptr);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setBlender(TestBlender1);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setBlender(TestBlender2);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setBlender(TestBlender3);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setBlendMode(SkBlendMode::kSrcOver);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setBlender(nullptr);}}, } }, { "SetShader", { - {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setShader(nullptr);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setShader(TestShader1);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setShader(TestShader2);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setShader(TestShader3);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setShader(nullptr);}}, } }, { "SetImageFilter", { - {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setImageFilter(nullptr);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setImageFilter(TestImageFilter1);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setImageFilter(TestImageFilter2);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setImageFilter(nullptr);}}, } }, { "SetColorFilter", { - {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setColorFilter(nullptr);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setColorFilter(TestColorFilter1);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setColorFilter(TestColorFilter2);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setColorFilter(nullptr);}}, } }, { "SetPathEffect", { - {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setPathEffect(nullptr);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setPathEffect(TestPathEffect1);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setPathEffect(TestPathEffect2);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setPathEffect(nullptr);}}, } }, { "SetMaskFilter", { - {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setMaskFilter(nullptr);}}, {0, 16, 0, 0, [](DisplayListBuilder& b) {b.setMaskFilter(TestMaskFilter);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setMaskBlurFilter(kNormal_SkBlurStyle, 3.0);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setMaskBlurFilter(kNormal_SkBlurStyle, 5.0);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setMaskBlurFilter(kSolid_SkBlurStyle, 3.0);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setMaskBlurFilter(kInner_SkBlurStyle, 3.0);}}, {0, 8, 0, 0, [](DisplayListBuilder& b) {b.setMaskBlurFilter(kOuter_SkBlurStyle, 3.0);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.setMaskFilter(nullptr);}}, } }, { "Save(Layer)+Restore", { @@ -376,33 +378,34 @@ std::vector allGroups = { }, { "Translate", { // cv.translate(0, 0) is ignored - {1, 16, 0, 0, [](DisplayListBuilder& b) {b.translate(0, 0);}}, {1, 16, 1, 16, [](DisplayListBuilder& b) {b.translate(10, 10);}}, {1, 16, 1, 16, [](DisplayListBuilder& b) {b.translate(10, 15);}}, {1, 16, 1, 16, [](DisplayListBuilder& b) {b.translate(15, 10);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.translate(0, 0);}}, } }, { "Scale", { // cv.scale(1, 1) is ignored - {1, 16, 0, 0, [](DisplayListBuilder& b) {b.scale(1, 1);}}, {1, 16, 1, 16, [](DisplayListBuilder& b) {b.scale(2, 2);}}, {1, 16, 1, 16, [](DisplayListBuilder& b) {b.scale(2, 3);}}, {1, 16, 1, 16, [](DisplayListBuilder& b) {b.scale(3, 2);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.scale(1, 1);}}, } }, { "Rotate", { // cv.rotate(0) is ignored, otherwise expressed as concat(rotmatrix) - {1, 8, 0, 0, [](DisplayListBuilder& b) {b.rotate(0);}}, {1, 8, 1, 32, [](DisplayListBuilder& b) {b.rotate(30);}}, {1, 8, 1, 32, [](DisplayListBuilder& b) {b.rotate(45);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.rotate(0);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.rotate(360);}}, } }, { "Skew", { // cv.skew(0, 0) is ignored, otherwise expressed as concat(skewmatrix) - {1, 16, 0, 0, [](DisplayListBuilder& b) {b.skew(0, 0);}}, {1, 16, 1, 32, [](DisplayListBuilder& b) {b.skew(0.1, 0.1);}}, {1, 16, 1, 32, [](DisplayListBuilder& b) {b.skew(0.1, 0.2);}}, {1, 16, 1, 32, [](DisplayListBuilder& b) {b.skew(0.2, 0.1);}}, + {0, 0, 0, 0, [](DisplayListBuilder& b) {b.skew(0, 0);}}, } }, { "Transform2DAffine", { @@ -833,7 +836,9 @@ TEST(DisplayList, SingleOpDisplayListsCompareToEachOther) { sk_sp listB = listsB[j]; auto desc = group.op_name + "(variant " + std::to_string(i + 1) + " ==? variant " + std::to_string(j + 1) + ")"; - if (i == j) { + if (i == j || + (group.variants[i].is_empty() && group.variants[j].is_empty())) { + // They are the same variant, or both variants are NOPs ASSERT_EQ(listA->op_count(false), listB->op_count(false)) << desc; ASSERT_EQ(listA->bytes(false), listB->bytes(false)) << desc; ASSERT_EQ(listA->op_count(true), listB->op_count(true)) << desc; @@ -852,6 +857,31 @@ TEST(DisplayList, SingleOpDisplayListsCompareToEachOther) { } } +TEST(DisplayList, FullRotationsAreNop) { + DisplayListBuilder builder; + builder.rotate(0); + builder.rotate(360); + builder.rotate(720); + builder.rotate(1080); + builder.rotate(1440); + sk_sp dl = builder.Build(); + ASSERT_EQ(dl->bytes(false), sizeof(DisplayList)); + ASSERT_EQ(dl->bytes(true), sizeof(DisplayList)); + ASSERT_EQ(dl->op_count(false), 0); + ASSERT_EQ(dl->op_count(true), 0); +} + +TEST(DisplayList, AllBlendModeNops) { + DisplayListBuilder builder; + builder.setBlendMode(SkBlendMode::kSrcOver); + builder.setBlender(nullptr); + sk_sp dl = builder.Build(); + ASSERT_EQ(dl->bytes(false), sizeof(DisplayList)); + ASSERT_EQ(dl->bytes(true), sizeof(DisplayList)); + ASSERT_EQ(dl->op_count(false), 0); + ASSERT_EQ(dl->op_count(true), 0); +} + static sk_sp Build(size_t g_index, size_t v_index) { DisplayListBuilder builder; int op_count = 0; @@ -859,8 +889,9 @@ static sk_sp Build(size_t g_index, size_t v_index) { for (size_t i = 0; i < allGroups.size(); i++) { DisplayListInvocationGroup& group = allGroups[i]; size_t j = (i == g_index ? v_index : 0); - if (j >= group.variants.size()) + if (j >= group.variants.size()) { continue; + } DisplayListInvocation& invocation = group.variants[j]; op_count += invocation.op_count(); byte_count += invocation.raw_byte_count(); @@ -872,7 +903,7 @@ static sk_sp Build(size_t g_index, size_t v_index) { name = "Default"; } else { name = allGroups[g_index].op_name; - if (v_index < 0) { + if (v_index >= allGroups[g_index].variants.size()) { name += " skipped"; } else { name += " variant " + std::to_string(v_index + 1); @@ -889,12 +920,12 @@ TEST(DisplayList, DisplayListsWithVaryingOpComparisons) { for (size_t gi = 0; gi < allGroups.size(); gi++) { DisplayListInvocationGroup& group = allGroups[gi]; sk_sp missing_dl = Build(gi, group.variants.size()); - auto desc = "[Group " + std::to_string(gi + 1) + " omitted]"; + auto desc = "[Group " + group.op_name + " omitted]"; ASSERT_TRUE(missing_dl->Equals(*missing_dl)) << desc << " == itself"; ASSERT_FALSE(missing_dl->Equals(*default_dl)) << desc << " != Default"; ASSERT_FALSE(default_dl->Equals(*missing_dl)) << "Default != " << desc; for (size_t vi = 0; vi < group.variants.size(); vi++) { - auto desc = "[Group " + std::to_string(gi + 1) + " variant " + + auto desc = "[Group " + group.op_name + " variant " + std::to_string(vi + 1) + "]"; sk_sp variant_dl = Build(gi, vi); ASSERT_TRUE(variant_dl->Equals(*variant_dl)) << desc << " == itself"; @@ -1072,5 +1103,299 @@ TEST(DisplayList, DisplayListSaveLayerBoundsWithAlphaFilter) { } } +TEST(DisplayList, NestedOpCountMetricsSameAsSkPicture) { + SkPictureRecorder recorder; + recorder.beginRecording(SkRect::MakeWH(150, 100)); + SkCanvas* canvas = recorder.getRecordingCanvas(); + SkPaint paint; + for (int y = 10; y <= 60; y += 10) { + for (int x = 10; x <= 60; x += 10) { + paint.setColor(((x + y) % 20) == 10 ? SK_ColorRED : SK_ColorBLUE); + canvas->drawRect(SkRect::MakeXYWH(x, y, 80, 80), paint); + } + } + SkPictureRecorder outer_recorder; + outer_recorder.beginRecording(SkRect::MakeWH(150, 100)); + canvas = outer_recorder.getRecordingCanvas(); + canvas->drawPicture(recorder.finishRecordingAsPicture()); + + auto picture = outer_recorder.finishRecordingAsPicture(); + ASSERT_EQ(picture->approximateOpCount(), 1); + ASSERT_EQ(picture->approximateOpCount(true), 36); + + DisplayListBuilder builder(SkRect::MakeWH(150, 100)); + for (int y = 10; y <= 60; y += 10) { + for (int x = 10; x <= 60; x += 10) { + builder.setColor(((x + y) % 20) == 10 ? SK_ColorRED : SK_ColorBLUE); + builder.drawRect(SkRect::MakeXYWH(x, y, 80, 80)); + } + } + DisplayListBuilder outer_builder(SkRect::MakeWH(150, 100)); + outer_builder.drawDisplayList(builder.Build()); + + auto display_list = outer_builder.Build(); + ASSERT_EQ(display_list->op_count(), 1); + ASSERT_EQ(display_list->op_count(true), 36); + + ASSERT_EQ(picture->approximateOpCount(), display_list->op_count()); + ASSERT_EQ(picture->approximateOpCount(true), display_list->op_count(true)); + + DisplayListCanvasRecorder dl_recorder(SkRect::MakeWH(150, 100)); + picture->playback(&dl_recorder); + + auto sk_display_list = dl_recorder.Build(); + ASSERT_EQ(display_list->op_count(), 1); + ASSERT_EQ(display_list->op_count(true), 36); +} + +class AttributeRefTester { + public: + virtual void setRefToPaint(SkPaint& paint) const = 0; + virtual void setRefToDisplayList(DisplayListBuilder& builder) const = 0; + virtual bool ref_is_unique() const = 0; + + void testDisplayList() { + { + DisplayListBuilder builder; + setRefToDisplayList(builder); + builder.drawRect(SkRect::MakeLTRB(50, 50, 100, 100)); + ASSERT_FALSE(ref_is_unique()); + } + ASSERT_TRUE(ref_is_unique()); + } + void testPaint() { + { + SkPaint paint; + setRefToPaint(paint); + ASSERT_FALSE(ref_is_unique()); + } + ASSERT_TRUE(ref_is_unique()); + } + void testCanvasRecorder() { + { + sk_sp display_list; + { + DisplayListCanvasRecorder recorder(SkRect::MakeLTRB(0, 0, 200, 200)); + { + { + SkPaint paint; + setRefToPaint(paint); + recorder.drawRect(SkRect::MakeLTRB(50, 50, 100, 100), paint); + ASSERT_FALSE(ref_is_unique()); + } + ASSERT_FALSE(ref_is_unique()); + } + display_list = recorder.Build(); + ASSERT_FALSE(ref_is_unique()); + } + ASSERT_FALSE(ref_is_unique()); + } + ASSERT_TRUE(ref_is_unique()); + } + + void test() { + testDisplayList(); + testPaint(); + testCanvasRecorder(); + } +}; + +TEST(DisplayList, DisplayListImageFilterRefHandling) { + class ImageFilterRefTester : public virtual AttributeRefTester { + public: + void setRefToPaint(SkPaint& paint) const override { + paint.setImageFilter(image_filter); + } + void setRefToDisplayList(DisplayListBuilder& builder) const override { + builder.setImageFilter(image_filter); + } + bool ref_is_unique() const override { return image_filter->unique(); } + + private: + sk_sp image_filter = SkImageFilters::Blur(2.0, 2.0, nullptr); + }; + + ImageFilterRefTester tester; + tester.test(); + ASSERT_TRUE(tester.ref_is_unique()); +} + +TEST(DisplayList, DisplayListColorFilterRefHandling) { + class ColorFilterRefTester : public virtual AttributeRefTester { + public: + void setRefToPaint(SkPaint& paint) const override { + paint.setColorFilter(color_filter); + } + void setRefToDisplayList(DisplayListBuilder& builder) const override { + builder.setColorFilter(color_filter); + } + bool ref_is_unique() const override { return color_filter->unique(); } + + private: + sk_sp color_filter = + SkColorFilters::Blend(SK_ColorBLUE, SkBlendMode::kSrcIn); + }; + + ColorFilterRefTester tester; + tester.test(); + ASSERT_TRUE(tester.ref_is_unique()); +} + +TEST(DisplayList, DisplayListMaskFilterRefHandling) { + class MaskFilterRefTester : public virtual AttributeRefTester { + public: + void setRefToPaint(SkPaint& paint) const override { + paint.setMaskFilter(mask_filter); + } + void setRefToDisplayList(DisplayListBuilder& builder) const override { + builder.setMaskFilter(mask_filter); + } + bool ref_is_unique() const override { return mask_filter->unique(); } + + private: + sk_sp mask_filter = + SkMaskFilter::MakeBlur(SkBlurStyle::kNormal_SkBlurStyle, 2.0); + }; + + MaskFilterRefTester tester; + tester.test(); + ASSERT_TRUE(tester.ref_is_unique()); +} + +TEST(DisplayList, DisplayListBlenderRefHandling) { + class BlenderRefTester : public virtual AttributeRefTester { + public: + void setRefToPaint(SkPaint& paint) const override { + paint.setBlender(blender); + } + void setRefToDisplayList(DisplayListBuilder& builder) const override { + builder.setBlender(blender); + } + bool ref_is_unique() const override { return blender->unique(); } + + private: + sk_sp blender = + SkBlenders::Arithmetic(0.25, 0.25, 0.25, 0.25, true); + }; + + BlenderRefTester tester; + tester.test(); + ASSERT_TRUE(tester.ref_is_unique()); +} + +TEST(DisplayList, DisplayListShaderRefHandling) { + class ShaderRefTester : public virtual AttributeRefTester { + public: + void setRefToPaint(SkPaint& paint) const override { + paint.setShader(shader); + } + void setRefToDisplayList(DisplayListBuilder& builder) const override { + builder.setShader(shader); + } + bool ref_is_unique() const override { return shader->unique(); } + + private: + sk_sp shader = SkShaders::Color(SK_ColorBLUE); + }; + + ShaderRefTester tester; + tester.test(); + ASSERT_TRUE(tester.ref_is_unique()); +} + +TEST(DisplayList, DisplayListPathEffectRefHandling) { + class PathEffectRefTester : public virtual AttributeRefTester { + public: + void setRefToPaint(SkPaint& paint) const override { + paint.setPathEffect(path_effect); + } + void setRefToDisplayList(DisplayListBuilder& builder) const override { + builder.setPathEffect(path_effect); + } + bool ref_is_unique() const override { return path_effect->unique(); } + + private: + sk_sp path_effect = + SkDashPathEffect::Make(TestDashes1, 2, 0.0); + }; + + PathEffectRefTester tester; + tester.test(); + ASSERT_TRUE(tester.ref_is_unique()); +} + +TEST(DisplayList, DisplayListFullPerspectiveTransformHandling) { + // SkM44 constructor takes row-major order + SkM44 sk_matrix = SkM44( + // clang-format off + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16 + // clang-format on + ); + + { // First test == + DisplayListBuilder builder; + // builder.transformFullPerspective takes row-major order + builder.transformFullPerspective( + // clang-format off + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16 + // clang-format on + ); + sk_sp display_list = builder.Build(); + sk_sp surface = SkSurface::MakeRasterN32Premul(10, 10); + SkCanvas* canvas = surface->getCanvas(); + display_list->RenderTo(canvas); + SkM44 dl_matrix = canvas->getLocalToDevice(); + ASSERT_EQ(sk_matrix, dl_matrix); + } + { // Next test != + DisplayListBuilder builder; + // builder.transformFullPerspective takes row-major order + builder.transformFullPerspective( + // clang-format off + 1, 5, 9, 13, + 2, 6, 7, 11, + 3, 7, 11, 15, + 4, 8, 12, 16 + // clang-format on + ); + sk_sp display_list = builder.Build(); + sk_sp surface = SkSurface::MakeRasterN32Premul(10, 10); + SkCanvas* canvas = surface->getCanvas(); + display_list->RenderTo(canvas); + SkM44 dl_matrix = canvas->getLocalToDevice(); + ASSERT_NE(sk_matrix, dl_matrix); + } +} + +TEST(DisplayList, SetMaskBlurSigmaZeroResetsMaskFilter) { + DisplayListBuilder builder; + builder.setMaskBlurFilter(SkBlurStyle::kNormal_SkBlurStyle, 2.0); + builder.drawRect({10, 10, 20, 20}); + builder.setMaskBlurFilter(SkBlurStyle::kNormal_SkBlurStyle, 0.0); + EXPECT_EQ(builder.getMaskFilter(), nullptr); + builder.drawRect({30, 30, 40, 40}); + sk_sp display_list = builder.Build(); + ASSERT_EQ(display_list->op_count(), 2); + ASSERT_EQ(display_list->bytes(), sizeof(DisplayList) + 8u + 24u + 8u + 24u); +} + +TEST(DisplayList, SetMaskFilterNullResetsMaskFilter) { + DisplayListBuilder builder; + builder.setMaskBlurFilter(SkBlurStyle::kNormal_SkBlurStyle, 2.0); + builder.drawRect({10, 10, 20, 20}); + builder.setMaskFilter(nullptr); + EXPECT_EQ(builder.getMaskFilter(), nullptr); + builder.drawRect({30, 30, 40, 40}); + sk_sp display_list = builder.Build(); + ASSERT_EQ(display_list->op_count(), 2); + ASSERT_EQ(display_list->bytes(), sizeof(DisplayList) + 8u + 24u + 8u + 24u); +} + } // namespace testing } // namespace flutter diff --git a/flow/display_list_utils.cc b/flow/display_list_utils.cc index b6d86fa533894..5532390cfb575 100644 --- a/flow/display_list_utils.cc +++ b/flow/display_list_utils.cc @@ -214,7 +214,7 @@ void ClipBoundsDispatchHelper::restore() { } } void ClipBoundsDispatchHelper::reset(const SkRect* cull_rect) { - if ((has_clip_ = ((bool)cull_rect)) && !cull_rect->isEmpty()) { + if ((has_clip_ = cull_rect != nullptr) && !cull_rect->isEmpty()) { bounds_ = *cull_rect; } else { bounds_.setEmpty(); @@ -234,8 +234,7 @@ void DisplayListBoundsCalculator::setStrokeJoin(SkPaint::Join join) { join_is_miter_ = (join == SkPaint::kMiter_Join); } void DisplayListBoundsCalculator::setStyle(SkPaint::Style style) { - style_flag_ = (style == SkPaint::kFill_Style) ? kIsFilledGeometry // - : kIsStrokedGeometry; + style_ = style; } void DisplayListBoundsCalculator::setStrokeWidth(SkScalar width) { half_stroke_width_ = std::max(width * 0.5f, kMinStrokeWidth); @@ -290,7 +289,9 @@ void DisplayListBoundsCalculator::saveLayer(const SkRect* bounds, // Accumulate the layer in its own coordinate system and then // filter and transform its bounds on restore. SkMatrixDispatchHelper::reset(); - ClipBoundsDispatchHelper::reset(bounds); + if (bounds) { + clipRect(*bounds, SkClipOp::kIntersect, false); + } } void DisplayListBoundsCalculator::restore() { if (layer_infos_.size() > 1) { @@ -310,7 +311,7 @@ void DisplayListBoundsCalculator::restore() { // modifications based on the attributes that were in place // when it was instantiated. Modifying it further base on the // current attributes would mix attribute states. - AccumulateRect(layer_bounds, kIsUnfiltered); + AccumulateRect(layer_bounds, kSaveLayerFlags); } if (layer_unbounded) { AccumulateUnbounded(); @@ -327,39 +328,35 @@ void DisplayListBoundsCalculator::drawColor(SkColor color, SkBlendMode mode) { void DisplayListBoundsCalculator::drawLine(const SkPoint& p0, const SkPoint& p1) { SkRect bounds = SkRect::MakeLTRB(p0.fX, p0.fY, p1.fX, p1.fY).makeSorted(); - int cap_flag = kIsStrokedGeometry; - if (bounds.width() > 0.0f && bounds.height() > 0.0f) { - cap_flag |= kGeometryMayHaveDiagonalEndCaps; - } - AccumulateRect(bounds, cap_flag); + DisplayListAttributeFlags flags = + (bounds.width() > 0.0f && bounds.height() > 0.0f) ? kDrawLineFlags + : kDrawHVLineFlags; + AccumulateRect(bounds, flags); } void DisplayListBoundsCalculator::drawRect(const SkRect& rect) { - AccumulateRect(rect, kIsDrawnGeometry); + AccumulateRect(rect, kDrawRectFlags); } void DisplayListBoundsCalculator::drawOval(const SkRect& bounds) { - AccumulateRect(bounds, kIsDrawnGeometry); + AccumulateRect(bounds, kDrawOvalFlags); } void DisplayListBoundsCalculator::drawCircle(const SkPoint& center, SkScalar radius) { AccumulateRect(SkRect::MakeLTRB(center.fX - radius, center.fY - radius, center.fX + radius, center.fY + radius), - kIsDrawnGeometry); + kDrawCircleFlags); } void DisplayListBoundsCalculator::drawRRect(const SkRRect& rrect) { - AccumulateRect(rrect.getBounds(), kIsDrawnGeometry); + AccumulateRect(rrect.getBounds(), kDrawRRectFlags); } void DisplayListBoundsCalculator::drawDRRect(const SkRRect& outer, const SkRRect& inner) { - AccumulateRect(outer.getBounds(), kIsDrawnGeometry); + AccumulateRect(outer.getBounds(), kDrawDRRectFlags); } void DisplayListBoundsCalculator::drawPath(const SkPath& path) { if (path.isInverseFillType()) { AccumulateUnbounded(); } else { - AccumulateRect(path.getBounds(), // - (kIsDrawnGeometry | // - kGeometryMayHaveDiagonalEndCaps | // - kGeometryMayHaveProblematicJoins)); + AccumulateRect(path.getBounds(), kDrawPathFlags); } } void DisplayListBoundsCalculator::drawArc(const SkRect& bounds, @@ -369,7 +366,10 @@ void DisplayListBoundsCalculator::drawArc(const SkRect& bounds, // This could be tighter if we compute where the start and end // angles are and then also consider the quadrants swept and // the center if specified. - AccumulateRect(bounds, kIsDrawnGeometry | kGeometryMayHaveDiagonalEndCaps); + AccumulateRect(bounds, + useCenter // + ? kDrawArcWithCenterFlags + : kDrawArcNoCenterFlags); } void DisplayListBoundsCalculator::drawPoints(SkCanvas::PointMode mode, uint32_t count, @@ -379,17 +379,23 @@ void DisplayListBoundsCalculator::drawPoints(SkCanvas::PointMode mode, for (size_t i = 0; i < count; i++) { ptBounds.accumulate(pts[i]); } - int flags = kIsStrokedGeometry; - if (mode != SkCanvas::kPoints_PointMode) { - flags |= kGeometryMayHaveDiagonalEndCaps; - // Even Polygon mode just draws (count-1) separate lines, no joins + SkRect point_bounds = ptBounds.bounds(); + switch (mode) { + case SkCanvas::kPoints_PointMode: + AccumulateRect(point_bounds, kDrawPointsAsPointsFlags); + break; + case SkCanvas::kLines_PointMode: + AccumulateRect(point_bounds, kDrawPointsAsLinesFlags); + break; + case SkCanvas::kPolygon_PointMode: + AccumulateRect(point_bounds, kDrawPointsAsPolygonFlags); + break; } - AccumulateRect(ptBounds.bounds(), flags); } } void DisplayListBoundsCalculator::drawVertices(const sk_sp vertices, SkBlendMode mode) { - AccumulateRect(vertices->bounds(), kIsNonGeometric); + AccumulateRect(vertices->bounds(), kDrawVerticesFlags); } void DisplayListBoundsCalculator::drawImage(const sk_sp image, const SkPoint point, @@ -397,8 +403,9 @@ void DisplayListBoundsCalculator::drawImage(const sk_sp image, bool render_with_attributes) { SkRect bounds = SkRect::MakeXYWH(point.fX, point.fY, // image->width(), image->height()); - int flags = render_with_attributes ? kIsNonGeometric | kApplyMaskFilter - : kIsUnfiltered; + DisplayListAttributeFlags flags = render_with_attributes // + ? kDrawImageWithPaintFlags + : kDrawImageFlags; AccumulateRect(bounds, flags); } void DisplayListBoundsCalculator::drawImageRect( @@ -408,8 +415,9 @@ void DisplayListBoundsCalculator::drawImageRect( const SkSamplingOptions& sampling, bool render_with_attributes, SkCanvas::SrcRectConstraint constraint) { - int flags = render_with_attributes ? kIsNonGeometric | kApplyMaskFilter - : kIsUnfiltered; + DisplayListAttributeFlags flags = render_with_attributes + ? kDrawImageRectWithPaintFlags + : kDrawImageRectFlags; AccumulateRect(dst, flags); } void DisplayListBoundsCalculator::drawImageNine(const sk_sp image, @@ -417,7 +425,10 @@ void DisplayListBoundsCalculator::drawImageNine(const sk_sp image, const SkRect& dst, SkFilterMode filter, bool render_with_attributes) { - AccumulateRect(dst, render_with_attributes ? kIsNonGeometric : kIsUnfiltered); + DisplayListAttributeFlags flags = render_with_attributes + ? kDrawImageNineWithPaintFlags + : kDrawImageNineFlags; + AccumulateRect(dst, flags); } void DisplayListBoundsCalculator::drawImageLattice( const sk_sp image, @@ -425,8 +436,9 @@ void DisplayListBoundsCalculator::drawImageLattice( const SkRect& dst, SkFilterMode filter, bool render_with_attributes) { - int flags = render_with_attributes ? kIsNonGeometric | kApplyMaskFilter - : kIsUnfiltered; + DisplayListAttributeFlags flags = render_with_attributes + ? kDrawImageLatticeWithPaintFlags + : kDrawImageLatticeFlags; AccumulateRect(dst, flags); } void DisplayListBoundsCalculator::drawAtlas(const sk_sp atlas, @@ -448,7 +460,9 @@ void DisplayListBoundsCalculator::drawAtlas(const sk_sp atlas, } } if (atlasBounds.is_not_empty()) { - int flags = render_with_attributes ? kIsNonGeometric : kIsUnfiltered; + DisplayListAttributeFlags flags = render_with_attributes // + ? kDrawAtlasWithPaintFlags + : kDrawAtlasFlags; AccumulateRect(atlasBounds.bounds(), flags); } } @@ -462,17 +476,19 @@ void DisplayListBoundsCalculator::drawPicture(const sk_sp picture, if (pic_matrix) { pic_matrix->mapRect(&bounds); } - AccumulateRect(bounds, - render_with_attributes ? kIsNonGeometric : kIsUnfiltered); + DisplayListAttributeFlags flags = render_with_attributes // + ? kDrawPictureWithPaintFlags + : kDrawPictureFlags; + AccumulateRect(bounds, flags); } void DisplayListBoundsCalculator::drawDisplayList( const sk_sp display_list) { - AccumulateRect(display_list->bounds(), kIsUnfiltered); + AccumulateRect(display_list->bounds(), kDrawDisplayListFlags); } void DisplayListBoundsCalculator::drawTextBlob(const sk_sp blob, SkScalar x, SkScalar y) { - AccumulateRect(blob->bounds().makeOffset(x, y), kIsFilledGeometry); + AccumulateRect(blob->bounds().makeOffset(x, y), kDrawTextBlobFlags); } void DisplayListBoundsCalculator::drawShadow(const SkPath& path, const SkColor color, @@ -481,7 +497,7 @@ void DisplayListBoundsCalculator::drawShadow(const SkPath& path, SkScalar dpr) { SkRect shadow_bounds = PhysicalShapeLayer::ComputeShadowBounds(path, elevation, dpr, matrix()); - AccumulateRect(shadow_bounds, kIsUnfiltered); + AccumulateRect(shadow_bounds, kDrawShadowFlags); } bool DisplayListBoundsCalculator::ComputeFilteredBounds(SkRect& bounds, @@ -495,64 +511,41 @@ bool DisplayListBoundsCalculator::ComputeFilteredBounds(SkRect& bounds, return true; } -bool DisplayListBoundsCalculator::AdjustBoundsForPaint(SkRect& bounds, - int flags) { - if ((flags & kIsUnfiltered) != 0) { - FML_DCHECK(flags == kIsUnfiltered); +bool DisplayListBoundsCalculator::AdjustBoundsForPaint( + SkRect& bounds, + DisplayListAttributeFlags flags) { + if (flags.ignores_paint()) { return true; } - if ((flags & kIsAnyGeometryMask) != 0) { - if ((flags & kIsDrawnGeometry) != 0) { - FML_DCHECK((flags & (kIsFilledGeometry | kIsStrokedGeometry)) == 0); - flags |= style_flag_; - } - + if (flags.is_geometric()) { // Path effect occurs before stroking... + DisplayListSpecialGeometryFlags special_flags = + flags.WithPathEffect(path_effect_); if (path_effect_) { - SkPathEffect::DashInfo info; - if (path_effect_->asADash(&info) == SkPathEffect::kDash_DashType) { - // A dash effect has a very simple impact. It cannot introduce any - // miter joins that weren't already present in the original path - // and it does not grow the bounds of the path, but it can add - // end caps to areas that might not have had them before so all - // we need to do is to indicate the potential for diagonal - // end caps and move on. - flags |= kGeometryMayHaveDiagonalEndCaps; - } else { - SkPaint p; - p.setPathEffect(path_effect_); - if (!p.canComputeFastBounds()) { - return false; - } - bounds = p.computeFastBounds(bounds, &bounds); - flags |= (kGeometryMayHaveDiagonalEndCaps | - kGeometryMayHaveProblematicJoins); + SkPaint p; + p.setPathEffect(path_effect_); + if (!p.canComputeFastBounds()) { + return false; } + bounds = p.computeFastBounds(bounds, &bounds); } - if ((flags & kIsStrokedGeometry) != 0) { - FML_DCHECK((flags & kIsFilledGeometry) == 0); + if (flags.is_stroked(style_)) { // Determine the max multiplier to the stroke width first. SkScalar pad = 1.0f; - if (join_is_miter_ && (flags & kGeometryMayHaveProblematicJoins) != 0) { + if (join_is_miter_ && special_flags.may_have_acute_joins()) { pad = std::max(pad, miter_limit_); } - if (cap_is_square_ && (flags & kGeometryMayHaveDiagonalEndCaps) != 0) { + if (cap_is_square_ && special_flags.may_have_diagonal_caps()) { pad = std::max(pad, SK_ScalarSqrt2); } pad *= half_stroke_width_; bounds.outset(pad, pad); - } else { - FML_DCHECK((flags & kIsStrokedGeometry) == 0); } - flags |= kApplyMaskFilter; - } else { - FML_DCHECK((flags & (kGeometryMayHaveDiagonalEndCaps | - kGeometryMayHaveProblematicJoins)) == 0); } - if ((flags & kApplyMaskFilter) != 0) { + if (flags.applies_mask_filter()) { if (mask_filter_) { SkPaint p; p.setMaskFilter(mask_filter_); @@ -566,7 +559,11 @@ bool DisplayListBoundsCalculator::AdjustBoundsForPaint(SkRect& bounds, } } - return ComputeFilteredBounds(bounds, image_filter_.get()); + if (flags.applies_image_filter()) { + return ComputeFilteredBounds(bounds, image_filter_.get()); + } + + return true; } void DisplayListBoundsCalculator::AccumulateUnbounded() { @@ -576,7 +573,9 @@ void DisplayListBoundsCalculator::AccumulateUnbounded() { layer_infos_.back()->set_unbounded(); } } -void DisplayListBoundsCalculator::AccumulateRect(SkRect& rect, int flags) { +void DisplayListBoundsCalculator::AccumulateRect( + SkRect& rect, + DisplayListAttributeFlags flags) { if (AdjustBoundsForPaint(rect, flags)) { matrix().mapRect(&rect); if (!has_clip() || rect.intersect(clip_bounds())) { diff --git a/flow/display_list_utils.h b/flow/display_list_utils.h index 495a62f294fc7..a8d0d6f88cbc4 100644 --- a/flow/display_list_utils.h +++ b/flow/display_list_utils.h @@ -65,9 +65,9 @@ class IgnoreAttributeDispatchHelper : public virtual Dispatcher { // A utility class that will ignore all Dispatcher methods relating // to setting a clip. class IgnoreClipDispatchHelper : public virtual Dispatcher { - void clipRect(const SkRect& rect, SkClipOp clip_op, bool isAA) override {} - void clipRRect(const SkRRect& rrect, SkClipOp clip_op, bool isAA) override {} - void clipPath(const SkPath& path, SkClipOp clip_op, bool isAA) override {} + void clipRect(const SkRect& rect, SkClipOp clip_op, bool is_aa) override {} + void clipRRect(const SkRRect& rrect, SkClipOp clip_op, bool is_aa) override {} + void clipPath(const SkPath& path, SkClipOp clip_op, bool is_aa) override {} }; // A utility class that will ignore all Dispatcher methods relating @@ -199,7 +199,7 @@ class ClipBoundsDispatchHelper : public virtual Dispatcher, public: ClipBoundsDispatchHelper() : ClipBoundsDispatchHelper(nullptr) {} - ClipBoundsDispatchHelper(const SkRect* cull_rect) + explicit ClipBoundsDispatchHelper(const SkRect* cull_rect) : has_clip_(cull_rect), bounds_(cull_rect && !cull_rect->isEmpty() ? *cull_rect : SkRect::MakeEmpty()) {} @@ -267,7 +267,8 @@ class DisplayListBoundsCalculator final : public virtual Dispatcher, public virtual IgnoreAttributeDispatchHelper, public virtual SkMatrixDispatchHelper, - public virtual ClipBoundsDispatchHelper { + public virtual ClipBoundsDispatchHelper, + DisplayListOpFlags { public: // Construct a Calculator to determine the bounds of a list of // DisplayList dispatcher method calls. Since 2 of the method calls @@ -278,7 +279,7 @@ class DisplayListBoundsCalculator final // queried using |isUnbounded| if an alternate plan is available // for such cases. // The flag should never be set if a cull_rect is provided. - DisplayListBoundsCalculator(const SkRect* cull_rect = nullptr); + explicit DisplayListBoundsCalculator(const SkRect* cull_rect = nullptr); void setStrokeCap(SkPaint::Cap cap) override; void setStrokeJoin(SkPaint::Join join) override; @@ -402,7 +403,8 @@ class DisplayListBoundsCalculator final // the stack. // Some layers may substitute their own accumulator to compute // their own local bounds while they are on the stack. - LayerData(BoundsAccumulator* outer) : outer_(outer), is_unbounded_(false) {} + explicit LayerData(BoundsAccumulator* outer) + : outer_(outer), is_unbounded_(false) {} virtual ~LayerData() = default; // The accumulator to use while this layer is put in play by @@ -531,61 +533,6 @@ class DisplayListBoundsCalculator final std::vector> layer_infos_; - // A drawing operation that is not geometric in nature (but which - // may still apply a MaskFilter - see |kApplyMaskFilter| below). - static constexpr int kIsNonGeometric = 0x00; - - // A geometric operation that is defined as a fill operation - // regardless of what the current paint Style is set to. - // This flag will automatically assume |kApplyMaskFilter|. - static constexpr int kIsFilledGeometry = 0x01; - - // A geometric operation that is defined as a stroke operation - // regardless of what the current paint Style is set to. - // This flag will automatically assume |kApplyMaskFilter|. - static constexpr int kIsStrokedGeometry = 0x02; - - // A geometric operation that may be a stroke or fill operation - // depending on the current state of the paint Style attribute. - // This flag will automatically assume |kApplyMaskFilter|. - static constexpr int kIsDrawnGeometry = 0x04; - - static constexpr int kIsAnyGeometryMask = // - kIsFilledGeometry | // - kIsStrokedGeometry | // - kIsDrawnGeometry; - - // A geometric operation which has a path that might have - // end caps that are not rectilinear which means that square - // end caps might project further than half the stroke width - // from the geometry bounds. - // A rectilinear path such as |drawRect| will not have - // diagonal end caps. |drawLine| might have diagonal end - // caps depending on the angle of the line, and more likely - // |drawPath| will often have such end caps. - static constexpr int kGeometryMayHaveDiagonalEndCaps = 0x08; - - // A geometric operation which has joined vertices that are - // not guaranteed to be smooth (angles of incoming and outgoing) - // segments at some joins may not have the same angle) or - // rectilinear (squares have right angles at the corners, but - // those corners will never extend past the bounding box of - // the geometry pre-transform). - // |drawRect|, |drawOval| and |drawRRect| all have well - // behaved joins, but |drawPath| might have joins that cause - // mitered extensions outside the pre-transformed bounding box. - static constexpr int kGeometryMayHaveProblematicJoins = 0x10; - - // Some operations are inherently non-geometric and yet have the - // mask filter applied anyway. - // |drawImage| variants behave this way. - static constexpr int kApplyMaskFilter = 0x20; - - // In very rare circumstances the ImageFilter is ignored. - // This is one of the few flags that turns off a step in - // estimating the bounds, rather than turning on any steps. - static constexpr int kIsUnfiltered = 0x40; - static constexpr SkScalar kMinStrokeWidth = 0.01; skstd::optional blend_mode_ = SkBlendMode::kSrcOver; @@ -593,7 +540,7 @@ class DisplayListBoundsCalculator final SkScalar half_stroke_width_ = kMinStrokeWidth; SkScalar miter_limit_ = 4.0; - int style_flag_ = kIsFilledGeometry; + SkPaint::Style style_ = SkPaint::Style::kFill_Style; bool join_is_miter_ = true; bool cap_is_square_ = false; sk_sp image_filter_; @@ -604,14 +551,14 @@ class DisplayListBoundsCalculator final bool paint_nops_on_transparency(); static bool ComputeFilteredBounds(SkRect& rect, SkImageFilter* filter); - bool AdjustBoundsForPaint(SkRect& bounds, int flags); + bool AdjustBoundsForPaint(SkRect& bounds, DisplayListAttributeFlags flags); void AccumulateUnbounded(); - void AccumulateRect(const SkRect& rect, int flags) { + void AccumulateRect(const SkRect& rect, DisplayListAttributeFlags flags) { SkRect bounds = rect; AccumulateRect(bounds, flags); } - void AccumulateRect(SkRect& rect, int flags); + void AccumulateRect(SkRect& rect, DisplayListAttributeFlags flags); }; } // namespace flutter diff --git a/flow/frame_timings.cc b/flow/frame_timings.cc index 6ead668c3656a..d37c22fb1db8f 100644 --- a/flow/frame_timings.cc +++ b/flow/frame_timings.cc @@ -130,28 +130,26 @@ void FrameTimingsRecorder::RecordBuildEnd(fml::TimePoint build_end) { build_end_ = build_end; } -void FrameTimingsRecorder::RecordRasterStart(fml::TimePoint raster_start, - const RasterCache* cache) { +void FrameTimingsRecorder::RecordRasterStart(fml::TimePoint raster_start) { std::scoped_lock state_lock(state_mutex_); FML_DCHECK(state_ == State::kBuildEnd); state_ = State::kRasterStart; raster_start_ = raster_start; - sweep_count_at_raster_start_ = cache ? cache->sweep_count() : -1; } FrameTiming FrameTimingsRecorder::RecordRasterEnd(const RasterCache* cache) { std::scoped_lock state_lock(state_mutex_); FML_DCHECK(state_ == State::kRasterStart); - FML_DCHECK(sweep_count_at_raster_start_ == - (cache ? cache->sweep_count() : -1)); state_ = State::kRasterEnd; raster_end_ = fml::TimePoint::Now(); raster_end_wall_time_ = fml::TimePoint::CurrentWallTime(); if (cache) { - layer_cache_count_ = cache->GetLayerCachedEntriesCount(); - layer_cache_bytes_ = cache->EstimateLayerCacheByteSize(); - picture_cache_count_ = cache->GetPictureCachedEntriesCount(); - picture_cache_bytes_ = cache->EstimatePictureCacheByteSize(); + const RasterCacheMetrics& layer_metrics = cache->layer_metrics(); + const RasterCacheMetrics& picture_metrics = cache->picture_metrics(); + layer_cache_count_ = layer_metrics.total_count(); + layer_cache_bytes_ = layer_metrics.total_bytes(); + picture_cache_count_ = picture_metrics.total_count(); + picture_cache_bytes_ = picture_metrics.total_bytes(); } else { layer_cache_count_ = layer_cache_bytes_ = picture_cache_count_ = picture_cache_bytes_ = 0; @@ -197,7 +195,6 @@ std::unique_ptr FrameTimingsRecorder::CloneUntil( if (state >= State::kRasterStart) { recorder->raster_start_ = raster_start_; - recorder->sweep_count_at_raster_start_ = sweep_count_at_raster_start_; } if (state >= State::kRasterEnd) { diff --git a/flow/frame_timings.h b/flow/frame_timings.h index e79c4d0e5b9ea..8d39bb61457d8 100644 --- a/flow/frame_timings.h +++ b/flow/frame_timings.h @@ -41,7 +41,7 @@ class FrameTimingsRecorder { FrameTimingsRecorder(); /// Constructor with a pre-populated frame number. - FrameTimingsRecorder(uint64_t frame_number); + explicit FrameTimingsRecorder(uint64_t frame_number); ~FrameTimingsRecorder(); @@ -93,8 +93,7 @@ class FrameTimingsRecorder { void RecordBuildEnd(fml::TimePoint build_end); /// Records a raster start event. - void RecordRasterStart(fml::TimePoint raster_start, - const RasterCache* cache = nullptr); + void RecordRasterStart(fml::TimePoint raster_start); /// Clones the recorder until (and including) the specified state. std::unique_ptr CloneUntil(State state); @@ -131,7 +130,6 @@ class FrameTimingsRecorder { fml::TimePoint raster_end_; fml::TimePoint raster_end_wall_time_; - int sweep_count_at_raster_start_; size_t layer_cache_count_; size_t layer_cache_bytes_; size_t picture_cache_count_; diff --git a/flow/frame_timings_recorder_unittests.cc b/flow/frame_timings_recorder_unittests.cc index 525244cb7265f..434f06b802e6f 100644 --- a/flow/frame_timings_recorder_unittests.cc +++ b/flow/frame_timings_recorder_unittests.cc @@ -91,10 +91,10 @@ TEST(FrameTimingsRecorderTest, RecordRasterTimesWithCache) { using namespace std::chrono_literals; MockRasterCache cache(1, 10); - cache.SweepAfterFrame(); + cache.PrepareNewFrame(); const auto raster_start = fml::TimePoint::Now(); - recorder->RecordRasterStart(raster_start, &cache); + recorder->RecordRasterStart(raster_start); cache.AddMockLayer(100, 100); size_t layer_bytes = cache.EstimateLayerCacheByteSize(); @@ -103,6 +103,8 @@ TEST(FrameTimingsRecorderTest, RecordRasterTimesWithCache) { size_t picture_bytes = cache.EstimatePictureCacheByteSize(); EXPECT_GT(picture_bytes, 0u); + cache.CleanupAfterFrame(); + const auto before_raster_end_wall_time = fml::TimePoint::CurrentWallTime(); std::this_thread::sleep_for(1ms); const auto timing = recorder->RecordRasterEnd(&cache); @@ -123,32 +125,6 @@ TEST(FrameTimingsRecorderTest, RecordRasterTimesWithCache) { #if !defined(OS_FUCHSIA) && !defined(OS_WIN) && \ (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG) -TEST(FrameTimingsRecorderTest, ThrowAfterUnexpectedCacheSweep) { - auto recorder = std::make_unique(); - - const auto st = fml::TimePoint::Now(); - const auto en = st + fml::TimeDelta::FromMillisecondsF(16); - recorder->RecordVsync(st, en); - - const auto build_start = fml::TimePoint::Now(); - const auto build_end = build_start + fml::TimeDelta::FromMillisecondsF(16); - recorder->RecordBuildStart(build_start); - recorder->RecordBuildEnd(build_end); - - using namespace std::chrono_literals; - - MockRasterCache cache; - - const auto raster_start = fml::TimePoint::Now(); - recorder->RecordRasterStart(raster_start, &cache); - std::this_thread::sleep_for(1ms); - cache.SweepAfterFrame(); - EXPECT_EXIT(recorder->RecordRasterEnd(&cache), - ::testing::KilledBySignal(SIGABRT), - "Check failed: sweep_count_at_raster_start_ == \\(cache \\? " - "cache->sweep_count\\(\\) : -1\\)."); -} - TEST(FrameTimingsRecorderTest, ThrowWhenRecordBuildBeforeVsync) { auto recorder = std::make_unique(); @@ -276,13 +252,13 @@ TEST(FrameTimingsRecorderTest, ClonedHasSameRasterEnd) { TEST(FrameTimingsRecorderTest, ClonedHasSameRasterEndWithCache) { auto recorder = std::make_unique(); MockRasterCache cache(1, 10); - cache.SweepAfterFrame(); + cache.PrepareNewFrame(); const auto now = fml::TimePoint::Now(); recorder->RecordVsync(now, now + fml::TimeDelta::FromMilliseconds(16)); recorder->RecordBuildStart(fml::TimePoint::Now()); recorder->RecordBuildEnd(fml::TimePoint::Now()); - recorder->RecordRasterStart(fml::TimePoint::Now(), &cache); + recorder->RecordRasterStart(fml::TimePoint::Now()); cache.AddMockLayer(100, 100); size_t layer_bytes = cache.EstimateLayerCacheByteSize(); @@ -291,6 +267,7 @@ TEST(FrameTimingsRecorderTest, ClonedHasSameRasterEndWithCache) { size_t picture_bytes = cache.EstimatePictureCacheByteSize(); EXPECT_GT(picture_bytes, 0u); + cache.CleanupAfterFrame(); recorder->RecordRasterEnd(&cache); auto cloned = recorder->CloneUntil(FrameTimingsRecorder::State::kRasterEnd); diff --git a/flow/instrumentation.h b/flow/instrumentation.h index 49ca887844cec..e38e7118f56f4 100644 --- a/flow/instrumentation.h +++ b/flow/instrumentation.h @@ -16,7 +16,7 @@ namespace flutter { class Stopwatch { public: - Stopwatch(fml::Milliseconds frame_budget = fml::kDefaultFrameBudget); + explicit Stopwatch(fml::Milliseconds frame_budget = fml::kDefaultFrameBudget); ~Stopwatch(); diff --git a/flow/layers/backdrop_filter_layer.cc b/flow/layers/backdrop_filter_layer.cc index 6d3fd7ecc6d8a..548db3227d72e 100644 --- a/flow/layers/backdrop_filter_layer.cc +++ b/flow/layers/backdrop_filter_layer.cc @@ -10,8 +10,6 @@ BackdropFilterLayer::BackdropFilterLayer(sk_sp filter, SkBlendMode blend_mode) : filter_(std::move(filter)), blend_mode_(blend_mode) {} -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void BackdropFilterLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); auto* prev = static_cast(old_layer); @@ -26,24 +24,24 @@ void BackdropFilterLayer::Diff(DiffContext* context, const Layer* old_layer) { auto paint_bounds = context->GetCullRect(); context->AddLayerBounds(paint_bounds); - // convert paint bounds and filter to screen coordinates - context->GetTransform().mapRect(&paint_bounds); - auto input_filter_bounds = paint_bounds.roundOut(); - auto filter = filter_->makeWithLocalMatrix(context->GetTransform()); + if (filter_) { + // convert paint bounds and filter to screen coordinates + context->GetTransform().mapRect(&paint_bounds); + auto input_filter_bounds = paint_bounds.roundOut(); + auto filter = filter_->makeWithLocalMatrix(context->GetTransform()); - auto filter_bounds = // in screen coordinates - filter->filterBounds(input_filter_bounds, SkMatrix::I(), - SkImageFilter::kReverse_MapDirection); + auto filter_bounds = // in screen coordinates + filter->filterBounds(input_filter_bounds, SkMatrix::I(), + SkImageFilter::kReverse_MapDirection); - context->AddReadbackRegion(filter_bounds); + context->AddReadbackRegion(filter_bounds); + } DiffChildren(context, prev); context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void BackdropFilterLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { Layer::AutoPrerollSaveLayerState save = diff --git a/flow/layers/backdrop_filter_layer.h b/flow/layers/backdrop_filter_layer.h index c4f1b33b44384..c8cbeba9215e1 100644 --- a/flow/layers/backdrop_filter_layer.h +++ b/flow/layers/backdrop_filter_layer.h @@ -14,12 +14,8 @@ class BackdropFilterLayer : public ContainerLayer { public: BackdropFilterLayer(sk_sp filter, SkBlendMode blend_mode); -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void Diff(DiffContext* context, const Layer* old_layer) override; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* context, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; diff --git a/flow/layers/backdrop_filter_layer_unittests.cc b/flow/layers/backdrop_filter_layer_unittests.cc index b01137c4b5410..7a5a066fd7344 100644 --- a/flow/layers/backdrop_filter_layer_unittests.cc +++ b/flow/layers/backdrop_filter_layer_unittests.cc @@ -269,8 +269,6 @@ TEST_F(BackdropFilterLayerTest, Readback) { EXPECT_FALSE(preroll_context()->surface_needs_readback); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - using BackdropLayerDiffTest = DiffContextTest; TEST_F(BackdropLayerDiffTest, BackdropLayer) { @@ -329,7 +327,5 @@ TEST_F(BackdropLayerDiffTest, BackdropLayer) { EXPECT_EQ(damage.frame_damage, SkIRect::MakeLTRB(0, 0, 190, 190)); } -#endif - } // namespace testing } // namespace flutter diff --git a/flow/layers/clip_path_layer.cc b/flow/layers/clip_path_layer.cc index c895f6611be1e..ca30baf2ac8cc 100644 --- a/flow/layers/clip_path_layer.cc +++ b/flow/layers/clip_path_layer.cc @@ -12,8 +12,6 @@ ClipPathLayer::ClipPathLayer(const SkPath& clip_path, Clip clip_behavior) FML_DCHECK(clip_behavior != Clip::none); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void ClipPathLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); auto* prev = static_cast(old_layer); @@ -30,8 +28,6 @@ void ClipPathLayer::Diff(DiffContext* context, const Layer* old_layer) { context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void ClipPathLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "ClipPathLayer::Preroll"); @@ -63,6 +59,7 @@ void ClipPathLayer::Paint(PaintContext& context) const { clip_behavior_ != Clip::hardEdge); if (UsesSaveLayer()) { + TRACE_EVENT0("flutter", "Canvas::saveLayer"); context.internal_nodes_canvas->saveLayer(paint_bounds(), nullptr); } PaintChildren(context); diff --git a/flow/layers/clip_path_layer.h b/flow/layers/clip_path_layer.h index bed8c9609f2d5..6e19e0a3741a6 100644 --- a/flow/layers/clip_path_layer.h +++ b/flow/layers/clip_path_layer.h @@ -11,14 +11,11 @@ namespace flutter { class ClipPathLayer : public ContainerLayer { public: - ClipPathLayer(const SkPath& clip_path, Clip clip_behavior = Clip::antiAlias); - -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT + explicit ClipPathLayer(const SkPath& clip_path, + Clip clip_behavior = Clip::antiAlias); void Diff(DiffContext* context, const Layer* old_layer) override; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* context, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; diff --git a/flow/layers/clip_rect_layer.cc b/flow/layers/clip_rect_layer.cc index 3c885a472ce97..8504d41ed15b4 100644 --- a/flow/layers/clip_rect_layer.cc +++ b/flow/layers/clip_rect_layer.cc @@ -12,8 +12,6 @@ ClipRectLayer::ClipRectLayer(const SkRect& clip_rect, Clip clip_behavior) FML_DCHECK(clip_behavior != Clip::none); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void ClipRectLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); auto* prev = static_cast(old_layer); @@ -30,8 +28,6 @@ void ClipRectLayer::Diff(DiffContext* context, const Layer* old_layer) { context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void ClipRectLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "ClipRectLayer::Preroll"); @@ -62,6 +58,7 @@ void ClipRectLayer::Paint(PaintContext& context) const { clip_behavior_ != Clip::hardEdge); if (UsesSaveLayer()) { + TRACE_EVENT0("flutter", "Canvas::saveLayer"); context.internal_nodes_canvas->saveLayer(clip_rect_, nullptr); } PaintChildren(context); diff --git a/flow/layers/clip_rect_layer.h b/flow/layers/clip_rect_layer.h index e37227fbe21b3..6c7c8d2f17209 100644 --- a/flow/layers/clip_rect_layer.h +++ b/flow/layers/clip_rect_layer.h @@ -13,12 +13,8 @@ class ClipRectLayer : public ContainerLayer { public: ClipRectLayer(const SkRect& clip_rect, Clip clip_behavior); -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void Diff(DiffContext* context, const Layer* old_layer) override; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* context, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; diff --git a/flow/layers/clip_rrect_layer.cc b/flow/layers/clip_rrect_layer.cc index 225c110abda04..511b3da5526da 100644 --- a/flow/layers/clip_rrect_layer.cc +++ b/flow/layers/clip_rrect_layer.cc @@ -12,8 +12,6 @@ ClipRRectLayer::ClipRRectLayer(const SkRRect& clip_rrect, Clip clip_behavior) FML_DCHECK(clip_behavior != Clip::none); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void ClipRRectLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); auto* prev = static_cast(old_layer); @@ -30,8 +28,6 @@ void ClipRRectLayer::Diff(DiffContext* context, const Layer* old_layer) { context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void ClipRRectLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "ClipRRectLayer::Preroll"); @@ -63,6 +59,7 @@ void ClipRRectLayer::Paint(PaintContext& context) const { clip_behavior_ != Clip::hardEdge); if (UsesSaveLayer()) { + TRACE_EVENT0("flutter", "Canvas::saveLayer"); context.internal_nodes_canvas->saveLayer(paint_bounds(), nullptr); } PaintChildren(context); diff --git a/flow/layers/clip_rrect_layer.h b/flow/layers/clip_rrect_layer.h index 85626b0d2a9c3..80953b26f567b 100644 --- a/flow/layers/clip_rrect_layer.h +++ b/flow/layers/clip_rrect_layer.h @@ -13,12 +13,8 @@ class ClipRRectLayer : public ContainerLayer { public: ClipRRectLayer(const SkRRect& clip_rrect, Clip clip_behavior); -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void Diff(DiffContext* context, const Layer* old_layer) override; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* context, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; diff --git a/flow/layers/color_filter_layer.cc b/flow/layers/color_filter_layer.cc index 7d662f07f4ab2..bc58805817488 100644 --- a/flow/layers/color_filter_layer.cc +++ b/flow/layers/color_filter_layer.cc @@ -9,8 +9,6 @@ namespace flutter { ColorFilterLayer::ColorFilterLayer(sk_sp filter) : filter_(std::move(filter)) {} -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void ColorFilterLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); auto* prev = static_cast(old_layer); @@ -26,8 +24,6 @@ void ColorFilterLayer::Diff(DiffContext* context, const Layer* old_layer) { context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void ColorFilterLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { Layer::AutoPrerollSaveLayerState save = diff --git a/flow/layers/color_filter_layer.h b/flow/layers/color_filter_layer.h index b1fb32acfbcad..e19d69377a5d5 100644 --- a/flow/layers/color_filter_layer.h +++ b/flow/layers/color_filter_layer.h @@ -12,14 +12,10 @@ namespace flutter { class ColorFilterLayer : public ContainerLayer { public: - ColorFilterLayer(sk_sp filter); - -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT + explicit ColorFilterLayer(sk_sp filter); void Diff(DiffContext* context, const Layer* old_layer) override; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* context, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; diff --git a/flow/layers/container_layer.cc b/flow/layers/container_layer.cc index 2a62359ba4dc8..552d413f26f37 100644 --- a/flow/layers/container_layer.cc +++ b/flow/layers/container_layer.cc @@ -10,8 +10,6 @@ namespace flutter { ContainerLayer::ContainerLayer() {} -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void ContainerLayer::Diff(DiffContext* context, const Layer* old_layer) { auto old_container = static_cast(old_layer); DiffContext::AutoSubtreeRestore subtree(context); @@ -79,7 +77,8 @@ void ContainerLayer::DiffChildren(DiffContext* context, auto layer = layers_[i]; auto prev_layer = prev_layers[i_prev]; auto paint_region = context->GetOldLayerPaintRegion(prev_layer.get()); - if (layer == prev_layer && !paint_region.has_readback()) { + if (layer == prev_layer && !paint_region.has_readback() && + !paint_region.has_texture()) { // for retained layers, stop processing the subtree and add existing // region; We know current subtree is not dirty (every ancestor up to // here matches) so the retained subtree will render identically to @@ -104,8 +103,6 @@ void ContainerLayer::DiffChildren(DiffContext* context, } } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void ContainerLayer::Add(std::shared_ptr layer) { layers_.emplace_back(std::move(layer)); } @@ -175,6 +172,9 @@ void ContainerLayer::TryToPrepareRasterCache(PrerollContext* context, context->raster_cache && SkRect::Intersects(context->cull_rect, layer->paint_bounds())) { context->raster_cache->Prepare(context, layer, matrix); + } else if (context->raster_cache) { + // Don't evict raster cache entry during partial repaint + context->raster_cache->Touch(layer, matrix); } } @@ -191,7 +191,6 @@ MergedContainerLayer::MergedContainerLayer() { ContainerLayer::Add(std::make_shared()); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT void MergedContainerLayer::DiffChildren(DiffContext* context, const ContainerLayer* old_layer) { if (context->IsSubtreeDirty()) { @@ -207,7 +206,6 @@ void MergedContainerLayer::DiffChildren(DiffContext* context, auto layer = static_cast(old_layer); GetChildContainer()->DiffChildren(context, layer->GetChildContainer()); } -#endif void MergedContainerLayer::Add(std::shared_ptr layer) { GetChildContainer()->Add(std::move(layer)); diff --git a/flow/layers/container_layer.h b/flow/layers/container_layer.h index 699cf4d61b530..dfea4011a770b 100644 --- a/flow/layers/container_layer.h +++ b/flow/layers/container_layer.h @@ -15,13 +15,9 @@ class ContainerLayer : public Layer { public: ContainerLayer(); -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void Diff(DiffContext* context, const Layer* old_layer) override; void PreservePaintRegion(DiffContext* context) override; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - virtual void Add(std::shared_ptr layer); void Preroll(PrerollContext* context, const SkMatrix& matrix) override; @@ -29,13 +25,9 @@ class ContainerLayer : public Layer { const std::vector>& layers() const { return layers_; } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - virtual void DiffChildren(DiffContext* context, const ContainerLayer* old_layer); -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - protected: void PrerollChildren(PrerollContext* context, const SkMatrix& child_matrix, @@ -109,10 +101,8 @@ class MergedContainerLayer : public ContainerLayer { void Add(std::shared_ptr layer) override; -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT void DiffChildren(DiffContext* context, const ContainerLayer* old_layer) override; -#endif protected: /** diff --git a/flow/layers/container_layer_unittests.cc b/flow/layers/container_layer_unittests.cc index aa34048bf2b89..aea84513a6f9a 100644 --- a/flow/layers/container_layer_unittests.cc +++ b/flow/layers/container_layer_unittests.cc @@ -251,8 +251,6 @@ TEST_F(ContainerLayerTest, MergedMultipleChildren) { child_path2, child_paint2}}})); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - using ContainerLayerDiffTest = DiffContextTest; // Insert PictureLayer amongst container layers @@ -456,7 +454,5 @@ TEST_F(ContainerLayerDiffTest, ReplaceLayer) { EXPECT_EQ(damage.frame_damage, SkIRect::MakeLTRB(200, 0, 250, 150)); } -#endif - } // namespace testing } // namespace flutter diff --git a/flow/layers/display_list_layer.cc b/flow/layers/display_list_layer.cc index a2a2a9b4c5d28..8a9e562661670 100644 --- a/flow/layers/display_list_layer.cc +++ b/flow/layers/display_list_layer.cc @@ -17,8 +17,6 @@ DisplayListLayer::DisplayListLayer(const SkPoint& offset, is_complex_(is_complex), will_change_(will_change) {} -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - bool DisplayListLayer::IsReplacing(DiffContext* context, const Layer* layer) const { // Only return true for identical display lists; This way @@ -85,21 +83,24 @@ bool DisplayListLayer::Compare(DiffContext::Statistics& statistics, return res; } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void DisplayListLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "DisplayListLayer::Preroll"); DisplayList* disp_list = display_list(); + SkRect bounds = disp_list->bounds().makeOffset(offset_.x(), offset_.y()); + if (auto* cache = context->raster_cache) { TRACE_EVENT0("flutter", "DisplayListLayer::RasterCache (Preroll)"); - cache->Prepare(context, disp_list, is_complex_, will_change_, matrix, - offset_); + if (context->cull_rect.intersects(bounds)) { + cache->Prepare(context, disp_list, is_complex_, will_change_, matrix, + offset_); + } else { + // Don't evict raster cache entry during partial repaint + cache->Touch(disp_list, matrix); + } } - - SkRect bounds = disp_list->bounds().makeOffset(offset_.x(), offset_.y()); set_paint_bounds(bounds); } diff --git a/flow/layers/display_list_layer.h b/flow/layers/display_list_layer.h index c3203da57ce8f..cf48fc489fd5d 100644 --- a/flow/layers/display_list_layer.h +++ b/flow/layers/display_list_layer.h @@ -24,8 +24,6 @@ class DisplayListLayer : public Layer { return display_list_.skia_object().get(); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - bool IsReplacing(DiffContext* context, const Layer* layer) const override; void Diff(DiffContext* context, const Layer* old_layer) override; @@ -34,8 +32,6 @@ class DisplayListLayer : public Layer { return this; } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* frame, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; @@ -46,14 +42,10 @@ class DisplayListLayer : public Layer { bool is_complex_ = false; bool will_change_ = false; -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - static bool Compare(DiffContext::Statistics& statistics, const DisplayListLayer* l1, const DisplayListLayer* l2); -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - FML_DISALLOW_COPY_AND_ASSIGN(DisplayListLayer); }; diff --git a/flow/layers/display_list_layer_unittests.cc b/flow/layers/display_list_layer_unittests.cc index 39570590c45ba..fafd4f25dd855 100644 --- a/flow/layers/display_list_layer_unittests.cc +++ b/flow/layers/display_list_layer_unittests.cc @@ -106,8 +106,6 @@ TEST_F(DisplayListLayerTest, SimpleDisplayList) { EXPECT_EQ(mock_canvas().draw_calls(), expected_draw_calls); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - using DisplayListLayerDiffTest = DiffContextTest; TEST_F(DisplayListLayerDiffTest, SimpleDisplayList) { @@ -179,7 +177,5 @@ TEST_F(DisplayListLayerDiffTest, DisplayListCompare) { EXPECT_EQ(damage.frame_damage, SkIRect::MakeLTRB(20, 20, 70, 70)); } -#endif - } // namespace testing } // namespace flutter diff --git a/flow/layers/image_filter_layer.cc b/flow/layers/image_filter_layer.cc index e332e067c7db9..d99db54998d21 100644 --- a/flow/layers/image_filter_layer.cc +++ b/flow/layers/image_filter_layer.cc @@ -11,8 +11,6 @@ ImageFilterLayer::ImageFilterLayer(sk_sp filter) transformed_filter_(nullptr), render_count_(1) {} -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void ImageFilterLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); auto* prev = static_cast(old_layer); @@ -23,25 +21,21 @@ void ImageFilterLayer::Diff(DiffContext* context, const Layer* old_layer) { } } - DiffChildren(context, prev); - - SkMatrix inverse; - if (context->GetTransform().invert(&inverse)) { - auto screen_bounds = context->CurrentSubtreeRegion().ComputeBounds(); - + if (filter_) { auto filter = filter_->makeWithLocalMatrix(context->GetTransform()); - - auto filter_bounds = - filter->filterBounds(screen_bounds.roundOut(), SkMatrix::I(), - SkImageFilter::kForward_MapDirection); - context->AddLayerBounds(inverse.mapRect(SkRect::Make(filter_bounds))); + if (filter) { + // This transform will be applied to every child rect in the subtree + context->PushFilterBoundsAdjustment([filter](SkRect rect) { + return SkRect::Make( + filter->filterBounds(rect.roundOut(), SkMatrix::I(), + SkImageFilter::kForward_MapDirection)); + }); + } } - + DiffChildren(context, prev); context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void ImageFilterLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "ImageFilterLayer::Preroll"); diff --git a/flow/layers/image_filter_layer.h b/flow/layers/image_filter_layer.h index 635d57a432365..48a0760d4b21a 100644 --- a/flow/layers/image_filter_layer.h +++ b/flow/layers/image_filter_layer.h @@ -12,14 +12,10 @@ namespace flutter { class ImageFilterLayer : public MergedContainerLayer { public: - ImageFilterLayer(sk_sp filter); - -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT + explicit ImageFilterLayer(sk_sp filter); void Diff(DiffContext* context, const Layer* old_layer) override; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* context, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; diff --git a/flow/layers/image_filter_layer_unittests.cc b/flow/layers/image_filter_layer_unittests.cc index 61dcdc18fcb63..d1f2f89417162 100644 --- a/flow/layers/image_filter_layer_unittests.cc +++ b/flow/layers/image_filter_layer_unittests.cc @@ -334,8 +334,6 @@ TEST_F(ImageFilterLayerTest, ChildrenNotCached) { EXPECT_FALSE(raster_cache()->Draw(mock_layer2.get(), cache_canvas)); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - using ImageFilterLayerDiffTest = DiffContextTest; TEST_F(ImageFilterLayerDiffTest, ImageFilterLayer) { @@ -385,7 +383,45 @@ TEST_F(ImageFilterLayerDiffTest, ImageFilterLayer) { EXPECT_EQ(damage.frame_damage, SkIRect::MakeLTRB(130, 130, 141, 141)); } -#endif +TEST_F(ImageFilterLayerDiffTest, ImageFilterLayerInflatestChildSize) { + auto filter = SkImageFilters::Blur(10, 10, SkTileMode::kClamp, nullptr); + + { + // tests later assume 30px paint area, fail early if that's not the case + auto paint_rect = + filter->filterBounds(SkIRect::MakeWH(10, 10), SkMatrix::I(), + SkImageFilter::kForward_MapDirection); + EXPECT_EQ(paint_rect, SkIRect::MakeLTRB(-30, -30, 40, 40)); + } + + MockLayerTree l1; + + // Use nested filter layers to check if both contribute to child bounds + auto filter_layer_1_1 = std::make_shared(filter); + auto filter_layer_1_2 = std::make_shared(filter); + filter_layer_1_1->Add(filter_layer_1_2); + auto path = SkPath().addRect(SkRect::MakeLTRB(100, 100, 110, 110)); + filter_layer_1_2->Add( + std::make_shared(path, SkPaint(SkColors::kYellow))); + l1.root()->Add(filter_layer_1_1); + + // second layer tree with identical filter layers but different child layer + MockLayerTree l2; + auto filter_layer2_1 = std::make_shared(filter); + filter_layer2_1->AssignOldLayer(filter_layer_1_1.get()); + auto filter_layer2_2 = std::make_shared(filter); + filter_layer2_2->AssignOldLayer(filter_layer_1_2.get()); + filter_layer2_1->Add(filter_layer2_2); + filter_layer2_2->Add( + std::make_shared(path, SkPaint(SkColors::kRed))); + l2.root()->Add(filter_layer2_1); + + DiffLayerTree(l1, MockLayerTree()); + auto damage = DiffLayerTree(l2, l1); + + // ensure that filter properly inflated child size + EXPECT_EQ(damage.frame_damage, SkIRect::MakeLTRB(40, 40, 170, 170)); +} } // namespace testing } // namespace flutter diff --git a/flow/layers/layer.cc b/flow/layers/layer.cc index e66359857409a..8a34b775ef766 100644 --- a/flow/layers/layer.cc +++ b/flow/layers/layer.cc @@ -65,6 +65,7 @@ Layer::AutoSaveLayer::AutoSaveLayer(const PaintContext& paint_context, canvas_(save_mode == SaveMode::kInternalNodesCanvas ? *(paint_context.internal_nodes_canvas) : *(paint_context.leaf_nodes_canvas)) { + TRACE_EVENT0("flutter", "Canvas::saveLayer"); canvas_.saveLayer(bounds_, paint); } @@ -76,6 +77,7 @@ Layer::AutoSaveLayer::AutoSaveLayer(const PaintContext& paint_context, canvas_(save_mode == SaveMode::kInternalNodesCanvas ? *(paint_context.internal_nodes_canvas) : *(paint_context.leaf_nodes_canvas)) { + TRACE_EVENT0("flutter", "Canvas::saveLayer"); canvas_.saveLayer(layer_rec); } diff --git a/flow/layers/layer.h b/flow/layers/layer.h index e52fe3f7569db..f62cc3beb9835 100644 --- a/flow/layers/layer.h +++ b/flow/layers/layer.h @@ -79,8 +79,6 @@ class Layer { original_layer_id_ = old_layer->original_layer_id_; } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - // Used to establish link between old layer and new layer that replaces it. // If this method returns true, it is assumed that this layer replaces the old // layer in tree and is able to diff with it. @@ -100,8 +98,6 @@ class Layer { context->SetLayerPaintRegion(this, context->GetOldLayerPaintRegion(this)); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - virtual void Preroll(PrerollContext* context, const SkMatrix& matrix); // Used during Preroll by layers that employ a saveLayer to manage the @@ -271,8 +267,6 @@ class Layer { uint64_t unique_id() const { return unique_id_; } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - virtual const PictureLayer* as_picture_layer() const { return nullptr; } virtual const DisplayListLayer* as_display_list_layer() const { return nullptr; @@ -283,8 +277,6 @@ class Layer { } virtual const testing::MockLayer* as_mock_layer() const { return nullptr; } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - private: SkRect paint_bounds_; uint64_t unique_id_; diff --git a/flow/layers/layer_tree.cc b/flow/layers/layer_tree.cc index 58205ae64590f..447998ec5a786 100644 --- a/flow/layers/layer_tree.cc +++ b/flow/layers/layer_tree.cc @@ -23,7 +23,8 @@ LayerTree::LayerTree(const SkISize& frame_size, float device_pixel_ratio) } bool LayerTree::Preroll(CompositorContext::ScopedFrame& frame, - bool ignore_raster_cache) { + bool ignore_raster_cache, + SkRect cull_rect) { TRACE_EVENT0("flutter", "LayerTree::Preroll"); if (!root_layer_) { @@ -42,7 +43,7 @@ bool LayerTree::Preroll(CompositorContext::ScopedFrame& frame, frame.view_embedder(), stack, color_space, - kGiantRect, + cull_rect, false, frame.context().raster_time(), frame.context().ui_time(), diff --git a/flow/layers/layer_tree.h b/flow/layers/layer_tree.h index 6e4a202deaf76..17b969dcf6e7b 100644 --- a/flow/layers/layer_tree.h +++ b/flow/layers/layer_tree.h @@ -29,7 +29,8 @@ class LayerTree { // layer tree performs any operations that require readback // from the root surface. bool Preroll(CompositorContext::ScopedFrame& frame, - bool ignore_raster_cache = false); + bool ignore_raster_cache = false, + SkRect cull_rect = kGiantRect); void Paint(CompositorContext::ScopedFrame& frame, bool ignore_raster_cache = false) const; @@ -45,13 +46,9 @@ class LayerTree { const SkISize& frame_size() const { return frame_size_; } float device_pixel_ratio() const { return device_pixel_ratio_; } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - const PaintRegionMap& paint_region_map() const { return paint_region_map_; } PaintRegionMap& paint_region_map() { return paint_region_map_; } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - // The number of frame intervals missed after which the compositor must // trace the rasterized picture to a trace file. Specify 0 to disable all // tracing @@ -79,9 +76,7 @@ class LayerTree { bool checkerboard_raster_cache_images_; bool checkerboard_offscreen_layers_; -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT PaintRegionMap paint_region_map_; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT FML_DISALLOW_COPY_AND_ASSIGN(LayerTree); }; diff --git a/flow/layers/opacity_layer.cc b/flow/layers/opacity_layer.cc index df4c8620ce033..022dea79592c6 100644 --- a/flow/layers/opacity_layer.cc +++ b/flow/layers/opacity_layer.cc @@ -12,8 +12,6 @@ namespace flutter { OpacityLayer::OpacityLayer(SkAlpha alpha, const SkPoint& offset) : alpha_(alpha), offset_(offset) {} -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void OpacityLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); auto* prev = static_cast(old_layer); @@ -32,8 +30,6 @@ void OpacityLayer::Diff(DiffContext* context, const Layer* old_layer) { context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void OpacityLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "OpacityLayer::Preroll"); FML_DCHECK(!GetChildContainer()->layers().empty()); // We can't be a leaf. diff --git a/flow/layers/opacity_layer.h b/flow/layers/opacity_layer.h index 52010ce895ff1..b3ad2392c456b 100644 --- a/flow/layers/opacity_layer.h +++ b/flow/layers/opacity_layer.h @@ -27,12 +27,8 @@ class OpacityLayer : public MergedContainerLayer { // the propagation as repainting the OpacityLayer is expensive. OpacityLayer(SkAlpha alpha, const SkPoint& offset); -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void Diff(DiffContext* context, const Layer* old_layer) override; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* context, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; diff --git a/flow/layers/opacity_layer_unittests.cc b/flow/layers/opacity_layer_unittests.cc index 6ef5538433b85..5fa6690425005 100644 --- a/flow/layers/opacity_layer_unittests.cc +++ b/flow/layers/opacity_layer_unittests.cc @@ -428,8 +428,6 @@ TEST_F(OpacityLayerTest, CullRectIsTransformed) { EXPECT_EQ(mockLayer->parent_cull_rect().fTop, -20); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - using OpacityLayerDiffTest = DiffContextTest; TEST_F(OpacityLayerDiffTest, FractionalTranslation) { @@ -448,7 +446,5 @@ TEST_F(OpacityLayerDiffTest, FractionalTranslation) { #endif } -#endif - } // namespace testing } // namespace flutter diff --git a/flow/layers/performance_overlay_layer.cc b/flow/layers/performance_overlay_layer.cc index b9ccee84c7a79..d211959a89c52 100644 --- a/flow/layers/performance_overlay_layer.cc +++ b/flow/layers/performance_overlay_layer.cc @@ -74,8 +74,6 @@ PerformanceOverlayLayer::PerformanceOverlayLayer(uint64_t options, } } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void PerformanceOverlayLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); @@ -88,8 +86,6 @@ void PerformanceOverlayLayer::Diff(DiffContext* context, context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void PerformanceOverlayLayer::Paint(PaintContext& context) const { const int padding = 8; diff --git a/flow/layers/performance_overlay_layer.h b/flow/layers/performance_overlay_layer.h index 91b90244e3f61..2645a6a508452 100644 --- a/flow/layers/performance_overlay_layer.h +++ b/flow/layers/performance_overlay_layer.h @@ -26,8 +26,6 @@ class PerformanceOverlayLayer : public Layer { const std::string& label_prefix, const std::string& font_path); -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - bool IsReplacing(DiffContext* context, const Layer* layer) const override { return layer->as_performance_overlay_layer() != nullptr; } @@ -38,8 +36,6 @@ class PerformanceOverlayLayer : public Layer { return this; } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - explicit PerformanceOverlayLayer(uint64_t options, const char* font_path = nullptr); diff --git a/flow/layers/physical_shape_layer.cc b/flow/layers/physical_shape_layer.cc index a9b76e1ea18eb..8215f3d6e592f 100644 --- a/flow/layers/physical_shape_layer.cc +++ b/flow/layers/physical_shape_layer.cc @@ -23,8 +23,6 @@ PhysicalShapeLayer::PhysicalShapeLayer(SkColor color, path_(path), clip_behavior_(clip_behavior) {} -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void PhysicalShapeLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); auto* prev = static_cast(old_layer); @@ -48,14 +46,13 @@ void PhysicalShapeLayer::Diff(DiffContext* context, const Layer* old_layer) { context->AddLayerBounds(bounds); - if (context->PushCullRect(bounds)) { + // Only push cull rect if there is clip. + if (clip_behavior_ == Clip::none || context->PushCullRect(bounds)) { DiffChildren(context, prev); } context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void PhysicalShapeLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "PhysicalShapeLayer::Preroll"); @@ -101,10 +98,11 @@ void PhysicalShapeLayer::Paint(PaintContext& context) const { case Clip::antiAlias: context.internal_nodes_canvas->clipPath(path_, true); break; - case Clip::antiAliasWithSaveLayer: + case Clip::antiAliasWithSaveLayer: { + TRACE_EVENT0("flutter", "Canvas::saveLayer"); context.internal_nodes_canvas->clipPath(path_, true); context.internal_nodes_canvas->saveLayer(paint_bounds(), nullptr); - break; + } break; case Clip::none: break; } diff --git a/flow/layers/physical_shape_layer.h b/flow/layers/physical_shape_layer.h index 44e555e9fb5ef..cbce4eef68d5e 100644 --- a/flow/layers/physical_shape_layer.h +++ b/flow/layers/physical_shape_layer.h @@ -28,12 +28,8 @@ class PhysicalShapeLayer : public ContainerLayer { bool transparentOccluder, SkScalar dpr); -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void Diff(DiffContext* context, const Layer* old_layer) override; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* context, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; diff --git a/flow/layers/physical_shape_layer_unittests.cc b/flow/layers/physical_shape_layer_unittests.cc index e81c28080ea53..9a9bafb8f6895 100644 --- a/flow/layers/physical_shape_layer_unittests.cc +++ b/flow/layers/physical_shape_layer_unittests.cc @@ -4,6 +4,7 @@ #include "flutter/flow/layers/physical_shape_layer.h" +#include "flutter/flow/testing/diff_context_test.h" #include "flutter/flow/testing/layer_test.h" #include "flutter/flow/testing/mock_layer.h" #include "flutter/fml/macros.h" @@ -350,5 +351,43 @@ TEST_F(PhysicalShapeLayerTest, Readback) { EXPECT_TRUE(ReadbackResult(context, save_layer, reader, true)); } +using PhysicalShapeLayerDiffTest = DiffContextTest; + +TEST_F(PhysicalShapeLayerDiffTest, NoClipPaintRegion) { + MockLayerTree tree1; + const SkPath layer_path = SkPath().addRect(SkRect::MakeXYWH(0, 0, 100, 100)); + auto layer = + std::make_shared(SK_ColorGREEN, SK_ColorBLACK, + 0.0f, // elevation + layer_path, Clip::none); + + const SkPath layer_path2 = + SkPath().addRect(SkRect::MakeXYWH(200, 200, 200, 200)); + auto layer2 = std::make_shared(layer_path2); + layer->Add(layer2); + tree1.root()->Add(layer); + + auto damage = DiffLayerTree(tree1, MockLayerTree()); + EXPECT_EQ(damage.frame_damage, SkIRect::MakeLTRB(0, 0, 400, 400)); +} + +TEST_F(PhysicalShapeLayerDiffTest, ClipPaintRegion) { + MockLayerTree tree1; + const SkPath layer_path = SkPath().addRect(SkRect::MakeXYWH(0, 0, 100, 100)); + auto layer = + std::make_shared(SK_ColorGREEN, SK_ColorBLACK, + 0.0f, // elevation + layer_path, Clip::hardEdge); + + const SkPath layer_path2 = + SkPath().addRect(SkRect::MakeXYWH(200, 200, 200, 200)); + auto layer2 = std::make_shared(layer_path2); + layer->Add(layer2); + tree1.root()->Add(layer); + + auto damage = DiffLayerTree(tree1, MockLayerTree()); + EXPECT_EQ(damage.frame_damage, SkIRect::MakeLTRB(0, 0, 100, 100)); +} + } // namespace testing } // namespace flutter diff --git a/flow/layers/picture_layer.cc b/flow/layers/picture_layer.cc index 7bb2921258577..4103aadae0aff 100644 --- a/flow/layers/picture_layer.cc +++ b/flow/layers/picture_layer.cc @@ -18,8 +18,6 @@ PictureLayer::PictureLayer(const SkPoint& offset, is_complex_(is_complex), will_change_(will_change) {} -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - bool PictureLayer::IsReplacing(DiffContext* context, const Layer* layer) const { // Only return true for identical pictures; This way // ContainerLayer::DiffChildren can detect when a picture layer got inserted @@ -109,20 +107,24 @@ sk_sp PictureLayer::SerializedPicture() const { return cached_serialized_picture_; } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void PictureLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "PictureLayer::Preroll"); SkPicture* sk_picture = picture(); + SkRect bounds = sk_picture->cullRect().makeOffset(offset_.x(), offset_.y()); + if (auto* cache = context->raster_cache) { TRACE_EVENT0("flutter", "PictureLayer::RasterCache (Preroll)"); - cache->Prepare(context, sk_picture, is_complex_, will_change_, matrix, - offset_); + if (context->cull_rect.intersects(bounds)) { + cache->Prepare(context, sk_picture, is_complex_, will_change_, matrix, + offset_); + } else { + // Don't evict raster cache entry during partial repaint + cache->Touch(sk_picture, matrix); + } } - SkRect bounds = sk_picture->cullRect().makeOffset(offset_.x(), offset_.y()); set_paint_bounds(bounds); } diff --git a/flow/layers/picture_layer.h b/flow/layers/picture_layer.h index d90d32cedf2b3..590dc72b595cd 100644 --- a/flow/layers/picture_layer.h +++ b/flow/layers/picture_layer.h @@ -22,16 +22,12 @@ class PictureLayer : public Layer { SkPicture* picture() const { return picture_.skia_object().get(); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - bool IsReplacing(DiffContext* context, const Layer* layer) const override; void Diff(DiffContext* context, const Layer* old_layer) override; const PictureLayer* as_picture_layer() const override { return this; } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* frame, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; @@ -44,16 +40,12 @@ class PictureLayer : public Layer { bool is_complex_ = false; bool will_change_ = false; -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - sk_sp SerializedPicture() const; mutable sk_sp cached_serialized_picture_; static bool Compare(DiffContext::Statistics& statistics, const PictureLayer* l1, const PictureLayer* l2); -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - FML_DISALLOW_COPY_AND_ASSIGN(PictureLayer); }; diff --git a/flow/layers/picture_layer_unittests.cc b/flow/layers/picture_layer_unittests.cc index 5ddb04cab6363..1d619acb08feb 100644 --- a/flow/layers/picture_layer_unittests.cc +++ b/flow/layers/picture_layer_unittests.cc @@ -97,8 +97,6 @@ TEST_F(PictureLayerTest, SimplePicture) { EXPECT_EQ(mock_canvas().draw_calls(), expected_draw_calls); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - using PictureLayerDiffTest = DiffContextTest; TEST_F(PictureLayerDiffTest, SimplePicture) { @@ -167,7 +165,5 @@ TEST_F(PictureLayerDiffTest, PictureCompare) { EXPECT_EQ(damage.frame_damage, SkIRect::MakeLTRB(20, 20, 70, 70)); } -#endif - } // namespace testing } // namespace flutter diff --git a/flow/layers/shader_mask_layer.cc b/flow/layers/shader_mask_layer.cc index 5106d2b3d8251..e007ede2145df 100644 --- a/flow/layers/shader_mask_layer.cc +++ b/flow/layers/shader_mask_layer.cc @@ -11,8 +11,6 @@ ShaderMaskLayer::ShaderMaskLayer(sk_sp shader, SkBlendMode blend_mode) : shader_(shader), mask_rect_(mask_rect), blend_mode_(blend_mode) {} -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void ShaderMaskLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); auto* prev = static_cast(old_layer); @@ -29,8 +27,6 @@ void ShaderMaskLayer::Diff(DiffContext* context, const Layer* old_layer) { context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void ShaderMaskLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { Layer::AutoPrerollSaveLayerState save = Layer::AutoPrerollSaveLayerState::Create(context); diff --git a/flow/layers/shader_mask_layer.h b/flow/layers/shader_mask_layer.h index 193ba5baf1cfd..409a257b6a0a6 100644 --- a/flow/layers/shader_mask_layer.h +++ b/flow/layers/shader_mask_layer.h @@ -16,12 +16,8 @@ class ShaderMaskLayer : public ContainerLayer { const SkRect& mask_rect, SkBlendMode blend_mode); -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void Diff(DiffContext* context, const Layer* old_layer) override; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* context, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; diff --git a/flow/layers/texture_layer.cc b/flow/layers/texture_layer.cc index 7197d52296be9..e360b50d55467 100644 --- a/flow/layers/texture_layer.cc +++ b/flow/layers/texture_layer.cc @@ -19,8 +19,6 @@ TextureLayer::TextureLayer(const SkPoint& offset, freeze_(freeze), sampling_(sampling) {} -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void TextureLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); if (!context->IsSubtreeDirty()) { @@ -30,13 +28,18 @@ void TextureLayer::Diff(DiffContext* context, const Layer* old_layer) { // dirty context->MarkSubtreeDirty(context->GetOldLayerPaintRegion(prev)); } + + // Make sure DiffContext knows there is a TextureLayer in this subtree. + // This prevents ContainerLayer from skipping TextureLayer diffing when + // TextureLayer is inside retained layer. + // See ContainerLayer::DiffChildren + // https://github.com/flutter/flutter/issues/92925 + context->MarkSubtreeHasTextureLayer(); context->AddLayerBounds(SkRect::MakeXYWH(offset_.x(), offset_.y(), size_.width(), size_.height())); context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void TextureLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "TextureLayer::Preroll"); diff --git a/flow/layers/texture_layer.h b/flow/layers/texture_layer.h index 61f7149f7b4c4..15a1c6e6c718b 100644 --- a/flow/layers/texture_layer.h +++ b/flow/layers/texture_layer.h @@ -19,8 +19,6 @@ class TextureLayer : public Layer { bool freeze, const SkSamplingOptions& sampling); -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - bool IsReplacing(DiffContext* context, const Layer* layer) const override { return layer->as_texture_layer() != nullptr; } @@ -29,8 +27,6 @@ class TextureLayer : public Layer { const TextureLayer* as_texture_layer() const override { return this; } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* context, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; diff --git a/flow/layers/texture_layer_unittests.cc b/flow/layers/texture_layer_unittests.cc index 4f6adf9944fd5..4d251120efd9b 100644 --- a/flow/layers/texture_layer_unittests.cc +++ b/flow/layers/texture_layer_unittests.cc @@ -4,6 +4,7 @@ #include "flutter/flow/layers/texture_layer.h" +#include "flutter/flow/testing/diff_context_test.h" #include "flutter/flow/testing/layer_test.h" #include "flutter/flow/testing/mock_layer.h" #include "flutter/flow/testing/mock_texture.h" @@ -94,5 +95,26 @@ TEST_F(TextureLayerTest, PaintingWithLinearSampling) { EXPECT_EQ(mock_canvas().draw_calls(), std::vector()); } +using TextureLayerDiffTest = DiffContextTest; + +TEST_F(TextureLayerDiffTest, TextureInRetainedLayer) { + MockLayerTree tree1; + auto container = std::make_shared(); + tree1.root()->Add(container); + auto layer = std::make_shared( + SkPoint::Make(0, 0), SkSize::Make(100, 100), 0, false, + SkSamplingOptions(SkFilterMode::kLinear)); + container->Add(layer); + + MockLayerTree tree2; + tree2.root()->Add(container); // retained layer + + auto damage = DiffLayerTree(tree1, MockLayerTree()); + EXPECT_EQ(damage.frame_damage, SkIRect::MakeLTRB(0, 0, 100, 100)); + + damage = DiffLayerTree(tree2, tree1); + EXPECT_EQ(damage.frame_damage, SkIRect::MakeLTRB(0, 0, 100, 100)); +} + } // namespace testing } // namespace flutter diff --git a/flow/layers/transform_layer.cc b/flow/layers/transform_layer.cc index aff4710c23f37..60b1737add8dd 100644 --- a/flow/layers/transform_layer.cc +++ b/flow/layers/transform_layer.cc @@ -26,8 +26,6 @@ TransformLayer::TransformLayer(const SkMatrix& transform) } } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - void TransformLayer::Diff(DiffContext* context, const Layer* old_layer) { DiffContext::AutoSubtreeRestore subtree(context); auto* prev = static_cast(old_layer); @@ -42,8 +40,6 @@ void TransformLayer::Diff(DiffContext* context, const Layer* old_layer) { context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void TransformLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { TRACE_EVENT0("flutter", "TransformLayer::Preroll"); diff --git a/flow/layers/transform_layer.h b/flow/layers/transform_layer.h index 69757975df122..72ad561acd46e 100644 --- a/flow/layers/transform_layer.h +++ b/flow/layers/transform_layer.h @@ -13,14 +13,10 @@ namespace flutter { // at all. Hence |set_transform| must be called with an initialized SkMatrix. class TransformLayer : public ContainerLayer { public: - TransformLayer(const SkMatrix& transform); - -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT + explicit TransformLayer(const SkMatrix& transform); void Diff(DiffContext* context, const Layer* old_layer) override; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - void Preroll(PrerollContext* context, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; diff --git a/flow/layers/transform_layer_unittests.cc b/flow/layers/transform_layer_unittests.cc index 2cb1adf35f91b..345a1051f38e2 100644 --- a/flow/layers/transform_layer_unittests.cc +++ b/flow/layers/transform_layer_unittests.cc @@ -225,8 +225,6 @@ TEST_F(TransformLayerTest, NestedSeparated) { MockCanvas::DrawCall{1, MockCanvas::RestoreData{0}}})); } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - using TransformLayerLayerDiffTest = DiffContextTest; TEST_F(TransformLayerLayerDiffTest, Transform) { @@ -333,7 +331,5 @@ TEST_F(TransformLayerLayerDiffTest, TransformNested) { EXPECT_EQ(damage.frame_damage, SkIRect::MakeLTRB(200, 200, 300, 302)); } -#endif - } // namespace testing } // namespace flutter diff --git a/flow/paint_region.cc b/flow/paint_region.cc index 760259a12c422..9b836edd13ffd 100644 --- a/flow/paint_region.cc +++ b/flow/paint_region.cc @@ -6,8 +6,6 @@ namespace flutter { -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - SkRect PaintRegion::ComputeBounds() const { SkRect res = SkRect::MakeEmpty(); for (const auto& r : *this) { @@ -16,6 +14,4 @@ SkRect PaintRegion::ComputeBounds() const { return res; } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - } // namespace flutter diff --git a/flow/paint_region.h b/flow/paint_region.h index 0750e5f7ffa62..1fd8896b68bd9 100644 --- a/flow/paint_region.h +++ b/flow/paint_region.h @@ -8,8 +8,6 @@ namespace flutter { -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - // Corresponds to area on the screen where the layer subtree has painted to. // // The area is used when adding damage of removed or dirty layer to overall @@ -27,8 +25,13 @@ class PaintRegion { PaintRegion(std::shared_ptr> rects, size_t from, size_t to, - bool has_readback) - : rects_(rects), from_(from), to_(to), has_readback_(has_readback) {} + bool has_readback, + bool has_texture) + : rects_(rects), + from_(from), + to_(to), + has_readback_(has_readback), + has_texture_(has_texture) {} std::vector::const_iterator begin() const { FML_DCHECK(is_valid()); @@ -49,13 +52,16 @@ class PaintRegion { // that performs readback bool has_readback() const { return has_readback_; } + // Returns whether there is a TextureLayer in subtree represented by this + // region. + bool has_texture() const { return has_texture_; } + private: std::shared_ptr> rects_; size_t from_ = 0; size_t to_ = 0; bool has_readback_ = false; + bool has_texture_ = false; }; -#endif - } // namespace flutter diff --git a/flow/raster_cache.cc b/flow/raster_cache.cc index f41f48747f5c7..110974b4bca65 100644 --- a/flow/raster_cache.cc +++ b/flow/raster_cache.cc @@ -20,8 +20,9 @@ namespace flutter { RasterCacheResult::RasterCacheResult(sk_sp image, - const SkRect& logical_rect) - : image_(std::move(image)), logical_rect_(logical_rect) {} + const SkRect& logical_rect, + const char* type) + : image_(std::move(image)), logical_rect_(logical_rect), flow_(type) {} void RasterCacheResult::draw(SkCanvas& canvas, const SkPaint* paint) const { TRACE_EVENT0("flutter", "RasterCacheResult::draw"); @@ -32,6 +33,7 @@ void RasterCacheResult::draw(SkCanvas& canvas, const SkPaint* paint) const { std::abs(bounds.size().width() - image_->dimensions().width()) <= 1 && std::abs(bounds.size().height() - image_->dimensions().height()) <= 1); canvas.resetMatrix(); + flow_.Step(); canvas.drawImage(image_, bounds.fLeft, bounds.fTop, SkSamplingOptions(), paint); } @@ -81,7 +83,7 @@ static bool IsPictureWorthRasterizing(SkPicture* picture, // TODO(abarth): We should find a better heuristic here that lets us avoid // wasting memory on trivial layers that are easy to re-rasterize every frame. - return picture->approximateOpCount() > 5; + return picture->approximateOpCount(true) > 5; } static bool IsDisplayListWorthRasterizing(DisplayList* display_list, @@ -107,7 +109,7 @@ static bool IsDisplayListWorthRasterizing(DisplayList* display_list, // TODO(abarth): We should find a better heuristic here that lets us avoid // wasting memory on trivial layers that are easy to re-rasterize every frame. - return display_list->op_count() > 5; + return display_list->op_count(true) > 5; } /// @note Procedure doesn't copy all closures. @@ -117,6 +119,7 @@ static std::unique_ptr Rasterize( SkColorSpace* dst_color_space, bool checkerboard, const SkRect& logical_rect, + const char* type, const std::function& draw_function) { TRACE_EVENT0("flutter", "RasterCachePopulate"); SkIRect cache_rect = RasterCache::GetDeviceBounds(logical_rect, ctm); @@ -144,7 +147,7 @@ static std::unique_ptr Rasterize( } return std::make_unique(surface->makeImageSnapshot(), - logical_rect); + logical_rect, type); } std::unique_ptr RasterCache::RasterizePicture( @@ -154,7 +157,7 @@ std::unique_ptr RasterCache::RasterizePicture( SkColorSpace* dst_color_space, bool checkerboard) const { return Rasterize(context, ctm, dst_color_space, checkerboard, - picture->cullRect(), + picture->cullRect(), "RasterCacheFlow::SkPicture", [=](SkCanvas* canvas) { canvas->drawPicture(picture); }); } @@ -165,7 +168,7 @@ std::unique_ptr RasterCache::RasterizeDisplayList( SkColorSpace* dst_color_space, bool checkerboard) const { return Rasterize(context, ctm, dst_color_space, checkerboard, - display_list->bounds(), + display_list->bounds(), "RasterCacheFlow::DisplayList", [=](SkCanvas* canvas) { display_list->RenderTo(canvas); }); } @@ -188,7 +191,8 @@ std::unique_ptr RasterCache::RasterizeLayer( bool checkerboard) const { return Rasterize( context->gr_context, ctm, context->dst_color_space, checkerboard, - layer->paint_bounds(), [layer, context](SkCanvas* canvas) { + layer->paint_bounds(), "RasterCacheFlow::Layer", + [layer, context](SkCanvas* canvas) { SkISize canvas_size = canvas->getBaseLayerSize(); SkNWayCanvas internal_nodes_canvas(canvas_size.width(), canvas_size.height()); @@ -230,11 +234,7 @@ bool RasterCache::Prepare(PrerollContext* context, SkMatrix transformation_matrix = untranslated_matrix; transformation_matrix.preTranslate(offset.x(), offset.y()); - // Decompose the matrix (once) for all subsequent operations. We want to make - // sure to avoid volumetric distortions while accounting for scaling. - const MatrixDecomposition matrix(transformation_matrix); - - if (!matrix.IsValid()) { + if (!transformation_matrix.invert(nullptr)) { // The matrix was singular. No point in going further. return false; } @@ -281,11 +281,7 @@ bool RasterCache::Prepare(PrerollContext* context, SkMatrix transformation_matrix = untranslated_matrix; transformation_matrix.preTranslate(offset.x(), offset.y()); - // Decompose the matrix (once) for all subsequent operations. We want to make - // sure to avoid volumetric distortions while accounting for scaling. - const MatrixDecomposition matrix(transformation_matrix); - - if (!matrix.IsValid()) { + if (!transformation_matrix.invert(nullptr)) { // The matrix was singular. No point in going further. return false; } @@ -315,6 +311,36 @@ bool RasterCache::Prepare(PrerollContext* context, return true; } +void RasterCache::Touch(Layer* layer, const SkMatrix& ctm) { + LayerRasterCacheKey cache_key(layer->unique_id(), ctm); + auto it = layer_cache_.find(cache_key); + if (it != layer_cache_.end()) { + it->second.used_this_frame = true; + it->second.access_count++; + } +} + +void RasterCache::Touch(SkPicture* picture, + const SkMatrix& transformation_matrix) { + PictureRasterCacheKey cache_key(picture->uniqueID(), transformation_matrix); + auto it = picture_cache_.find(cache_key); + if (it != picture_cache_.end()) { + it->second.used_this_frame = true; + it->second.access_count++; + } +} + +void RasterCache::Touch(DisplayList* display_list, + const SkMatrix& transformation_matrix) { + DisplayListRasterCacheKey cache_key(display_list->unique_id(), + transformation_matrix); + auto it = display_list_cache_.find(cache_key); + if (it != display_list_cache_.end()) { + it->second.used_this_frame = true; + it->second.access_count++; + } +} + bool RasterCache::Draw(const SkPicture& picture, SkCanvas& canvas) const { PictureRasterCacheKey cache_key(picture.uniqueID(), canvas.getTotalMatrix()); auto it = picture_cache_.find(cache_key); @@ -376,20 +402,29 @@ bool RasterCache::Draw(const Layer* layer, return false; } -void RasterCache::SweepAfterFrame() { - TraceStatsToTimeline(); - SweepOneCacheAfterFrame(picture_cache_); - SweepOneCacheAfterFrame(display_list_cache_); - SweepOneCacheAfterFrame(layer_cache_); +void RasterCache::PrepareNewFrame() { picture_cached_this_frame_ = 0; display_list_cached_this_frame_ = 0; - sweep_count_++; +} + +void RasterCache::CleanupAfterFrame() { + picture_metrics_ = {}; + layer_metrics_ = {}; + { + TRACE_EVENT0("flutter", "RasterCache::SweepCaches"); + SweepOneCacheAfterFrame(picture_cache_, picture_metrics_); + SweepOneCacheAfterFrame(display_list_cache_, picture_metrics_); + SweepOneCacheAfterFrame(layer_cache_, layer_metrics_); + } + TraceStatsToTimeline(); } void RasterCache::Clear() { picture_cache_.clear(); display_list_cache_.clear(); layer_cache_.clear(); + picture_metrics_ = {}; + layer_metrics_ = {}; } size_t RasterCache::GetCachedEntriesCount() const { @@ -422,10 +457,10 @@ void RasterCache::TraceStatsToTimeline() const { FML_TRACE_COUNTER( "flutter", // "RasterCache", reinterpret_cast(this), // - "LayerCount", GetLayerCachedEntriesCount(), // - "LayerMBytes", EstimateLayerCacheByteSize() / kMegaByteSizeInBytes, // - "PictureCount", GetPictureCachedEntriesCount(), // - "PictureMBytes", EstimatePictureCacheByteSize() / kMegaByteSizeInBytes); + "LayerCount", layer_metrics_.total_count(), // + "LayerMBytes", layer_metrics_.total_bytes() / kMegaByteSizeInBytes, // + "PictureCount", picture_metrics_.total_count(), // + "PictureMBytes", picture_metrics_.total_bytes() / kMegaByteSizeInBytes); #endif // !FLUTTER_RELEASE } diff --git a/flow/raster_cache.h b/flow/raster_cache.h index 92fbea16511e5..2dc052bab67ea 100644 --- a/flow/raster_cache.h +++ b/flow/raster_cache.h @@ -12,6 +12,7 @@ #include "flutter/flow/raster_cache_key.h" #include "flutter/fml/macros.h" #include "flutter/fml/memory/weak_ptr.h" +#include "flutter/fml/trace_event.h" #include "third_party/skia/include/core/SkImage.h" #include "third_party/skia/include/core/SkSize.h" @@ -19,7 +20,9 @@ namespace flutter { class RasterCacheResult { public: - RasterCacheResult(sk_sp image, const SkRect& logical_rect); + RasterCacheResult(sk_sp image, + const SkRect& logical_rect, + const char* type); virtual ~RasterCacheResult() = default; @@ -36,10 +39,47 @@ class RasterCacheResult { private: sk_sp image_; SkRect logical_rect_; + fml::tracing::TraceFlow flow_; }; struct PrerollContext; +struct RasterCacheMetrics { + /** + * The number of cache entries with images evicted in this frame. + */ + size_t eviction_count = 0; + + /** + * The size of all of the images evicted in this frame. + */ + size_t eviction_bytes = 0; + + /** + * The number of cache entries with images used in this frame. + */ + size_t in_use_count = 0; + + /** + * The size of all of the images used in this frame. + */ + size_t in_use_bytes = 0; + + /** + * The total cache entries that had images during this frame whether + * they were used in the frame or held memory during the frame and then + * were evicted after it ended. + */ + size_t total_count() const { return in_use_count + eviction_count; } + + /** + * The size of all of the cached images during this frame whether + * they were used in the frame or held memory during the frame and then + * were evicted after it ended. + */ + size_t total_bytes() const { return in_use_bytes + eviction_bytes; } +}; + class RasterCache { public: // The default max number of picture and display list raster caches to be @@ -152,6 +192,14 @@ class RasterCache { const SkMatrix& untranslated_matrix, const SkPoint& offset = SkPoint()); + // If there is cache entry for this picture, display list or layer, mark it as + // used for this frame in order to not get evicted. This is needed during + // partial repaint for layers that are outside of current clip and are culled + // away. + void Touch(SkPicture* picture, const SkMatrix& transformation_matrix); + void Touch(DisplayList* display_list, const SkMatrix& transformation_matrix); + void Touch(Layer* layer, const SkMatrix& ctm); + void Prepare(PrerollContext* context, Layer* layer, const SkMatrix& ctm); // Find the raster cache for the picture and draw it to the canvas. @@ -174,16 +222,29 @@ class RasterCache { SkCanvas& canvas, SkPaint* paint = nullptr) const; - void SweepAfterFrame(); + void PrepareNewFrame(); + void CleanupAfterFrame(); void Clear(); void SetCheckboardCacheImages(bool checkerboard); + const RasterCacheMetrics& picture_metrics() const { return picture_metrics_; } + const RasterCacheMetrics& layer_metrics() const { return layer_metrics_; } + size_t GetCachedEntriesCount() const; + /** + * Return the number of map entries in the layer cache regardless of whether + * the entries have been populated with an image. + */ size_t GetLayerCachedEntriesCount() const; + /** + * Return the number of map entries in the picture caches (SkPicture and + * DisplayList) regardless of whether the entries have been populated with + * an image. + */ size_t GetPictureCachedEntriesCount() const; /** @@ -207,15 +268,6 @@ class RasterCache { */ size_t EstimateLayerCacheByteSize() const; - /** - * @brief Return the count of cache sweeps that have occured. - * - * The sweep count will help to determine if a sweep of the cache may have - * removed expired entries since the last time the method was called. - * The count will increment even if the sweep performs no evictions. - */ - int sweep_count() const { return sweep_count_; } - /** * @brief Return the number of frames that a picture must be prepared * before it will be cached. If the number is 0, then no picture will @@ -234,18 +286,26 @@ class RasterCache { }; template - static void SweepOneCacheAfterFrame(Cache& cache) { + static void SweepOneCacheAfterFrame(Cache& cache, + RasterCacheMetrics& metrics) { std::vector dead; for (auto it = cache.begin(); it != cache.end(); ++it) { Entry& entry = it->second; if (!entry.used_this_frame) { dead.push_back(it); + } else if (entry.image) { + metrics.in_use_count++; + metrics.in_use_bytes += entry.image->image_bytes(); } entry.used_this_frame = false; } for (auto it : dead) { + if (it->second.image) { + metrics.eviction_count++; + metrics.eviction_bytes += it->second.image->image_bytes(); + } cache.erase(it); } } @@ -261,7 +321,8 @@ class RasterCache { const size_t picture_and_display_list_cache_limit_per_frame_; size_t picture_cached_this_frame_ = 0; size_t display_list_cached_this_frame_ = 0; - int sweep_count_ = 0; + RasterCacheMetrics layer_metrics_; + RasterCacheMetrics picture_metrics_; mutable PictureRasterCacheKey::Map picture_cache_; mutable DisplayListRasterCacheKey::Map display_list_cache_; mutable LayerRasterCacheKey::Map layer_cache_; diff --git a/flow/raster_cache_key.h b/flow/raster_cache_key.h index 21d6bfe3dfdcb..2b71ca748545b 100644 --- a/flow/raster_cache_key.h +++ b/flow/raster_cache_key.h @@ -7,8 +7,8 @@ #include -#include "flutter/flow/matrix_decomposition.h" #include "flutter/fml/logging.h" +#include "third_party/skia/include/core/SkMatrix.h" namespace flutter { diff --git a/flow/raster_cache_unittests.cc b/flow/raster_cache_unittests.cc index 928a6da06765e..e7c48c2730611 100644 --- a/flow/raster_cache_unittests.cc +++ b/flow/raster_cache_unittests.cc @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "flutter/flow/display_list.h" #include "flutter/flow/raster_cache.h" #include "flutter/flow/testing/mock_raster_cache.h" @@ -25,6 +26,44 @@ sk_sp GetSamplePicture() { return recorder.finishRecordingAsPicture(); } +sk_sp GetSampleDisplayList() { + DisplayListBuilder builder(SkRect::MakeWH(150, 100)); + builder.setColor(SK_ColorRED); + builder.drawRect(SkRect::MakeXYWH(10, 10, 80, 80)); + return builder.Build(); +} + +sk_sp GetSampleNestedPicture() { + SkPictureRecorder recorder; + recorder.beginRecording(SkRect::MakeWH(150, 100)); + SkCanvas* canvas = recorder.getRecordingCanvas(); + SkPaint paint; + for (int y = 10; y <= 60; y += 10) { + for (int x = 10; x <= 60; x += 10) { + paint.setColor(((x + y) % 20) == 10 ? SK_ColorRED : SK_ColorBLUE); + canvas->drawRect(SkRect::MakeXYWH(x, y, 80, 80), paint); + } + } + SkPictureRecorder outer_recorder; + outer_recorder.beginRecording(SkRect::MakeWH(150, 100)); + canvas = outer_recorder.getRecordingCanvas(); + canvas->drawPicture(recorder.finishRecordingAsPicture()); + return outer_recorder.finishRecordingAsPicture(); +} + +sk_sp GetSampleNestedDisplayList() { + DisplayListBuilder builder(SkRect::MakeWH(150, 100)); + for (int y = 10; y <= 60; y += 10) { + for (int x = 10; x <= 60; x += 10) { + builder.setColor(((x + y) % 20) == 10 ? SK_ColorRED : SK_ColorBLUE); + builder.drawRect(SkRect::MakeXYWH(x, y, 80, 80)); + } + } + DisplayListBuilder outer_builder(SkRect::MakeWH(150, 100)); + outer_builder.drawDisplayList(builder.Build()); + return outer_builder.Build(); +} + } // namespace TEST(RasterCache, SimpleInitialization) { @@ -32,7 +71,44 @@ TEST(RasterCache, SimpleInitialization) { ASSERT_TRUE(true); } -TEST(RasterCache, ThresholdIsRespected) { +TEST(RasterCache, ThresholdIsRespectedForSkPicture) { + size_t threshold = 2; + flutter::RasterCache cache(threshold); + + SkMatrix matrix = SkMatrix::I(); + + auto picture = GetSamplePicture(); + + SkCanvas dummy_canvas; + + PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + + cache.PrepareNewFrame(); + + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, + picture.get(), true, false, matrix)); + // 1st access. + ASSERT_FALSE(cache.Draw(*picture, dummy_canvas)); + + cache.CleanupAfterFrame(); + cache.PrepareNewFrame(); + + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, + picture.get(), true, false, matrix)); + + // 2nd access. + ASSERT_FALSE(cache.Draw(*picture, dummy_canvas)); + + cache.CleanupAfterFrame(); + cache.PrepareNewFrame(); + + // Now Prepare should cache it. + ASSERT_TRUE(cache.Prepare(&preroll_context_holder.preroll_context, + picture.get(), true, false, matrix)); + ASSERT_TRUE(cache.Draw(*picture, dummy_canvas)); +} + +TEST(RasterCache, MetricsOmitUnpopulatedEntries) { size_t threshold = 2; flutter::RasterCache cache(threshold); @@ -44,12 +120,17 @@ TEST(RasterCache, ThresholdIsRespected) { PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + cache.PrepareNewFrame(); + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, picture.get(), true, false, matrix)); // 1st access. ASSERT_FALSE(cache.Draw(*picture, dummy_canvas)); - cache.SweepAfterFrame(); + cache.CleanupAfterFrame(); + ASSERT_EQ(cache.picture_metrics().total_count(), 0u); + ASSERT_EQ(cache.picture_metrics().total_bytes(), 0u); + cache.PrepareNewFrame(); ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, picture.get(), true, false, matrix)); @@ -57,15 +138,60 @@ TEST(RasterCache, ThresholdIsRespected) { // 2nd access. ASSERT_FALSE(cache.Draw(*picture, dummy_canvas)); - cache.SweepAfterFrame(); + cache.CleanupAfterFrame(); + ASSERT_EQ(cache.picture_metrics().total_count(), 0u); + ASSERT_EQ(cache.picture_metrics().total_bytes(), 0u); + cache.PrepareNewFrame(); // Now Prepare should cache it. ASSERT_TRUE(cache.Prepare(&preroll_context_holder.preroll_context, picture.get(), true, false, matrix)); ASSERT_TRUE(cache.Draw(*picture, dummy_canvas)); + + cache.CleanupAfterFrame(); + ASSERT_EQ(cache.picture_metrics().total_count(), 1u); + // 150w * 100h * 4bpp + ASSERT_EQ(cache.picture_metrics().total_bytes(), 60000u); +} + +TEST(RasterCache, ThresholdIsRespectedForDisplayList) { + size_t threshold = 2; + flutter::RasterCache cache(threshold); + + SkMatrix matrix = SkMatrix::I(); + + auto display_list = GetSampleDisplayList(); + + SkCanvas dummy_canvas; + + PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + + cache.PrepareNewFrame(); + + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, + display_list.get(), true, false, matrix)); + // 1st access. + ASSERT_FALSE(cache.Draw(*display_list, dummy_canvas)); + + cache.CleanupAfterFrame(); + cache.PrepareNewFrame(); + + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, + display_list.get(), true, false, matrix)); + + // 2nd access. + ASSERT_FALSE(cache.Draw(*display_list, dummy_canvas)); + + cache.CleanupAfterFrame(); + cache.PrepareNewFrame(); + + // Now Prepare should cache it. + ASSERT_TRUE(cache.Prepare(&preroll_context_holder.preroll_context, + display_list.get(), true, false, matrix)); + ASSERT_TRUE(cache.Draw(*display_list, dummy_canvas)); } -TEST(RasterCache, AccessThresholdOfZeroDisablesCaching) { +TEST(RasterCache, AccessThresholdOfZeroDisablesCachingForSkPicture) { size_t threshold = 0; flutter::RasterCache cache(threshold); @@ -77,13 +203,35 @@ TEST(RasterCache, AccessThresholdOfZeroDisablesCaching) { PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + cache.PrepareNewFrame(); + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, picture.get(), true, false, matrix)); ASSERT_FALSE(cache.Draw(*picture, dummy_canvas)); } -TEST(RasterCache, PictureCacheLimitPerFrameIsRespectedWhenZero) { +TEST(RasterCache, AccessThresholdOfZeroDisablesCachingForDisplayList) { + size_t threshold = 0; + flutter::RasterCache cache(threshold); + + SkMatrix matrix = SkMatrix::I(); + + auto display_list = GetSampleDisplayList(); + + SkCanvas dummy_canvas; + + PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + + cache.PrepareNewFrame(); + + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, + display_list.get(), true, false, matrix)); + + ASSERT_FALSE(cache.Draw(*display_list, dummy_canvas)); +} + +TEST(RasterCache, PictureCacheLimitPerFrameIsRespectedWhenZeroForSkPicture) { size_t picture_cache_limit_per_frame = 0; flutter::RasterCache cache(3, picture_cache_limit_per_frame); @@ -95,13 +243,35 @@ TEST(RasterCache, PictureCacheLimitPerFrameIsRespectedWhenZero) { PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + cache.PrepareNewFrame(); + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, picture.get(), true, false, matrix)); ASSERT_FALSE(cache.Draw(*picture, dummy_canvas)); } -TEST(RasterCache, SweepsRemoveUnusedFrames) { +TEST(RasterCache, PictureCacheLimitPerFrameIsRespectedWhenZeroForDisplayList) { + size_t picture_cache_limit_per_frame = 0; + flutter::RasterCache cache(3, picture_cache_limit_per_frame); + + SkMatrix matrix = SkMatrix::I(); + + auto display_list = GetSampleDisplayList(); + + SkCanvas dummy_canvas; + + PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + + cache.PrepareNewFrame(); + + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, + display_list.get(), true, false, matrix)); + + ASSERT_FALSE(cache.Draw(*display_list, dummy_canvas)); +} + +TEST(RasterCache, SweepsRemoveUnusedSkPictures) { size_t threshold = 1; flutter::RasterCache cache(threshold); @@ -113,26 +283,68 @@ TEST(RasterCache, SweepsRemoveUnusedFrames) { PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + cache.PrepareNewFrame(); + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, picture.get(), true, false, matrix)); // 1 ASSERT_FALSE(cache.Draw(*picture, dummy_canvas)); - cache.SweepAfterFrame(); + cache.CleanupAfterFrame(); + cache.PrepareNewFrame(); ASSERT_TRUE(cache.Prepare(&preroll_context_holder.preroll_context, picture.get(), true, false, matrix)); // 2 ASSERT_TRUE(cache.Draw(*picture, dummy_canvas)); - cache.SweepAfterFrame(); - cache.SweepAfterFrame(); // Extra frame without a Get image access. + cache.CleanupAfterFrame(); + + cache.PrepareNewFrame(); + cache.CleanupAfterFrame(); // Extra frame without a Get image access. + + cache.PrepareNewFrame(); ASSERT_FALSE(cache.Draw(*picture, dummy_canvas)); } +TEST(RasterCache, SweepsRemoveUnusedDisplayLists) { + size_t threshold = 1; + flutter::RasterCache cache(threshold); + + SkMatrix matrix = SkMatrix::I(); + + auto display_list = GetSampleDisplayList(); + + SkCanvas dummy_canvas; + + PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + + cache.PrepareNewFrame(); + + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, + display_list.get(), true, false, matrix)); // 1 + ASSERT_FALSE(cache.Draw(*display_list, dummy_canvas)); + + cache.CleanupAfterFrame(); + cache.PrepareNewFrame(); + + ASSERT_TRUE(cache.Prepare(&preroll_context_holder.preroll_context, + display_list.get(), true, false, matrix)); // 2 + ASSERT_TRUE(cache.Draw(*display_list, dummy_canvas)); + + cache.CleanupAfterFrame(); + + cache.PrepareNewFrame(); + cache.CleanupAfterFrame(); // Extra frame without a Get image access. + + cache.PrepareNewFrame(); + + ASSERT_FALSE(cache.Draw(*display_list, dummy_canvas)); +} + // 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) { +TEST(RasterCache, DeviceRectRoundOutForSkPicture) { size_t threshold = 1; flutter::RasterCache cache(threshold); @@ -151,10 +363,15 @@ TEST(RasterCache, DeviceRectRoundOut) { PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + cache.PrepareNewFrame(); + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, picture.get(), true, false, ctm)); ASSERT_FALSE(cache.Draw(*picture, canvas)); - cache.SweepAfterFrame(); + + cache.CleanupAfterFrame(); + cache.PrepareNewFrame(); + ASSERT_TRUE(cache.Prepare(&preroll_context_holder.preroll_context, picture.get(), true, false, ctm)); ASSERT_TRUE(cache.Draw(*picture, canvas)); @@ -163,5 +380,167 @@ TEST(RasterCache, DeviceRectRoundOut) { ASSERT_TRUE(cache.Draw(*picture, canvas)); } +// 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, DeviceRectRoundOutForDisplayList) { + size_t threshold = 1; + flutter::RasterCache cache(threshold); + + SkRect logical_rect = SkRect::MakeLTRB(28, 0, 354.56731, 310.288); + DisplayListBuilder builder(logical_rect); + builder.setColor(SK_ColorRED); + builder.drawRect(logical_rect); + sk_sp display_list = builder.Build(); + + SkMatrix ctm = SkMatrix::MakeAll(1.3312, 0, 233, 0, 1.3312, 206, 0, 0, 1); + + SkCanvas canvas(100, 100, nullptr); + canvas.setMatrix(ctm); + + PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + + cache.PrepareNewFrame(); + + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, + display_list.get(), true, false, ctm)); + ASSERT_FALSE(cache.Draw(*display_list, canvas)); + + cache.CleanupAfterFrame(); + cache.PrepareNewFrame(); + + ASSERT_TRUE(cache.Prepare(&preroll_context_holder.preroll_context, + display_list.get(), true, false, ctm)); + ASSERT_TRUE(cache.Draw(*display_list, canvas)); + + canvas.translate(248, 0); + ASSERT_TRUE(cache.Draw(*display_list, canvas)); +} + +TEST(RasterCache, NestedOpCountMetricUsedForSkPicture) { + size_t threshold = 1; + flutter::RasterCache cache(threshold); + + SkMatrix matrix = SkMatrix::I(); + + auto picture = GetSampleNestedPicture(); + ASSERT_EQ(picture->approximateOpCount(), 1); + ASSERT_EQ(picture->approximateOpCount(true), 36); + + SkCanvas dummy_canvas; + + PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + + cache.PrepareNewFrame(); + + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, + picture.get(), false, false, matrix)); + ASSERT_FALSE(cache.Draw(*picture, dummy_canvas)); + + cache.CleanupAfterFrame(); + cache.PrepareNewFrame(); + + ASSERT_TRUE(cache.Prepare(&preroll_context_holder.preroll_context, + picture.get(), false, false, matrix)); + ASSERT_TRUE(cache.Draw(*picture, dummy_canvas)); +} + +TEST(RasterCache, NestedOpCountMetricUsedForDisplayList) { + size_t threshold = 1; + flutter::RasterCache cache(threshold); + + SkMatrix matrix = SkMatrix::I(); + + auto display_list = GetSampleNestedDisplayList(); + ASSERT_EQ(display_list->op_count(), 1); + ASSERT_EQ(display_list->op_count(true), 36); + + SkCanvas dummy_canvas; + + PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + + cache.PrepareNewFrame(); + + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, + display_list.get(), false, false, matrix)); + ASSERT_FALSE(cache.Draw(*display_list, dummy_canvas)); + + cache.CleanupAfterFrame(); + cache.PrepareNewFrame(); + + ASSERT_TRUE(cache.Prepare(&preroll_context_holder.preroll_context, + display_list.get(), false, false, matrix)); + ASSERT_TRUE(cache.Draw(*display_list, dummy_canvas)); +} + +TEST(RasterCache, SkPictureWithSingularMatrixIsNotCached) { + size_t threshold = 2; + flutter::RasterCache cache(threshold); + + SkMatrix matrices[] = { + SkMatrix::Scale(0, 1), + SkMatrix::Scale(1, 0), + SkMatrix::Skew(1, 1), + }; + int matrixCount = sizeof(matrices) / sizeof(matrices[0]); + + auto picture = GetSamplePicture(); + + SkCanvas dummy_canvas; + + PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + + for (int i = 0; i < 10; i++) { + cache.PrepareNewFrame(); + + for (int j = 0; j < matrixCount; j++) { + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, + picture.get(), true, false, matrices[j])); + } + + for (int j = 0; j < matrixCount; j++) { + dummy_canvas.setMatrix(matrices[j]); + ASSERT_FALSE(cache.Draw(*picture, dummy_canvas)); + } + + cache.CleanupAfterFrame(); + } +} + +TEST(RasterCache, DisplayListWithSingularMatrixIsNotCached) { + size_t threshold = 2; + flutter::RasterCache cache(threshold); + + SkMatrix matrices[] = { + SkMatrix::Scale(0, 1), + SkMatrix::Scale(1, 0), + SkMatrix::Skew(1, 1), + }; + int matrixCount = sizeof(matrices) / sizeof(matrices[0]); + + auto display_list = GetSampleDisplayList(); + + SkCanvas dummy_canvas; + + PrerollContextHolder preroll_context_holder = GetSamplePrerollContextHolder(); + + for (int i = 0; i < 10; i++) { + cache.PrepareNewFrame(); + + for (int j = 0; j < matrixCount; j++) { + ASSERT_FALSE(cache.Prepare(&preroll_context_holder.preroll_context, + display_list.get(), true, false, matrices[j])); + } + + for (int j = 0; j < matrixCount; j++) { + dummy_canvas.setMatrix(matrices[j]); + ASSERT_FALSE(cache.Draw(*display_list, dummy_canvas)); + } + + cache.CleanupAfterFrame(); + } +} + } // namespace testing + } // namespace flutter diff --git a/flow/surface_frame.cc b/flow/surface_frame.cc index bd49326782649..c7512b40487d6 100644 --- a/flow/surface_frame.cc +++ b/flow/surface_frame.cc @@ -8,21 +8,21 @@ namespace flutter { SurfaceFrame::SurfaceFrame(sk_sp surface, - bool supports_readback, + FramebufferInfo framebuffer_info, const SubmitCallback& submit_callback) : surface_(surface), - supports_readback_(supports_readback), + framebuffer_info_(std::move(framebuffer_info)), submit_callback_(submit_callback) { FML_DCHECK(submit_callback_); } SurfaceFrame::SurfaceFrame(sk_sp surface, - bool supports_readback, + FramebufferInfo framebuffer_info, const SubmitCallback& submit_callback, std::unique_ptr context_result) : submitted_(false), surface_(surface), - supports_readback_(supports_readback), + framebuffer_info_(std::move(framebuffer_info)), submit_callback_(submit_callback), context_result_(std::move(context_result)) { FML_DCHECK(submit_callback_); diff --git a/flow/surface_frame.h b/flow/surface_frame.h index ad4b3c95a95d2..0e15cde4908e7 100644 --- a/flow/surface_frame.h +++ b/flow/surface_frame.h @@ -6,6 +6,7 @@ #define FLUTTER_FLOW_SURFACE_FRAME_H_ #include +#include #include "flutter/common/graphics/gl_context_switch.h" #include "flutter/fml/macros.h" @@ -21,17 +22,52 @@ class SurfaceFrame { using SubmitCallback = std::function; + // Information about the underlying framebuffer + struct FramebufferInfo { + // Indicates whether or not the surface supports pixel readback as used in + // circumstances such as a BackdropFilter. + bool supports_readback = false; + + // This is the area of framebuffer that lags behind the front buffer. + // + // Correctly providing exiting_damage is necessary for supporting double and + // triple buffering. Embedder is responsible for tracking this area for each + // of the back buffers used. When doing partial redraw, this area will be + // repainted alongside of dirty area determined by diffing current and + // last successfully rasterized layer tree; + // + // If existing damage is unspecified (nullopt), entire frame will be + // rasterized (no partial redraw). To signal that there is no existing + // damage use an empty SkIRect. + std::optional existing_damage; + }; + SurfaceFrame(sk_sp surface, - bool supports_readback, + FramebufferInfo framebuffer_info, const SubmitCallback& submit_callback); SurfaceFrame(sk_sp surface, - bool supports_readback, + FramebufferInfo framebuffer_info, const SubmitCallback& submit_callback, std::unique_ptr context_result); ~SurfaceFrame(); + struct SubmitInfo { + // The frame damage for frame n is the difference between frame n and + // frame (n-1), and represents the area that a compositor must recompose. + // + // Corresponds to EGL_KHR_swap_buffers_with_damage + std::optional frame_damage; + + // The buffer damage for a frame is the area changed since that same buffer + // was last used. If the buffer has not been used before, the buffer damage + // is the entire area of the buffer. + // + // Corresponds to EGL_KHR_partial_update + std::optional buffer_damage; + }; + bool Submit(); bool IsSubmitted() const; @@ -40,12 +76,18 @@ class SurfaceFrame { sk_sp SkiaSurface() const; - bool supports_readback() { return supports_readback_; } + const FramebufferInfo& framebuffer_info() const { return framebuffer_info_; } + + void set_submit_info(const SubmitInfo& submit_info) { + submit_info_ = submit_info; + } + const SubmitInfo& submit_info() const { return submit_info_; } private: bool submitted_ = false; sk_sp surface_; - bool supports_readback_; + FramebufferInfo framebuffer_info_; + SubmitInfo submit_info_; SubmitCallback submit_callback_; std::unique_ptr context_result_; diff --git a/flow/testing/diff_context_test.cc b/flow/testing/diff_context_test.cc index 0ed8c9804ebb9..7276dd72c124f 100644 --- a/flow/testing/diff_context_test.cc +++ b/flow/testing/diff_context_test.cc @@ -7,8 +7,6 @@ namespace flutter { namespace testing { -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - DiffContextTest::DiffContextTest() : unref_queue_(fml::MakeRefCounted( GetCurrentTaskRunner(), @@ -77,7 +75,5 @@ std::shared_ptr DiffContextTest::CreateOpacityLater( return res; } -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - } // namespace testing } // namespace flutter diff --git a/flow/testing/diff_context_test.h b/flow/testing/diff_context_test.h index 42c8d0e7710f8..578661e778af7 100644 --- a/flow/testing/diff_context_test.h +++ b/flow/testing/diff_context_test.h @@ -13,8 +13,6 @@ namespace flutter { namespace testing { -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - class MockLayerTree { public: explicit MockLayerTree(SkISize size = SkISize::Make(1000, 1000)) @@ -77,7 +75,5 @@ class DiffContextTest : public ThreadTest { fml::RefPtr unref_queue_; }; -#endif // FLUTTER_ENABLE_DIFF_CONTEXT - } // namespace testing } // namespace flutter diff --git a/flow/testing/gl_context_switch_test.h b/flow/testing/gl_context_switch_test.h index e2cd8b96c0c35..f3d06daf6b138 100644 --- a/flow/testing/gl_context_switch_test.h +++ b/flow/testing/gl_context_switch_test.h @@ -20,7 +20,7 @@ class GLContextSwitchTest : public ::testing::Test { /// The renderer context used for testing class TestSwitchableGLContext : public SwitchableGLContext { public: - TestSwitchableGLContext(int context); + explicit TestSwitchableGLContext(int context); ~TestSwitchableGLContext() override; diff --git a/flow/testing/mock_layer.cc b/flow/testing/mock_layer.cc index 0c6ef36e899c6..dff041d36c95a 100644 --- a/flow/testing/mock_layer.cc +++ b/flow/testing/mock_layer.cc @@ -16,8 +16,6 @@ MockLayer::MockLayer(SkPath path, fake_has_platform_view_(fake_has_platform_view), fake_reads_surface_(fake_reads_surface) {} -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - bool MockLayer::IsReplacing(DiffContext* context, const Layer* layer) const { // Similar to PictureLayer, only return true for identical mock layers; // That way ContainerLayer::DiffChildren can properly detect mock layer @@ -33,8 +31,6 @@ void MockLayer::Diff(DiffContext* context, const Layer* old_layer) { context->SetLayerPaintRegion(this, context->CurrentSubtreeRegion()); } -#endif - void MockLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { parent_mutators_ = context->mutators_stack; parent_matrix_ = matrix; diff --git a/flow/testing/mock_layer.h b/flow/testing/mock_layer.h index aa41fe59faed1..77a01854a0029 100644 --- a/flow/testing/mock_layer.h +++ b/flow/testing/mock_layer.h @@ -16,10 +16,10 @@ namespace testing { // verify the data against expected values. class MockLayer : public Layer { public: - MockLayer(SkPath path, - SkPaint paint = SkPaint(), - bool fake_has_platform_view = false, - bool fake_reads_surface = false); + explicit MockLayer(SkPath path, + SkPaint paint = SkPaint(), + bool fake_has_platform_view = false, + bool fake_reads_surface = false); void Preroll(PrerollContext* context, const SkMatrix& matrix) override; void Paint(PaintContext& context) const override; @@ -29,14 +29,10 @@ class MockLayer : public Layer { const SkRect& parent_cull_rect() { return parent_cull_rect_; } bool parent_has_platform_view() { return parent_has_platform_view_; } -#ifdef FLUTTER_ENABLE_DIFF_CONTEXT - bool IsReplacing(DiffContext* context, const Layer* layer) const override; void Diff(DiffContext* context, const Layer* old_layer) override; const MockLayer* as_mock_layer() const override { return this; } -#endif - private: MutatorsStack parent_mutators_; SkMatrix parent_matrix_; diff --git a/flow/testing/mock_raster_cache.cc b/flow/testing/mock_raster_cache.cc index 38a4c58448f2d..f9c1f5e6c2747 100644 --- a/flow/testing/mock_raster_cache.cc +++ b/flow/testing/mock_raster_cache.cc @@ -10,7 +10,7 @@ namespace flutter { namespace testing { MockRasterCacheResult::MockRasterCacheResult(SkIRect device_rect) - : RasterCacheResult(nullptr, SkRect::MakeEmpty()), + : RasterCacheResult(nullptr, SkRect::MakeEmpty(), "RasterCacheFlow::test"), device_rect_(device_rect) {} std::unique_ptr MockRasterCache::RasterizePicture( diff --git a/flow/testing/mock_raster_cache.h b/flow/testing/mock_raster_cache.h index 835e58c3b2d9e..f51f700b3d224 100644 --- a/flow/testing/mock_raster_cache.h +++ b/flow/testing/mock_raster_cache.h @@ -24,7 +24,7 @@ namespace testing { */ class MockRasterCacheResult : public RasterCacheResult { public: - MockRasterCacheResult(SkIRect device_rect); + explicit MockRasterCacheResult(SkIRect device_rect); void draw(SkCanvas& canvas, const SkPaint* paint = nullptr) const override{}; diff --git a/fml/closure.h b/fml/closure.h index ca7f60fdbbd32..7631f3a7be439 100644 --- a/fml/closure.h +++ b/fml/closure.h @@ -33,7 +33,8 @@ class ScopedCleanupClosure { public: ScopedCleanupClosure() = default; - ScopedCleanupClosure(const fml::closure& closure) : closure_(closure) {} + explicit ScopedCleanupClosure(const fml::closure& closure) + : closure_(closure) {} ~ScopedCleanupClosure() { if (closure_) { diff --git a/fml/concurrent_message_loop.h b/fml/concurrent_message_loop.h index ebd8de983508d..b79f3091767e4 100644 --- a/fml/concurrent_message_loop.h +++ b/fml/concurrent_message_loop.h @@ -46,7 +46,7 @@ class ConcurrentMessageLoop std::map> thread_tasks_; bool shutdown_ = false; - ConcurrentMessageLoop(size_t worker_count); + explicit ConcurrentMessageLoop(size_t worker_count); void WorkerMain(); @@ -61,7 +61,7 @@ class ConcurrentMessageLoop class ConcurrentTaskRunner : public BasicTaskRunner { public: - ConcurrentTaskRunner(std::weak_ptr weak_loop); + explicit ConcurrentTaskRunner(std::weak_ptr weak_loop); virtual ~ConcurrentTaskRunner(); diff --git a/fml/file_unittest.cc b/fml/file_unittest.cc index f25f4f1966dd1..c0081383273a8 100644 --- a/fml/file_unittest.cc +++ b/fml/file_unittest.cc @@ -269,6 +269,36 @@ TEST(FileTest, EmptyMappingTest) { ASSERT_TRUE(fml::UnlinkFile(dir.fd(), "my_contents")); } +TEST(FileTest, MappingDontNeedSafeTest) { + fml::ScopedTemporaryDirectory dir; + + { + auto file = fml::OpenFile(dir.fd(), "my_contents", true, + fml::FilePermission::kReadWrite); + WriteStringToFile(file, "some content"); + } + + { + auto file = fml::OpenFile(dir.fd(), "my_contents", false, + fml::FilePermission::kRead); + fml::FileMapping mapping(file); + ASSERT_TRUE(mapping.IsValid()); + ASSERT_EQ(mapping.GetMutableMapping(), nullptr); + ASSERT_TRUE(mapping.IsDontNeedSafe()); + } + + { + auto file = fml::OpenFile(dir.fd(), "my_contents", false, + fml::FilePermission::kReadWrite); + fml::FileMapping mapping(file, {fml::FileMapping::Protection::kRead, + fml::FileMapping::Protection::kWrite}); + ASSERT_TRUE(mapping.IsValid()); + ASSERT_NE(mapping.GetMutableMapping(), nullptr); + ASSERT_FALSE(mapping.IsDontNeedSafe()); + } + ASSERT_TRUE(fml::UnlinkFile(dir.fd(), "my_contents")); +} + TEST(FileTest, FileTestsWork) { fml::ScopedTemporaryDirectory dir; ASSERT_TRUE(dir.fd().is_valid()); diff --git a/fml/log_settings.h b/fml/log_settings.h index 2921ebe8cc30f..6a3664f0d3989 100644 --- a/fml/log_settings.h +++ b/fml/log_settings.h @@ -37,7 +37,7 @@ int GetMinLogLevel(); class ScopedSetLogSettings { public: - ScopedSetLogSettings(const LogSettings& settings); + explicit ScopedSetLogSettings(const LogSettings& settings); ~ScopedSetLogSettings(); private: diff --git a/fml/mapping.cc b/fml/mapping.cc index 254ccafc441ea..1b3f1e65d13bd 100644 --- a/fml/mapping.cc +++ b/fml/mapping.cc @@ -81,11 +81,19 @@ const uint8_t* DataMapping::GetMapping() const { return data_.data(); } +bool DataMapping::IsDontNeedSafe() const { + return false; +} + // NonOwnedMapping NonOwnedMapping::NonOwnedMapping(const uint8_t* data, size_t size, - const ReleaseProc& release_proc) - : data_(data), size_(size), release_proc_(release_proc) {} + const ReleaseProc& release_proc, + bool dontneed_safe) + : data_(data), + size_(size), + release_proc_(release_proc), + dontneed_safe_(dontneed_safe) {} NonOwnedMapping::~NonOwnedMapping() { if (release_proc_) { @@ -101,6 +109,10 @@ const uint8_t* NonOwnedMapping::GetMapping() const { return data_; } +bool NonOwnedMapping::IsDontNeedSafe() const { + return dontneed_safe_; +} + // MallocMapping MallocMapping::MallocMapping() : data_(nullptr), size_(0) {} @@ -134,6 +146,10 @@ const uint8_t* MallocMapping::GetMapping() const { return data_; } +bool MallocMapping::IsDontNeedSafe() const { + return false; +} + uint8_t* MallocMapping::Release() { uint8_t* result = data_; data_ = nullptr; @@ -174,4 +190,8 @@ const uint8_t* SymbolMapping::GetMapping() const { return mapping_; } +bool SymbolMapping::IsDontNeedSafe() const { + return true; +} + } // namespace fml diff --git a/fml/mapping.h b/fml/mapping.h index cd1be8e47d73d..fd46eb728c166 100644 --- a/fml/mapping.h +++ b/fml/mapping.h @@ -28,6 +28,10 @@ class Mapping { virtual const uint8_t* GetMapping() const = 0; + // Whether calling madvise(DONTNEED) on the mapping is non-destructive. + // Generally true for file-mapped memory and false for anonymous memory. + virtual bool IsDontNeedSafe() const = 0; + private: FML_DISALLOW_COPY_AND_ASSIGN(Mapping); }; @@ -40,9 +44,9 @@ class FileMapping final : public Mapping { kExecute, }; - FileMapping(const fml::UniqueFD& fd, - std::initializer_list protection = { - Protection::kRead}); + explicit FileMapping(const fml::UniqueFD& fd, + std::initializer_list protection = { + Protection::kRead}); ~FileMapping() override; @@ -65,6 +69,9 @@ class FileMapping final : public Mapping { // |Mapping| const uint8_t* GetMapping() const override; + // |Mapping| + bool IsDontNeedSafe() const override; + uint8_t* GetMutableMapping(); bool IsValid() const; @@ -84,9 +91,9 @@ class FileMapping final : public Mapping { class DataMapping final : public Mapping { public: - DataMapping(std::vector data); + explicit DataMapping(std::vector data); - DataMapping(const std::string& string); + explicit DataMapping(const std::string& string); ~DataMapping() override; @@ -96,6 +103,9 @@ class DataMapping final : public Mapping { // |Mapping| const uint8_t* GetMapping() const override; + // |Mapping| + bool IsDontNeedSafe() const override; + private: std::vector data_; @@ -107,7 +117,8 @@ class NonOwnedMapping final : public Mapping { using ReleaseProc = std::function; NonOwnedMapping(const uint8_t* data, size_t size, - const ReleaseProc& release_proc = nullptr); + const ReleaseProc& release_proc = nullptr, + bool dontneed_safe = false); ~NonOwnedMapping() override; @@ -117,10 +128,14 @@ class NonOwnedMapping final : public Mapping { // |Mapping| const uint8_t* GetMapping() const override; + // |Mapping| + bool IsDontNeedSafe() const override; + private: const uint8_t* const data_; const size_t size_; const ReleaseProc release_proc_; + const bool dontneed_safe_; FML_DISALLOW_COPY_AND_ASSIGN(NonOwnedMapping); }; @@ -162,6 +177,9 @@ class MallocMapping final : public Mapping { // |Mapping| const uint8_t* GetMapping() const override; + // |Mapping| + bool IsDontNeedSafe() const override; + /// Removes ownership of the data buffer. /// After this is called; the mapping will point to nullptr. [[nodiscard]] uint8_t* Release(); @@ -186,6 +204,9 @@ class SymbolMapping final : public Mapping { // |Mapping| const uint8_t* GetMapping() const override; + // |Mapping| + bool IsDontNeedSafe() const override; + private: fml::RefPtr native_library_; const uint8_t* mapping_ = nullptr; diff --git a/fml/mapping_unittests.cc b/fml/mapping_unittests.cc index 0a7309ccf3c4c..7dc397dbde14e 100644 --- a/fml/mapping_unittests.cc +++ b/fml/mapping_unittests.cc @@ -25,7 +25,8 @@ TEST(MallocMapping, MoveConstructor) { MallocMapping mapping(reinterpret_cast(malloc(length)), length); MallocMapping moved = std::move(mapping); - ASSERT_EQ(nullptr, mapping.GetMapping()); + ASSERT_EQ(nullptr, + mapping.GetMapping()); // NOLINT(clang-analyzer-cplusplus.Move) ASSERT_EQ(0u, mapping.GetSize()); ASSERT_NE(nullptr, moved.GetMapping()); ASSERT_EQ(length, moved.GetSize()); @@ -52,4 +53,11 @@ TEST(MallocMapping, Release) { ASSERT_EQ(0u, mapping.GetSize()); } +TEST(MallocMapping, IsDontNeedSafe) { + size_t length = 10; + MallocMapping mapping(reinterpret_cast(malloc(length)), length); + ASSERT_NE(nullptr, mapping.GetMapping()); + ASSERT_FALSE(mapping.IsDontNeedSafe()); +} + } // namespace fml diff --git a/fml/memory/ref_ptr.h b/fml/memory/ref_ptr.h index 1c8efd4e0cf73..c426a4901d7a2 100644 --- a/fml/memory/ref_ptr.h +++ b/fml/memory/ref_ptr.h @@ -65,8 +65,8 @@ template class RefPtr final { public: RefPtr() : ptr_(nullptr) {} - RefPtr(std::nullptr_t) - : ptr_(nullptr) {} // NOLINT(google-explicit-constructor) + RefPtr(std::nullptr_t) // NOLINT(google-explicit-constructor) + : ptr_(nullptr) {} // Explicit constructor from a plain pointer (to an object that must have // already been adopted). (Note that in |T::T()|, references to |this| cannot @@ -80,22 +80,25 @@ class RefPtr final { } // Copy constructor. - RefPtr(const RefPtr& r) : ptr_(r.ptr_) { + RefPtr(const RefPtr& r) // NOLINT(google-explicit-constructor) + : ptr_(r.ptr_) { if (ptr_) { ptr_->AddRef(); } } template - RefPtr(const RefPtr& r) - : ptr_(r.ptr_) { // NOLINT(google-explicit-constructor) + RefPtr(const RefPtr& r) // NOLINT(google-explicit-constructor) + : ptr_(r.ptr_) { if (ptr_) { ptr_->AddRef(); } } // Move constructor. - RefPtr(RefPtr&& r) : ptr_(r.ptr_) { r.ptr_ = nullptr; } + RefPtr(RefPtr&& r) : ptr_(r.ptr_) { // NOLINT(google-explicit-constructor) + r.ptr_ = nullptr; + } template RefPtr(RefPtr&& r) : ptr_(r.ptr_) { // NOLINT(google-explicit-constructor) diff --git a/fml/memory/weak_ptr.h b/fml/memory/weak_ptr.h index caaecaff2f18d..2ff448ea36fa2 100644 --- a/fml/memory/weak_ptr.h +++ b/fml/memory/weak_ptr.h @@ -49,17 +49,18 @@ class WeakPtr { WeakPtr() : ptr_(nullptr) {} // Copy constructor. - explicit WeakPtr(const WeakPtr& r) = default; + // NOLINTNEXTLINE(google-explicit-constructor) + WeakPtr(const WeakPtr& r) = default; template - WeakPtr(const WeakPtr& r) + WeakPtr(const WeakPtr& r) // NOLINT(google-explicit-constructor) : ptr_(static_cast(r.ptr_)), flag_(r.flag_), checker_(r.checker_) {} // Move constructor. WeakPtr(WeakPtr&& r) = default; template - WeakPtr(WeakPtr&& r) + WeakPtr(WeakPtr&& r) // NOLINT(google-explicit-constructor) : ptr_(static_cast(r.ptr_)), flag_(std::move(r.flag_)), checker_(r.checker_) {} @@ -155,12 +156,14 @@ class TaskRunnerAffineWeakPtr : public WeakPtr { TaskRunnerAffineWeakPtr(const TaskRunnerAffineWeakPtr& r) = default; template + // NOLINTNEXTLINE(google-explicit-constructor) TaskRunnerAffineWeakPtr(const TaskRunnerAffineWeakPtr& r) : WeakPtr(r), checker_(r.checker_) {} TaskRunnerAffineWeakPtr(TaskRunnerAffineWeakPtr&& r) = default; template + // NOLINTNEXTLINE(google-explicit-constructor) TaskRunnerAffineWeakPtr(TaskRunnerAffineWeakPtr&& r) : WeakPtr(r), checker_(r.checker_) {} diff --git a/fml/native_library.h b/fml/native_library.h index a6174b9eb60d0..175afed3edc3a 100644 --- a/fml/native_library.h +++ b/fml/native_library.h @@ -56,7 +56,7 @@ class NativeLibrary : public fml::RefCountedThreadSafe { Handle handle_ = nullptr; bool close_handle_ = true; - NativeLibrary(const char* path); + explicit NativeLibrary(const char* path); NativeLibrary(Handle handle, bool close_handle); diff --git a/fml/platform/android/jni_util.cc b/fml/platform/android/jni_util.cc index d5937c90af77b..86942d20e9f8d 100644 --- a/fml/platform/android/jni_util.cc +++ b/fml/platform/android/jni_util.cc @@ -125,6 +125,35 @@ std::vector StringArrayToVector(JNIEnv* env, jobjectArray array) { return out; } +std::vector StringListToVector(JNIEnv* env, jobject list) { + std::vector out; + if (env == nullptr || list == nullptr) { + return out; + } + + ScopedJavaLocalRef list_clazz(env, env->FindClass("java/util/List")); + FML_DCHECK(!list_clazz.is_null()); + + jmethodID list_get = + env->GetMethodID(list_clazz.obj(), "get", "(I)Ljava/lang/Object;"); + jmethodID list_size = env->GetMethodID(list_clazz.obj(), "size", "()I"); + + jint size = env->CallIntMethod(list, list_size); + + if (size == 0) { + return out; + } + + out.resize(size); + for (jint i = 0; i < size; ++i) { + ScopedJavaLocalRef java_string( + env, static_cast(env->CallObjectMethod(list, list_get, i))); + out[i] = JavaStringToString(env, java_string.obj()); + } + + return out; +} + ScopedJavaLocalRef VectorToStringArray( JNIEnv* env, const std::vector& vector) { diff --git a/fml/platform/android/jni_util.h b/fml/platform/android/jni_util.h index da056f627bc2e..0fb2be7c2fa4f 100644 --- a/fml/platform/android/jni_util.h +++ b/fml/platform/android/jni_util.h @@ -30,6 +30,8 @@ ScopedJavaLocalRef StringToJavaString(JNIEnv* env, std::vector StringArrayToVector(JNIEnv* env, jobjectArray jargs); +std::vector StringListToVector(JNIEnv* env, jobject list); + ScopedJavaLocalRef VectorToStringArray( JNIEnv* env, const std::vector& vector); diff --git a/fml/platform/darwin/message_loop_darwin.h b/fml/platform/darwin/message_loop_darwin.h index fb4c178c071ba..fb43ca5af7053 100644 --- a/fml/platform/darwin/message_loop_darwin.h +++ b/fml/platform/darwin/message_loop_darwin.h @@ -16,6 +16,12 @@ namespace fml { class MessageLoopDarwin : public MessageLoopImpl { + public: + // A custom CFRunLoop mode used when processing flutter messages, + // so that the CFRunLoop can be run without being interrupted by UIKit, + // while still being able to receive and be interrupted by framework messages. + static CFStringRef kMessageLoopCFRunLoopMode; + private: std::atomic_bool running_; CFRef delayed_wake_timer_; diff --git a/fml/platform/darwin/message_loop_darwin.mm b/fml/platform/darwin/message_loop_darwin.mm index ca714c81a4bbd..969ff8b46705e 100644 --- a/fml/platform/darwin/message_loop_darwin.mm +++ b/fml/platform/darwin/message_loop_darwin.mm @@ -13,6 +13,8 @@ static constexpr CFTimeInterval kDistantFuture = 1.0e10; +CFStringRef MessageLoopDarwin::kMessageLoopCFRunLoopMode = CFSTR("fmlMessageLoop"); + MessageLoopDarwin::MessageLoopDarwin() : running_(false), loop_((CFRunLoopRef)CFRetain(CFRunLoopGetCurrent())) { FML_DCHECK(loop_ != nullptr); @@ -29,11 +31,14 @@ &timer_context /* context */)); FML_DCHECK(delayed_wake_timer_ != nullptr); CFRunLoopAddTimer(loop_, delayed_wake_timer_, kCFRunLoopCommonModes); + // This mode will be used by FlutterKeyboardManager. + CFRunLoopAddTimer(loop_, delayed_wake_timer_, kMessageLoopCFRunLoopMode); } MessageLoopDarwin::~MessageLoopDarwin() { CFRunLoopTimerInvalidate(delayed_wake_timer_); CFRunLoopRemoveTimer(loop_, delayed_wake_timer_, kCFRunLoopCommonModes); + CFRunLoopRemoveTimer(loop_, delayed_wake_timer_, kMessageLoopCFRunLoopMode); } void MessageLoopDarwin::Run() { diff --git a/fml/platform/darwin/string_range_sanitization.mm b/fml/platform/darwin/string_range_sanitization.mm index 8100ba9154bf7..bed509508feba 100644 --- a/fml/platform/darwin/string_range_sanitization.mm +++ b/fml/platform/darwin/string_range_sanitization.mm @@ -10,8 +10,9 @@ NSRange RangeForCharacterAtIndex(NSString* text, NSUInteger index) { if (text == nil || index > text.length) { return NSMakeRange(NSNotFound, 0); } - if (index < text.length) + if (index < text.length) { return [text rangeOfComposedCharacterSequenceAtIndex:index]; + } return NSMakeRange(index, 0); } diff --git a/fml/platform/linux/message_loop_linux.cc b/fml/platform/linux/message_loop_linux.cc index 5ed41aef8fa4e..6e6febc2201f6 100644 --- a/fml/platform/linux/message_loop_linux.cc +++ b/fml/platform/linux/message_loop_linux.cc @@ -81,6 +81,7 @@ void MessageLoopLinux::Terminate() { // |fml::MessageLoopImpl| void MessageLoopLinux::WakeUp(fml::TimePoint time_point) { bool result = TimerRearm(timer_fd_.get(), time_point); + (void)result; FML_DCHECK(result); } diff --git a/fml/platform/linux/timerfd.cc b/fml/platform/linux/timerfd.cc index 166b861909663..39d4f8a59c68d 100644 --- a/fml/platform/linux/timerfd.cc +++ b/fml/platform/linux/timerfd.cc @@ -44,7 +44,7 @@ bool TimerRearm(int fd, fml::TimePoint time_point) { } struct itimerspec spec = {}; - spec.it_value.tv_sec = (time_t)(nano_secs / NSEC_PER_SEC); + spec.it_value.tv_sec = static_cast(nano_secs / NSEC_PER_SEC); spec.it_value.tv_nsec = nano_secs % NSEC_PER_SEC; spec.it_interval = spec.it_value; // single expiry. diff --git a/fml/platform/posix/mapping_posix.cc b/fml/platform/posix/mapping_posix.cc index 535a72ebe4ccf..aff3d645d3a5b 100644 --- a/fml/platform/posix/mapping_posix.cc +++ b/fml/platform/posix/mapping_posix.cc @@ -100,6 +100,10 @@ const uint8_t* FileMapping::GetMapping() const { return mapping_; } +bool FileMapping::IsDontNeedSafe() const { + return mutable_mapping_ == nullptr; +} + bool FileMapping::IsValid() const { return valid_; } diff --git a/fml/platform/win/mapping_win.cc b/fml/platform/win/mapping_win.cc index b32178f54d4e9..6225a9c6d0311 100644 --- a/fml/platform/win/mapping_win.cc +++ b/fml/platform/win/mapping_win.cc @@ -115,6 +115,10 @@ const uint8_t* FileMapping::GetMapping() const { return mapping_; } +bool FileMapping::IsDontNeedSafe() const { + return mutable_mapping_ == nullptr; +} + bool FileMapping::IsValid() const { return valid_; } diff --git a/fml/synchronization/atomic_object.h b/fml/synchronization/atomic_object.h index 9fdbe19c82fce..c5c25cb27cee5 100644 --- a/fml/synchronization/atomic_object.h +++ b/fml/synchronization/atomic_object.h @@ -14,7 +14,7 @@ template class AtomicObject { public: AtomicObject() = default; - AtomicObject(T object) : object_(object) {} + explicit AtomicObject(T object) : object_(object) {} T Load() const { std::scoped_lock lock(mutex_); diff --git a/fml/synchronization/count_down_latch.h b/fml/synchronization/count_down_latch.h index 4fd4377a0dfbc..314db8d6e0c54 100644 --- a/fml/synchronization/count_down_latch.h +++ b/fml/synchronization/count_down_latch.h @@ -14,7 +14,7 @@ namespace fml { class CountDownLatch { public: - CountDownLatch(size_t count); + explicit CountDownLatch(size_t count); ~CountDownLatch(); diff --git a/fml/synchronization/semaphore.cc b/fml/synchronization/semaphore.cc index 9dff734603aa0..548731d6700ea 100644 --- a/fml/synchronization/semaphore.cc +++ b/fml/synchronization/semaphore.cc @@ -107,6 +107,7 @@ class PlatformSemaphore { ~PlatformSemaphore() { if (valid_) { int result = ::sem_destroy(&sem_); + (void)result; // Can only be EINVAL which should not be possible since we checked for // validity. FML_DCHECK(result == 0); diff --git a/fml/task_queue_id.h b/fml/task_queue_id.h index 2395f9db92be6..3b37e16bd9f4e 100644 --- a/fml/task_queue_id.h +++ b/fml/task_queue_id.h @@ -21,7 +21,9 @@ class TaskQueueId { /// Intializes a task queue with the given value as it's ID. explicit TaskQueueId(size_t value) : value_(value) {} - operator size_t() const { return value_; } + operator size_t() const { // NOLINT(google-explicit-constructor) + return value_; + } private: size_t value_ = kUnmerged; diff --git a/fml/task_runner.h b/fml/task_runner.h index d65c3e919b8a6..cdab31181ebac 100644 --- a/fml/task_runner.h +++ b/fml/task_runner.h @@ -63,7 +63,7 @@ class TaskRunner : public fml::RefCountedThreadSafe, const fml::closure& task); protected: - TaskRunner(fml::RefPtr loop); + explicit TaskRunner(fml::RefPtr loop); private: fml::RefPtr loop_; diff --git a/fml/thread_local.h b/fml/thread_local.h index 72ef80c8eb326..ed6336c961554 100644 --- a/fml/thread_local.h +++ b/fml/thread_local.h @@ -26,7 +26,7 @@ namespace internal { class ThreadLocalPointer { public: - ThreadLocalPointer(void (*destroy)(void*)); + explicit ThreadLocalPointer(void (*destroy)(void*)); ~ThreadLocalPointer(); void* get() const; diff --git a/fml/trace_event.h b/fml/trace_event.h index 9e1d0609e7cc6..38a3ea2a9b225 100644 --- a/fml/trace_event.h +++ b/fml/trace_event.h @@ -343,7 +343,7 @@ void TraceEventFlowEnd0(TraceArg category_group, TraceArg name, TraceIDArg id); class ScopedInstantEnd { public: - ScopedInstantEnd(const char* str) : label_(str) {} + explicit ScopedInstantEnd(const char* str) : label_(str) {} ~ScopedInstantEnd() { TraceEventEnd(label_); } @@ -360,7 +360,7 @@ class ScopedInstantEnd { // leads to corrupted or missing traces in the UI. class TraceFlow { public: - TraceFlow(const char* label) : label_(label), nonce_(TraceNonce()) { + explicit TraceFlow(const char* label) : label_(label), nonce_(TraceNonce()) { TraceEventFlowBegin0("flutter", label_, nonce_); } @@ -370,13 +370,13 @@ class TraceFlow { other.nonce_ = 0; } - void Step(const char* label) const { - TraceEventFlowStep0("flutter", label, nonce_); + void Step(const char* label = nullptr) const { + TraceEventFlowStep0("flutter", label ? label : label_, nonce_); } void End(const char* label = nullptr) { if (nonce_ != 0) { - TraceEventFlowEnd0("flutter", label == nullptr ? label_ : label, nonce_); + TraceEventFlowEnd0("flutter", label ? label : label_, nonce_); nonce_ = 0; } } diff --git a/lib/spirv/README.md b/lib/spirv/README.md index 2c3ee0198b6ac..6613b972b1011 100644 --- a/lib/spirv/README.md +++ b/lib/spirv/README.md @@ -19,17 +19,16 @@ the code will need to adhere to the following rules. - The output can only be written to from the main function. - `gl_FragCoord` can only be read from the main function, and its z and w components have no meaning. -- Control flow is prohibited aside from function calls and `return`. - `if`, `while`, `for`, `switch`, etc. +- Control flow is prohibited (`if`, `while`, `for`, `switch`, etc.) aside from function calls and `return`. - No inputs from other shader stages. -- Only float, float-vector types, and square float-matrix types. +- Only sampler2D, float, float-vector types, and square float-matrix types. - Only square matrices are supported. - Only built-in functions present in GLSL ES 100 are used. -- Debug symbols must be stripped, you can use the `spirv-opt` `--strip-debug` flag. +- Only the `texture` function is supported for sampling from a sampler2D object. These rules may become less strict in future versions. Conformant SPIR-V should successfully transpile from the current version onwards. In other words, a SPIR-V shader you use now that meets these rules should keep working, but the output of the transpiler may change for that shader. -Support for textures, control flow, and structured types is planned, but not currently included. +Support for control flow and structured types is planned but not currently included. ## Testing diff --git a/lib/spirv/lib/spirv.dart b/lib/spirv/lib/spirv.dart index 7034c284b862d..352e6262df7da 100644 --- a/lib/spirv/lib/spirv.dart +++ b/lib/spirv/lib/spirv.dart @@ -38,7 +38,15 @@ class TranspileResult { /// The number of float uniforms used in this shader. final int uniformFloatCount; - TranspileResult._(this.src, this.uniformFloatCount, this.language); + /// The number of samplers (children) used in this shader. + final int samplerCount; + + TranspileResult._( + this.src, + this.uniformFloatCount, + this.samplerCount, + this.language, + ); } /// Thrown during transpilation due to malformed or unsupported SPIR-V. @@ -64,6 +72,7 @@ TranspileResult transpile(ByteBuffer spirv, TargetLanguage target) { return TranspileResult._( t.src.toString(), t.uniformFloatCount, + t.samplerCount, target, ); } diff --git a/lib/spirv/lib/src/constants.dart b/lib/spirv/lib/src/constants.dart index 21569d2430296..a4c5a52f28eaf 100644 --- a/lib/spirv/lib/src/constants.dart +++ b/lib/spirv/lib/src/constants.dart @@ -34,6 +34,9 @@ const int _decorationLocation = 30; // Explicitly supported builtin types const int _builtinFragCoord = 15; +// Explicitly supported dimensionalities +const int _dim2D = 1; + // Ops that have no semantic meaning in output and can be safely ignored const int _opSource = 3; const int _opSourceExtension = 4; @@ -55,6 +58,8 @@ const int _opTypeInt = 21; const int _opTypeFloat = 22; const int _opTypeVector = 23; const int _opTypeMatrix = 24; +const int _opTypeImage = 25; +const int _opTypeSampledImage = 27; const int _opTypePointer = 32; const int _opTypeFunction = 33; const int _opConstantTrue = 41; @@ -74,6 +79,8 @@ const int _opDecorate = 71; const int _opVectorShuffle = 79; const int _opCompositeConstruct = 80; const int _opCompositeExtract = 81; +const int _opImageSampleImplicitLod = 87; +const int _opImageQuerySize = 104; const int _opConvertFToS = 110; const int _opConvertSToF = 111; const int _opFNegate = 127; diff --git a/lib/spirv/lib/src/transpiler.dart b/lib/spirv/lib/src/transpiler.dart index 8fa9636fa8a72..999ba82a34507 100644 --- a/lib/spirv/lib/src/transpiler.dart +++ b/lib/spirv/lib/src/transpiler.dart @@ -51,7 +51,7 @@ const String _mainFunctionName = 'main'; /// https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#OpConstant class _Transpiler { _Transpiler(this.spirv, this.target) { - out = src; + out = header; } final Uint32List spirv; @@ -60,6 +60,23 @@ class _Transpiler { /// The resulting source code of the target language is written to src. final StringBuffer src = StringBuffer(); + /// Contains shader header, constants, and uniforms. + final StringBuffer header = StringBuffer(); + + /// The main body of code, contains function definitions. + final StringBuffer body = StringBuffer(); + + /// Uniform declarations. + final Map uniformDeclarations = {}; + + /// Declarations for sampler sizes in SkSL. + /// + /// This is because the SkSL eval function uses texel coordinates when + /// sampling an ImageShader, and SkSL does not support the textureSize + /// function. These uniforms allow adding support for normalized + /// coordinates for [opImageSampleImplicitLod]. + final Map samplerSizeDeclarations = {}; + /// ID mapped to numerical types. final Map types = {}; @@ -80,6 +97,9 @@ class _Transpiler { /// ID mapped to ID. Used by [OpLoad]. final Map alias = {}; + /// ID mapped to a string to use instead of a generated name. + final Map nameOverloads = {}; + /// The ID for a constant true value. /// See [opConstantTrue]. int constantTrue = 0; @@ -113,6 +133,14 @@ class _Transpiler { /// See [opTypeFloat]. int floatType = 0; + /// The ID of the image type. + /// See [opTypeImage]. + int imageType = 0; + + /// The ID of the sampledImage type. + /// See [opTypeSampledImage]. + int sampledImageType = 0; + /// The ID of the function that is currently being defined. /// Set by [opFunction] and unset by [opFunctionEnd]. int currentFunction = 0; @@ -131,9 +159,12 @@ class _Transpiler { /// Set by [opDecorate]. int fragCoord = 0; - /// The number of floats used by uniform + /// The number of floats used by uniforms. int uniformFloatCount = 0; + /// The number of samplers used by uniforms. + int samplerCount = 0; + /// Current indentation to prepend to new lines. String indent = ''; @@ -147,6 +178,9 @@ class _Transpiler { void transpile() { parseHeader(); writeHeader(); + + // Parse instructions and write to body. + out = body; while (position < spirv.length) { final int lastPosition = position; parseInstruction(); @@ -154,10 +188,33 @@ class _Transpiler { assert(position > lastPosition); } - src.writeln(); // TODO(antrob): Investigate if `List.filled(maxFunctionId, false)` can be used here instead. final Set visited = {}; writeFunctionAndDeps(visited, entryPoint); + + // Add uniform declarations to header. + if (uniformDeclarations.isNotEmpty) { + header.writeln(); + final List locations = uniformDeclarations.keys.toList(); + locations.sort((int a, int b) => a - b); + for (final int location in locations) { + header.writeln(uniformDeclarations[location]); + } + } + + // Add SkSL sampler size declarations to header. + if (samplerSizeDeclarations.isNotEmpty) { + header.writeln(); + final List locations = samplerSizeDeclarations.keys.toList(); + locations.sort((int a, int b) => a - b); + for (final int location in locations) { + header.writeln(samplerSizeDeclarations[location]); + } + } + + src.write(header); + src.writeln(); + src.write(body); } TranspileException failure(String why) => @@ -171,19 +228,19 @@ class _Transpiler { for (final int dep in functionDeps[function]!) { writeFunctionAndDeps(visited, dep); } - src.write(functionDefs[function]!.toString()); + out.write(functionDefs[function]!.toString()); } void writeHeader() { switch (target) { case TargetLanguage.glslES: - src.writeln('#version 100\n'); - src.writeln('precision mediump float;\n'); + out.writeln('#version 100\n'); + out.writeln('precision mediump float;\n'); break; case TargetLanguage.glslES300: - src.writeln('#version 300 es\n'); - src.writeln('precision mediump float;\n'); - src.writeln('layout ( location = 0 ) out vec4 $_colorVariableName;\n'); + out.writeln('#version 300 es\n'); + out.writeln('precision mediump float;\n'); + out.writeln('layout ( location = 0 ) out vec4 $_colorVariableName;\n'); break; default: break; @@ -193,11 +250,14 @@ class _Transpiler { String resolveName(int id) { if (alias.containsKey(id)) { return resolveName(alias[id]!); + } + if (nameOverloads.containsKey(id)) { + return nameOverloads[id]!; } else if (constantTrue > 0 && id == constantTrue) { return 'true'; } else if (constantFalse > 0 && id == constantFalse) { return 'false'; - } else if (id == colorOutput) { + } if (id == colorOutput) { if (target == TargetLanguage.glslES) { return _glslESColorName; } else { @@ -259,6 +319,7 @@ class _Transpiler { out.writeln('$indent$type $name = $type($value);'); } + /// Read an instruction word, and handle the operation. /// /// SPIR-V instructions contain an op-code as well as a @@ -309,6 +370,12 @@ class _Transpiler { case _opTypeMatrix: opTypeMatrix(); break; + case _opTypeImage: + opTypeImage(); + break; + case _opTypeSampledImage: + opTypeSampledImage(); + break; case _opTypePointer: opTypePointer(); break; @@ -369,6 +436,9 @@ class _Transpiler { case _opCompositeExtract: opCompositeExtract(); break; + case _opImageSampleImplicitLod: + opImageSampleImplicitLod(); + break; case _opFNegate: opFNegate(); break; @@ -557,6 +627,54 @@ class _Transpiler { types[id] = t; } + void opTypeImage() { + if (imageType != 0) { + throw failure('Image type was previously declared.'); + } + final int id = readWord(); + final int sampledType = readWord(); + if (types[sampledType] != _Type.float) { + throw failure('Sampled type must be float.'); + } + final int dimensionality = readWord(); + if (dimensionality != _dim2D) { + throw failure('Dimensionality must be 2D.'); + } + final int depth = readWord(); + if (depth != 0) { + throw failure('Depth must be 0.'); + } + final int arrayed = readWord(); + if (arrayed != 0) { + throw failure('Arrayed must be 0.'); + } + final int multisampled = readWord(); + if (multisampled != 0) { + throw failure('Multisampled must be 0.'); + } + final int sampled = readWord(); + if (sampled != 1) { + throw failure('Sampled must be 1.'); + } + imageType = id; + } + + void opTypeSampledImage() { + if (sampledImageType != 0) { + throw failure('imageSampledType was previously declared.'); + } + if (imageType == 0) { + throw failure('imageType has not yet been declared.'); + } + final int id = readWord(); + final int imgType = readWord(); + if (imgType != imageType) { + throw failure('Invalid image type.'); + } + sampledImageType = id; + types[id] = _Type.sampledImage; + } + void opTypePointer() { final int id = readWord(); // ignore storage class @@ -580,14 +698,12 @@ class _Transpiler { } void opConstantTrue() { - // Skip type operand. - position++; + position++; // Skip type operand. constantTrue = readWord(); } void opConstantFalse() { - // Skip type operand. - position++; + position++; // Skip type operand. constantFalse = readWord(); } @@ -604,21 +720,21 @@ class _Transpiler { valueString = '$v'; } final String typeName = resolveType(type); - src.writeln('const $typeName $id = $valueString;'); + header.writeln('const $typeName $id = $valueString;'); } void opConstantComposite() { final String type = resolveType(readWord()); final String id = resolveName(readWord()); - src.write('const $type $id = $type('); + header.write('const $type $id = $type('); final int count = nextPosition - position; for (int i = 0; i < count; i++) { - src.write(resolveName(readWord())); + header.write(resolveName(readWord())); if (i < count - 1) { - src.write(', '); + header.write(', '); } } - src.writeln(');'); + header.writeln(');'); } void opFunction() { @@ -685,7 +801,7 @@ class _Transpiler { // Remove trailing two space characters, if present. indent = indent.substring(0, max(0, indent.length - 2)); currentFunction = 0; - out = src; + out = body; currentFunctionType = null; } @@ -721,16 +837,27 @@ class _Transpiler { switch (storageClass) { case _storageClassUniformConstant: + int? location = locations[id]; + if (location == null) { + throw failure('$id had no location specified'); + } + String prefix = ''; if (target == TargetLanguage.glslES300) { - final String location = locations[id].toString(); - src.write('layout ( location = $location ) '); + prefix = 'layout ( location = $location ) '; } - src.writeln('uniform $type $name;'); + uniformDeclarations[location] = '${prefix}uniform $type $name;'; final _Type? t = types[typeId]; if (t == null) { throw failure('$typeId is not a defined type'); } - uniformFloatCount += _typeFloatCounts[t]!; + if (t == _Type.sampledImage) { + samplerCount++; + if (target == TargetLanguage.sksl) { + samplerSizeDeclarations[location] = 'uniform half2 ${name}_size;'; + } + } else { + uniformFloatCount += _typeFloatCounts[t]!; + } return; case _storageClassInput: return; @@ -772,7 +899,7 @@ class _Transpiler { void opAccessChain() { final String type = resolveType(readWord()); - final String name = resolveName(readWord()); + final int id = readWord(); final String base = resolveName(readWord()); // opAccessChain currently only supports indexed access. @@ -780,13 +907,13 @@ class _Transpiler { // Currently, structs will be caught before this method is called, // since using the instruction to define a struct type will throw // an exception. - out.write('$indent$type $name = $base'); + String overload = base; final int count = nextPosition - position; for (int i = 0; i < count; i++) { final String index = resolveName(readWord()); - out.write('[$index]'); + overload += '[$index]'; } - out.writeln(';'); + nameOverloads[id] = overload; } void opDecorate() { @@ -853,6 +980,18 @@ class _Transpiler { out.writeln(';'); } + void opImageSampleImplicitLod() { + final String type = resolveType(readWord()); + final String name = resolveName(readWord()); + final String sampledImage = resolveName(readWord()); + final String coordinate = resolveName(readWord()); + if (target == TargetLanguage.sksl) { + out.writeln('$indent$type $name = $sampledImage.eval(${sampledImage}_size * $coordinate);'); + } else { + out.writeln('$indent$type $name = texture($sampledImage, $coordinate);'); + } + } + void opFNegate() { final String type = resolveType(readWord()); final String name = resolveName(readWord()); diff --git a/lib/spirv/lib/src/types.dart b/lib/spirv/lib/src/types.dart index 3fd3dde5c2866..e23ada3d7e527 100644 --- a/lib/spirv/lib/src/types.dart +++ b/lib/spirv/lib/src/types.dart @@ -15,6 +15,7 @@ enum _Type { float2x2, float3x3, float4x4, + sampledImage, } class _FunctionType { @@ -47,6 +48,7 @@ const Map<_Type, String> _skslTypeNames = <_Type, String>{ _Type.float2x2: 'float2x2', _Type.float3x3: 'float3x3', _Type.float4x4: 'float4x4', + _Type.sampledImage: 'shader', }; const Map<_Type, String> _glslTypeNames = <_Type, String>{ @@ -60,6 +62,7 @@ const Map<_Type, String> _glslTypeNames = <_Type, String>{ _Type.float2x2: 'mat2', _Type.float3x3: 'mat3', _Type.float4x4: 'mat4', + _Type.sampledImage: 'sampler2D', }; const Map<_Type, int> _typeFloatCounts = <_Type, int>{ diff --git a/lib/spirv/test/exception_shaders/BUILD.gn b/lib/spirv/test/exception_shaders/BUILD.gn index 8cdc9b8fa3bd6..79d41321c5367 100644 --- a/lib/spirv/test/exception_shaders/BUILD.gn +++ b/lib/spirv/test/exception_shaders/BUILD.gn @@ -10,6 +10,16 @@ if (enable_unittests) { tool = "//flutter/lib/spirv/test:spirv_assembler" sources = [ + "image_type_arrayed_must_be_zero.spvasm", + "image_type_depth_must_be_zero.spvasm", + "image_type_dimensionality_must_be_2D.spvasm", + "image_type_multisampled_must_be_zero.spvasm", + "image_type_must_be_float.spvasm", + "image_type_previously_declared.spvasm", + "image_type_sampled_must_be_one.spvasm", + "sampled_image_type_image_type_not_declared.spvasm", + "sampled_image_type_invalid_image_type.spvasm", + "sampled_image_type_previously_declared.spvasm", "unassigned_function_type.spvasm", "unassigned_pointer_type.spvasm", "unassigned_type.spvasm", diff --git a/lib/spirv/test/exception_shaders/image_type_arrayed_must_be_zero.spvasm b/lib/spirv/test/exception_shaders/image_type_arrayed_must_be_zero.spvasm new file mode 100644 index 0000000000000..f81daf936b4d5 --- /dev/null +++ b/lib/spirv/test/exception_shaders/image_type_arrayed_must_be_zero.spvasm @@ -0,0 +1,62 @@ +; 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. +; +; SPIR-V +; Version: 1.0 +; Generator: Khronos Glslang Reference Front End; 8 +; Bound: 28 +; Schema: 0 +; +; Line 43 contains a non-zero-arrayed image type. +; + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %main "main" %gl_FragCoord %oColor + OpExecutionMode %main OriginLowerLeft + OpSource GLSL 450 + OpName %main "main" + OpName %uv "uv" + OpName %gl_FragCoord "gl_FragCoord" + OpName %iResolution "iResolution" + OpName %oColor "oColor" + OpName %iImage "iImage" + OpDecorate %gl_FragCoord BuiltIn FragCoord + OpDecorate %iResolution Location 1 + OpDecorate %oColor Location 0 + OpDecorate %iImage Location 0 + OpDecorate %iImage DescriptorSet 0 + OpDecorate %iImage Binding 0 + %void = OpTypeVoid + %3 = OpTypeFunction %void + %float = OpTypeFloat 32 + %v2float = OpTypeVector %float 2 +%_ptr_Function_v2float = OpTypePointer Function %v2float + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float +%gl_FragCoord = OpVariable %_ptr_Input_v4float Input +%_ptr_UniformConstant_v2float = OpTypePointer UniformConstant %v2float +%iResolution = OpVariable %_ptr_UniformConstant_v2float UniformConstant +%_ptr_Output_v4float = OpTypePointer Output %v4float + %oColor = OpVariable %_ptr_Output_v4float Output + %21 = OpTypeImage %float 2D 0 1 0 1 Unknown + %22 = OpTypeSampledImage %21 +%_ptr_UniformConstant_22 = OpTypePointer UniformConstant %22 + %iImage = OpVariable %_ptr_UniformConstant_22 UniformConstant + %main = OpFunction %void None %3 + %5 = OpLabel + %uv = OpVariable %_ptr_Function_v2float Function + %13 = OpLoad %v4float %gl_FragCoord + %14 = OpVectorShuffle %v2float %13 %13 0 1 + %17 = OpLoad %v2float %iResolution + %18 = OpFDiv %v2float %14 %17 + OpStore %uv %18 + %25 = OpLoad %22 %iImage + %26 = OpLoad %v2float %uv + %27 = OpImageSampleImplicitLod %v4float %25 %26 + OpStore %oColor %27 + OpReturn + OpFunctionEnd + + diff --git a/lib/spirv/test/exception_shaders/image_type_depth_must_be_zero.spvasm b/lib/spirv/test/exception_shaders/image_type_depth_must_be_zero.spvasm new file mode 100644 index 0000000000000..7205a053dfc7c --- /dev/null +++ b/lib/spirv/test/exception_shaders/image_type_depth_must_be_zero.spvasm @@ -0,0 +1,62 @@ +; 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. +; +; SPIR-V +; Version: 1.0 +; Generator: Khronos Glslang Reference Front End; 8 +; Bound: 28 +; Schema: 0 +; +; Line 43 contains a non-zero-depth image type. +; + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %main "main" %gl_FragCoord %oColor + OpExecutionMode %main OriginLowerLeft + OpSource GLSL 450 + OpName %main "main" + OpName %uv "uv" + OpName %gl_FragCoord "gl_FragCoord" + OpName %iResolution "iResolution" + OpName %oColor "oColor" + OpName %iImage "iImage" + OpDecorate %gl_FragCoord BuiltIn FragCoord + OpDecorate %iResolution Location 1 + OpDecorate %oColor Location 0 + OpDecorate %iImage Location 0 + OpDecorate %iImage DescriptorSet 0 + OpDecorate %iImage Binding 0 + %void = OpTypeVoid + %3 = OpTypeFunction %void + %float = OpTypeFloat 32 + %v2float = OpTypeVector %float 2 +%_ptr_Function_v2float = OpTypePointer Function %v2float + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float +%gl_FragCoord = OpVariable %_ptr_Input_v4float Input +%_ptr_UniformConstant_v2float = OpTypePointer UniformConstant %v2float +%iResolution = OpVariable %_ptr_UniformConstant_v2float UniformConstant +%_ptr_Output_v4float = OpTypePointer Output %v4float + %oColor = OpVariable %_ptr_Output_v4float Output + %21 = OpTypeImage %float 2D 1 0 0 1 Unknown + %22 = OpTypeSampledImage %21 +%_ptr_UniformConstant_22 = OpTypePointer UniformConstant %22 + %iImage = OpVariable %_ptr_UniformConstant_22 UniformConstant + %main = OpFunction %void None %3 + %5 = OpLabel + %uv = OpVariable %_ptr_Function_v2float Function + %13 = OpLoad %v4float %gl_FragCoord + %14 = OpVectorShuffle %v2float %13 %13 0 1 + %17 = OpLoad %v2float %iResolution + %18 = OpFDiv %v2float %14 %17 + OpStore %uv %18 + %25 = OpLoad %22 %iImage + %26 = OpLoad %v2float %uv + %27 = OpImageSampleImplicitLod %v4float %25 %26 + OpStore %oColor %27 + OpReturn + OpFunctionEnd + + diff --git a/lib/spirv/test/exception_shaders/image_type_dimensionality_must_be_2D.spvasm b/lib/spirv/test/exception_shaders/image_type_dimensionality_must_be_2D.spvasm new file mode 100644 index 0000000000000..3bbd01180b187 --- /dev/null +++ b/lib/spirv/test/exception_shaders/image_type_dimensionality_must_be_2D.spvasm @@ -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. +; +; SPIR-V +; Version: 1.0 +; Generator: Khronos Glslang Reference Front End; 8 +; Bound: 28 +; Schema: 0 +; +; Line 43 contains a non-2D image type. +; + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %main "main" %gl_FragCoord %oColor + OpExecutionMode %main OriginLowerLeft + OpSource GLSL 450 + OpName %main "main" + OpName %uv "uv" + OpName %gl_FragCoord "gl_FragCoord" + OpName %iResolution "iResolution" + OpName %oColor "oColor" + OpName %iImage "iImage" + OpDecorate %gl_FragCoord BuiltIn FragCoord + OpDecorate %iResolution Location 1 + OpDecorate %oColor Location 0 + OpDecorate %iImage Location 0 + OpDecorate %iImage DescriptorSet 0 + OpDecorate %iImage Binding 0 + %void = OpTypeVoid + %3 = OpTypeFunction %void + %float = OpTypeFloat 32 + %v2float = OpTypeVector %float 2 +%_ptr_Function_v2float = OpTypePointer Function %v2float + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float +%gl_FragCoord = OpVariable %_ptr_Input_v4float Input +%_ptr_UniformConstant_v2float = OpTypePointer UniformConstant %v2float +%iResolution = OpVariable %_ptr_UniformConstant_v2float UniformConstant +%_ptr_Output_v4float = OpTypePointer Output %v4float + %oColor = OpVariable %_ptr_Output_v4float Output + %21 = OpTypeImage %float 3D 0 0 0 1 Unknown + %22 = OpTypeSampledImage %21 +%_ptr_UniformConstant_22 = OpTypePointer UniformConstant %22 + %iImage = OpVariable %_ptr_UniformConstant_22 UniformConstant + %main = OpFunction %void None %3 + %5 = OpLabel + %uv = OpVariable %_ptr_Function_v2float Function + %13 = OpLoad %v4float %gl_FragCoord + %14 = OpVectorShuffle %v2float %13 %13 0 1 + %17 = OpLoad %v2float %iResolution + %18 = OpFDiv %v2float %14 %17 + OpStore %uv %18 + %25 = OpLoad %22 %iImage + %26 = OpLoad %v2float %uv + %27 = OpImageSampleImplicitLod %v4float %25 %26 + OpStore %oColor %27 + OpReturn + OpFunctionEnd + diff --git a/lib/spirv/test/exception_shaders/image_type_multisampled_must_be_zero.spvasm b/lib/spirv/test/exception_shaders/image_type_multisampled_must_be_zero.spvasm new file mode 100644 index 0000000000000..0bf1a6a177679 --- /dev/null +++ b/lib/spirv/test/exception_shaders/image_type_multisampled_must_be_zero.spvasm @@ -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. +; +; SPIR-V +; Version: 1.0 +; Generator: Khronos Glslang Reference Front End; 8 +; Bound: 28 +; Schema: 0 +; +; Line 43 contains a non-zero-multisampled image type. +; + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %main "main" %gl_FragCoord %oColor + OpExecutionMode %main OriginLowerLeft + OpSource GLSL 450 + OpName %main "main" + OpName %uv "uv" + OpName %gl_FragCoord "gl_FragCoord" + OpName %iResolution "iResolution" + OpName %oColor "oColor" + OpName %iImage "iImage" + OpDecorate %gl_FragCoord BuiltIn FragCoord + OpDecorate %iResolution Location 1 + OpDecorate %oColor Location 0 + OpDecorate %iImage Location 0 + OpDecorate %iImage DescriptorSet 0 + OpDecorate %iImage Binding 0 + %void = OpTypeVoid + %3 = OpTypeFunction %void + %float = OpTypeFloat 32 + %v2float = OpTypeVector %float 2 +%_ptr_Function_v2float = OpTypePointer Function %v2float + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float +%gl_FragCoord = OpVariable %_ptr_Input_v4float Input +%_ptr_UniformConstant_v2float = OpTypePointer UniformConstant %v2float +%iResolution = OpVariable %_ptr_UniformConstant_v2float UniformConstant +%_ptr_Output_v4float = OpTypePointer Output %v4float + %oColor = OpVariable %_ptr_Output_v4float Output + %21 = OpTypeImage %float 2D 0 0 1 1 Unknown + %22 = OpTypeSampledImage %21 +%_ptr_UniformConstant_22 = OpTypePointer UniformConstant %22 + %iImage = OpVariable %_ptr_UniformConstant_22 UniformConstant + %main = OpFunction %void None %3 + %5 = OpLabel + %uv = OpVariable %_ptr_Function_v2float Function + %13 = OpLoad %v4float %gl_FragCoord + %14 = OpVectorShuffle %v2float %13 %13 0 1 + %17 = OpLoad %v2float %iResolution + %18 = OpFDiv %v2float %14 %17 + OpStore %uv %18 + %25 = OpLoad %22 %iImage + %26 = OpLoad %v2float %uv + %27 = OpImageSampleImplicitLod %v4float %25 %26 + OpStore %oColor %27 + OpReturn + OpFunctionEnd + diff --git a/lib/spirv/test/exception_shaders/image_type_must_be_float.spvasm b/lib/spirv/test/exception_shaders/image_type_must_be_float.spvasm new file mode 100644 index 0000000000000..f16eb17962b88 --- /dev/null +++ b/lib/spirv/test/exception_shaders/image_type_must_be_float.spvasm @@ -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. +; +; SPIR-V +; Version: 1.0 +; Generator: Khronos Glslang Reference Front End; 8 +; Bound: 28 +; Schema: 0 +; +; Line 43 contains a non-float image type. +; + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %main "main" %gl_FragCoord %oColor + OpExecutionMode %main OriginLowerLeft + OpSource GLSL 450 + OpName %main "main" + OpName %uv "uv" + OpName %gl_FragCoord "gl_FragCoord" + OpName %iResolution "iResolution" + OpName %oColor "oColor" + OpName %iImage "iImage" + OpDecorate %gl_FragCoord BuiltIn FragCoord + OpDecorate %iResolution Location 1 + OpDecorate %oColor Location 0 + OpDecorate %iImage Location 0 + OpDecorate %iImage DescriptorSet 0 + OpDecorate %iImage Binding 0 + %void = OpTypeVoid + %3 = OpTypeFunction %void + %float = OpTypeFloat 32 + %v2float = OpTypeVector %float 2 +%_ptr_Function_v2float = OpTypePointer Function %v2float + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float +%gl_FragCoord = OpVariable %_ptr_Input_v4float Input +%_ptr_UniformConstant_v2float = OpTypePointer UniformConstant %v2float +%iResolution = OpVariable %_ptr_UniformConstant_v2float UniformConstant +%_ptr_Output_v4float = OpTypePointer Output %v4float + %oColor = OpVariable %_ptr_Output_v4float Output + %21 = OpTypeImage %void 2D 0 0 0 1 Unknown + %22 = OpTypeSampledImage %21 +%_ptr_UniformConstant_22 = OpTypePointer UniformConstant %22 + %iImage = OpVariable %_ptr_UniformConstant_22 UniformConstant + %main = OpFunction %void None %3 + %5 = OpLabel + %uv = OpVariable %_ptr_Function_v2float Function + %13 = OpLoad %v4float %gl_FragCoord + %14 = OpVectorShuffle %v2float %13 %13 0 1 + %17 = OpLoad %v2float %iResolution + %18 = OpFDiv %v2float %14 %17 + OpStore %uv %18 + %25 = OpLoad %22 %iImage + %26 = OpLoad %v2float %uv + %27 = OpImageSampleImplicitLod %v4float %25 %26 + OpStore %oColor %27 + OpReturn + OpFunctionEnd + diff --git a/lib/spirv/test/exception_shaders/image_type_previously_declared.spvasm b/lib/spirv/test/exception_shaders/image_type_previously_declared.spvasm new file mode 100644 index 0000000000000..9003a3abb5f56 --- /dev/null +++ b/lib/spirv/test/exception_shaders/image_type_previously_declared.spvasm @@ -0,0 +1,62 @@ +; 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. +; +; SPIR-V +; Version: 1.0 +; Generator: Khronos Glslang Reference Front End; 8 +; Bound: 29 +; Schema: 0 +; +; Line 45 contains a second OpTypeImage declaration. +; + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %main "main" %gl_FragCoord %oColor + OpExecutionMode %main OriginLowerLeft + OpSource GLSL 450 + OpName %main "main" + OpName %uv "uv" + OpName %gl_FragCoord "gl_FragCoord" + OpName %iResolution "iResolution" + OpName %oColor "oColor" + OpName %iImage "iImage" + OpDecorate %gl_FragCoord BuiltIn FragCoord + OpDecorate %iResolution Location 1 + OpDecorate %oColor Location 0 + OpDecorate %iImage Location 0 + OpDecorate %iImage DescriptorSet 0 + OpDecorate %iImage Binding 0 + %void = OpTypeVoid + %3 = OpTypeFunction %void + %float = OpTypeFloat 32 + %v2float = OpTypeVector %float 2 +%_ptr_Function_v2float = OpTypePointer Function %v2float + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float +%gl_FragCoord = OpVariable %_ptr_Input_v4float Input +%_ptr_UniformConstant_v2float = OpTypePointer UniformConstant %v2float +%iResolution = OpVariable %_ptr_UniformConstant_v2float UniformConstant +%_ptr_Output_v4float = OpTypePointer Output %v4float + %oColor = OpVariable %_ptr_Output_v4float Output + %21 = OpTypeImage %float 2D 0 0 0 1 Unknown + %28 = OpTypeImage %float 2D 0 0 0 1 Unknown + %22 = OpTypeSampledImage %21 +%_ptr_UniformConstant_22 = OpTypePointer UniformConstant %22 + %iImage = OpVariable %_ptr_UniformConstant_22 UniformConstant + %main = OpFunction %void None %3 + %5 = OpLabel + %uv = OpVariable %_ptr_Function_v2float Function + %13 = OpLoad %v4float %gl_FragCoord + %14 = OpVectorShuffle %v2float %13 %13 0 1 + %17 = OpLoad %v2float %iResolution + %18 = OpFDiv %v2float %14 %17 + OpStore %uv %18 + %25 = OpLoad %22 %iImage + %26 = OpLoad %v2float %uv + %27 = OpImageSampleImplicitLod %v4float %25 %26 + OpStore %oColor %27 + OpReturn + OpFunctionEnd + diff --git a/lib/spirv/test/exception_shaders/image_type_sampled_must_be_one.spvasm b/lib/spirv/test/exception_shaders/image_type_sampled_must_be_one.spvasm new file mode 100644 index 0000000000000..93527c72f9112 --- /dev/null +++ b/lib/spirv/test/exception_shaders/image_type_sampled_must_be_one.spvasm @@ -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. +; +; SPIR-V +; Version: 1.0 +; Generator: Khronos Glslang Reference Front End; 8 +; Bound: 28 +; Schema: 0 +; +; Line 43 contains an image type with 'sampled' not set to one. +; + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %main "main" %gl_FragCoord %oColor + OpExecutionMode %main OriginLowerLeft + OpSource GLSL 450 + OpName %main "main" + OpName %uv "uv" + OpName %gl_FragCoord "gl_FragCoord" + OpName %iResolution "iResolution" + OpName %oColor "oColor" + OpName %iImage "iImage" + OpDecorate %gl_FragCoord BuiltIn FragCoord + OpDecorate %iResolution Location 1 + OpDecorate %oColor Location 0 + OpDecorate %iImage Location 0 + OpDecorate %iImage DescriptorSet 0 + OpDecorate %iImage Binding 0 + %void = OpTypeVoid + %3 = OpTypeFunction %void + %float = OpTypeFloat 32 + %v2float = OpTypeVector %float 2 +%_ptr_Function_v2float = OpTypePointer Function %v2float + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float +%gl_FragCoord = OpVariable %_ptr_Input_v4float Input +%_ptr_UniformConstant_v2float = OpTypePointer UniformConstant %v2float +%iResolution = OpVariable %_ptr_UniformConstant_v2float UniformConstant +%_ptr_Output_v4float = OpTypePointer Output %v4float + %oColor = OpVariable %_ptr_Output_v4float Output + %21 = OpTypeImage %float 2D 0 0 0 0 Unknown + %22 = OpTypeSampledImage %21 +%_ptr_UniformConstant_22 = OpTypePointer UniformConstant %22 + %iImage = OpVariable %_ptr_UniformConstant_22 UniformConstant + %main = OpFunction %void None %3 + %5 = OpLabel + %uv = OpVariable %_ptr_Function_v2float Function + %13 = OpLoad %v4float %gl_FragCoord + %14 = OpVectorShuffle %v2float %13 %13 0 1 + %17 = OpLoad %v2float %iResolution + %18 = OpFDiv %v2float %14 %17 + OpStore %uv %18 + %25 = OpLoad %22 %iImage + %26 = OpLoad %v2float %uv + %27 = OpImageSampleImplicitLod %v4float %25 %26 + OpStore %oColor %27 + OpReturn + OpFunctionEnd + + + diff --git a/lib/spirv/test/exception_shaders/sampled_image_type_image_type_not_declared.spvasm b/lib/spirv/test/exception_shaders/sampled_image_type_image_type_not_declared.spvasm new file mode 100644 index 0000000000000..4bde3531e1e25 --- /dev/null +++ b/lib/spirv/test/exception_shaders/sampled_image_type_image_type_not_declared.spvasm @@ -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. +; +; SPIR-V +; Version: 1.0 +; Generator: Khronos Glslang Reference Front End; 8 +; Bound: 29 +; Schema: 0 +; +; Line 43 contains an OpTypeSampledImage before OpImageType has been declared. +; + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %main "main" %gl_FragCoord %oColor + OpExecutionMode %main OriginLowerLeft + OpSource GLSL 450 + OpName %main "main" + OpName %uv "uv" + OpName %gl_FragCoord "gl_FragCoord" + OpName %iResolution "iResolution" + OpName %oColor "oColor" + OpName %iImage "iImage" + OpDecorate %gl_FragCoord BuiltIn FragCoord + OpDecorate %iResolution Location 1 + OpDecorate %oColor Location 0 + OpDecorate %iImage Location 0 + OpDecorate %iImage DescriptorSet 0 + OpDecorate %iImage Binding 0 + %void = OpTypeVoid + %3 = OpTypeFunction %void + %float = OpTypeFloat 32 + %v2float = OpTypeVector %float 2 +%_ptr_Function_v2float = OpTypePointer Function %v2float + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float +%gl_FragCoord = OpVariable %_ptr_Input_v4float Input +%_ptr_UniformConstant_v2float = OpTypePointer UniformConstant %v2float +%iResolution = OpVariable %_ptr_UniformConstant_v2float UniformConstant +%_ptr_Output_v4float = OpTypePointer Output %v4float + %oColor = OpVariable %_ptr_Output_v4float Output + %22 = OpTypeSampledImage %21 + %21 = OpTypeImage %float 2D 0 0 0 1 Unknown +%_ptr_UniformConstant_22 = OpTypePointer UniformConstant %22 + %iImage = OpVariable %_ptr_UniformConstant_22 UniformConstant + %main = OpFunction %void None %3 + %5 = OpLabel + %uv = OpVariable %_ptr_Function_v2float Function + %13 = OpLoad %v4float %gl_FragCoord + %14 = OpVectorShuffle %v2float %13 %13 0 1 + %17 = OpLoad %v2float %iResolution + %18 = OpFDiv %v2float %14 %17 + OpStore %uv %18 + %25 = OpLoad %22 %iImage + %26 = OpLoad %v2float %uv + %27 = OpImageSampleImplicitLod %v4float %25 %26 + OpStore %oColor %27 + OpReturn + OpFunctionEnd + + + diff --git a/lib/spirv/test/exception_shaders/sampled_image_type_invalid_image_type.spvasm b/lib/spirv/test/exception_shaders/sampled_image_type_invalid_image_type.spvasm new file mode 100644 index 0000000000000..c13f5e3382bfb --- /dev/null +++ b/lib/spirv/test/exception_shaders/sampled_image_type_invalid_image_type.spvasm @@ -0,0 +1,62 @@ +; 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. +; +; SPIR-V +; Version: 1.0 +; Generator: Khronos Glslang Reference Front End; 8 +; Bound: 29 +; Schema: 0 +; +; Line 44 contains an OpTypeSampledImage with an invalid image type. +; + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %main "main" %gl_FragCoord %oColor + OpExecutionMode %main OriginLowerLeft + OpSource GLSL 450 + OpName %main "main" + OpName %uv "uv" + OpName %gl_FragCoord "gl_FragCoord" + OpName %iResolution "iResolution" + OpName %oColor "oColor" + OpName %iImage "iImage" + OpDecorate %gl_FragCoord BuiltIn FragCoord + OpDecorate %iResolution Location 1 + OpDecorate %oColor Location 0 + OpDecorate %iImage Location 0 + OpDecorate %iImage DescriptorSet 0 + OpDecorate %iImage Binding 0 + %void = OpTypeVoid + %3 = OpTypeFunction %void + %float = OpTypeFloat 32 + %v2float = OpTypeVector %float 2 +%_ptr_Function_v2float = OpTypePointer Function %v2float + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float +%gl_FragCoord = OpVariable %_ptr_Input_v4float Input +%_ptr_UniformConstant_v2float = OpTypePointer UniformConstant %v2float +%iResolution = OpVariable %_ptr_UniformConstant_v2float UniformConstant +%_ptr_Output_v4float = OpTypePointer Output %v4float + %oColor = OpVariable %_ptr_Output_v4float Output + %21 = OpTypeImage %float 2D 0 0 0 1 Unknown + %22 = OpTypeSampledImage %float +%_ptr_UniformConstant_22 = OpTypePointer UniformConstant %22 + %iImage = OpVariable %_ptr_UniformConstant_22 UniformConstant + %main = OpFunction %void None %3 + %5 = OpLabel + %uv = OpVariable %_ptr_Function_v2float Function + %13 = OpLoad %v4float %gl_FragCoord + %14 = OpVectorShuffle %v2float %13 %13 0 1 + %17 = OpLoad %v2float %iResolution + %18 = OpFDiv %v2float %14 %17 + OpStore %uv %18 + %25 = OpLoad %22 %iImage + %26 = OpLoad %v2float %uv + %27 = OpImageSampleImplicitLod %v4float %25 %26 + OpStore %oColor %27 + OpReturn + OpFunctionEnd + + diff --git a/lib/spirv/test/exception_shaders/sampled_image_type_previously_declared.spvasm b/lib/spirv/test/exception_shaders/sampled_image_type_previously_declared.spvasm new file mode 100644 index 0000000000000..46286301771b3 --- /dev/null +++ b/lib/spirv/test/exception_shaders/sampled_image_type_previously_declared.spvasm @@ -0,0 +1,62 @@ +; 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. +; +; SPIR-V +; Version: 1.0 +; Generator: Khronos Glslang Reference Front End; 8 +; Bound: 29 +; Schema: 0 +; +; Line 45 contains a second OpTypeSampledImage declaration. +; + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %main "main" %gl_FragCoord %oColor + OpExecutionMode %main OriginLowerLeft + OpSource GLSL 450 + OpName %main "main" + OpName %uv "uv" + OpName %gl_FragCoord "gl_FragCoord" + OpName %iResolution "iResolution" + OpName %oColor "oColor" + OpName %iImage "iImage" + OpDecorate %gl_FragCoord BuiltIn FragCoord + OpDecorate %iResolution Location 1 + OpDecorate %oColor Location 0 + OpDecorate %iImage Location 0 + OpDecorate %iImage DescriptorSet 0 + OpDecorate %iImage Binding 0 + %void = OpTypeVoid + %3 = OpTypeFunction %void + %float = OpTypeFloat 32 + %v2float = OpTypeVector %float 2 +%_ptr_Function_v2float = OpTypePointer Function %v2float + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float +%gl_FragCoord = OpVariable %_ptr_Input_v4float Input +%_ptr_UniformConstant_v2float = OpTypePointer UniformConstant %v2float +%iResolution = OpVariable %_ptr_UniformConstant_v2float UniformConstant +%_ptr_Output_v4float = OpTypePointer Output %v4float + %oColor = OpVariable %_ptr_Output_v4float Output + %21 = OpTypeImage %float 2D 0 0 0 1 Unknown + %22 = OpTypeSampledImage %21 + %28 = OpTypeSampledImage %21 +%_ptr_UniformConstant_22 = OpTypePointer UniformConstant %22 + %iImage = OpVariable %_ptr_UniformConstant_22 UniformConstant + %main = OpFunction %void None %3 + %5 = OpLabel + %uv = OpVariable %_ptr_Function_v2float Function + %13 = OpLoad %v4float %gl_FragCoord + %14 = OpVectorShuffle %v2float %13 %13 0 1 + %17 = OpLoad %v2float %iResolution + %18 = OpFDiv %v2float %14 %17 + OpStore %uv %18 + %25 = OpLoad %22 %iImage + %26 = OpLoad %v2float %uv + %27 = OpImageSampleImplicitLod %v4float %25 %26 + OpStore %oColor %27 + OpReturn + OpFunctionEnd + diff --git a/lib/spirv/test/general_shaders/BUILD.gn b/lib/spirv/test/general_shaders/BUILD.gn index 828df5de1e350..01ceb01398d83 100644 --- a/lib/spirv/test/general_shaders/BUILD.gn +++ b/lib/spirv/test/general_shaders/BUILD.gn @@ -10,6 +10,8 @@ if (enable_unittests) { tool = "//flutter/lib/spirv/test:glsl_to_spirv" sources = [ + "blue_green_sampler.glsl", + "children_and_uniforms.glsl", "functions.glsl", "simple.glsl", "uniforms.glsl", diff --git a/lib/spirv/test/general_shaders/blue_green_sampler.glsl b/lib/spirv/test/general_shaders/blue_green_sampler.glsl new file mode 100644 index 0000000000000..4d71cbb91c216 --- /dev/null +++ b/lib/spirv/test/general_shaders/blue_green_sampler.glsl @@ -0,0 +1,19 @@ +#version 320 es + +// 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. + +precision highp float; + +layout ( location = 0 ) out vec4 oColor; + +layout ( location = 0 ) uniform sampler2D iChild; + +void main() { + // iChild1 is an image that is half blue, half green, + // so oColor should be set to vec2(0, 1, 0, 1) + oColor = texture(iChild, vec2(1, 0)); + oColor.a = 1.0; +} + diff --git a/lib/spirv/test/general_shaders/children_and_uniforms.glsl b/lib/spirv/test/general_shaders/children_and_uniforms.glsl new file mode 100644 index 0000000000000..44928b864cad3 --- /dev/null +++ b/lib/spirv/test/general_shaders/children_and_uniforms.glsl @@ -0,0 +1,26 @@ +#version 320 es + +// 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. + +precision highp float; + +layout ( location = 0 ) out vec4 color; + +layout ( location = 0 ) uniform sampler2D child1; +layout ( location = 1 ) uniform float a; +layout ( location = 2 ) uniform sampler2D child2; +layout ( location = 3 ) uniform float b; + +void main() { + // child1 is a 10x10 image where the left half is blue and the right + // half is green, and b should be 1, so c1 should be vec4(0, 1, 0, 1) + vec4 c1 = texture(child1, vec2(b, 0)); + + // child2 only contains vec4(0, 1, 0, 1). + vec4 c2 = texture(child2, vec2(0)); + + color = c1 * c2; +} + diff --git a/lib/ui/BUILD.gn b/lib/ui/BUILD.gn index bd7b644a3ab14..fdb56caa54095 100644 --- a/lib/ui/BUILD.gn +++ b/lib/ui/BUILD.gn @@ -30,6 +30,8 @@ source_set("ui") { "painting/color_filter.h", "painting/engine_layer.cc", "painting/engine_layer.h", + "painting/fragment_program.cc", + "painting/fragment_program.h", "painting/fragment_shader.cc", "painting/fragment_shader.h", "painting/gradient.cc", diff --git a/lib/ui/dart_ui.cc b/lib/ui/dart_ui.cc index 91e2c5b0d65c9..be820520db25a 100644 --- a/lib/ui/dart_ui.cc +++ b/lib/ui/dart_ui.cc @@ -13,7 +13,7 @@ #include "flutter/lib/ui/painting/codec.h" #include "flutter/lib/ui/painting/color_filter.h" #include "flutter/lib/ui/painting/engine_layer.h" -#include "flutter/lib/ui/painting/fragment_shader.h" +#include "flutter/lib/ui/painting/fragment_program.h" #include "flutter/lib/ui/painting/gradient.h" #include "flutter/lib/ui/painting/image.h" #include "flutter/lib/ui/painting/image_descriptor.h" @@ -67,7 +67,7 @@ void DartUI::InitForGlobal() { DartRuntimeHooks::RegisterNatives(g_natives); EngineLayer::RegisterNatives(g_natives); FontCollection::RegisterNatives(g_natives); - FragmentShader::RegisterNatives(g_natives); + FragmentProgram::RegisterNatives(g_natives); ImageDescriptor::RegisterNatives(g_natives); ImageFilter::RegisterNatives(g_natives); ImageShader::RegisterNatives(g_natives); diff --git a/lib/ui/geometry.dart b/lib/ui/geometry.dart index e273ec8bc0514..17346b561e5e0 100644 --- a/lib/ui/geometry.dart +++ b/lib/ui/geometry.dart @@ -1445,7 +1445,7 @@ class RRect { /// Therefore, this method is only needed for RRect use cases that require /// the appropriately scaled radii values. /// - /// See the [Skia scaling implementation](https://github.com/google/skia/blob/master/src/core/SkRRect.cpp) + /// See the [Skia scaling implementation](https://github.com/google/skia/blob/main/src/core/SkRRect.cpp) /// for more details. RRect scaleRadii() { double scale = 1.0; diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index 8ef8b66e662c6..6a03d9ec4c358 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -88,11 +88,6 @@ void _dispatchPointerDataPacket(ByteData packet) { PlatformDispatcher.instance._dispatchPointerDataPacket(packet); } -@pragma('vm:entry-point') -void _dispatchKeyData(ByteData packet, int responseId) { - PlatformDispatcher.instance._dispatchKeyData(packet, responseId); -} - @pragma('vm:entry-point') void _dispatchSemanticsAction(int id, int action, ByteData? args) { PlatformDispatcher.instance._dispatchSemanticsAction(id, action, args); diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 79dc945929a1d..33dcb8962575f 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -3751,108 +3751,162 @@ class ImageShader extends Shader { void _initWithImage(_Image image, int tmx, int tmy, int filterQualityIndex, Float64List matrix4) native 'ImageShader_initWithImage'; } -/// A shader (as used by [Paint.shader]) that runs provided SPIR-V code. +/// An instance of [FragmentProgram] creates [Shader] objects (as used by [Paint.shader]) that run SPIR-V code. /// /// This API is in beta and does not yet work on web. /// See https://github.com/flutter/flutter/projects/207 for roadmap. /// -/// [A current specification of valid SPIR-V is here.](https://github.com/flutter/engine/blob/master/lib/spirv/README.md) +/// [A current specification of valid SPIR-V is here.](https://github.com/flutter/engine/blob/main/lib/spirv/README.md) /// -/// When initializing or updating the `floatUniforms`, the length of float -/// uniforms must match the total number of floats defined as uniforms in -/// the shader. They will be updated in the order that they are defined. -/// -/// For example, if there are 3 uniforms: 1 of type float, 1 type float2/vec2, -/// and 1 of type vec3/float3, and 1 mat2x2 then the length of `floatUniforms` -/// must be 10. -/// -/// The uniforms could be updated as follows: -/// -/// Consider the following snippit of GLSL code. -/// -/// ``` -/// layout (location = 0) uniform float a; -/// layout (location = 1) uniform vec2 b; -/// layout (location = 2) uniform vec3 c; -/// layout (location = 3) uniform mat2x2 d; -/// ``` -/// -/// After being compiled to SPIR-V using [shaderc](https://github.com/google/shaderc) -/// and provided to the constructor, `floatUniforms` must always have a length -/// of 10. One per float-component of each uniform. -/// -/// Dart code to update uniforms. -/// -/// `shader.update(floatUniforms: Float32List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));` -/// -/// Results of shader uniforms. -/// -/// a: 1 -/// b: [2, 3] -/// c: [4, 5, 6] -/// d: [7, 8, 9, 10] // 2x2 matrix in column-major order -/// -class FragmentShader extends Shader { - - // TODO(chriscraws): Add `List? children` as a parameter to the - // constructor and to [update]. - // https://github.com/flutter/flutter/issues/85240 +class FragmentProgram extends NativeFieldWrapperClass1 { - /// Creates a fragment shader from SPIR-V byte data as an input. + /// Creates a fragment program from SPIR-V byte data as an input. + /// + /// One instance should be created per SPIR-V input. The constructed object + /// should then be reused via the [shader] method to create [Shader] objects + /// that can be used by [Shader.paint]. /// /// [A current specification of valid SPIR-V is here.](https://github.com/flutter/engine/blob/master/lib/spirv/README.md) /// SPIR-V not meeting this specification will throw an exception. - /// - /// `floatUniforms` can be passed optionally to initialize the shader's - /// uniforms. If they are not initially set, they will default - /// to 0. They can later be updated by invoking the [update] method. - /// - /// `floatUniforms` must be sized correctly, or an [ArgumentError] will - /// be thrown. See [FragmentShader] docs for details. - /// - /// The compilation of a shader gets more expensive the more complicated the source is. - /// Because of this, it is reccommended to construct a FragmentShader asynchrounously, - /// outside of a widget's `build` method, to minimize the chance of UI jank. + static Future compile({ + required ByteBuffer spirv, + bool debugPrint = false, + }) { + return Future(() => FragmentProgram._(spirv: spirv, debugPrint: debugPrint)); + } + @pragma('vm:entry-point') - FragmentShader({ + FragmentProgram._({ required ByteBuffer spirv, - Float32List? floatUniforms, bool debugPrint = false, - }) : super._() { + }) { _constructor(); final spv.TranspileResult result = spv.transpile( spirv, spv.TargetLanguage.sksl, ); - _uniformFloatCount = result.uniformFloatCount; _init(result.src, debugPrint); - update(floatUniforms: floatUniforms ?? Float32List(_uniformFloatCount)); + _uniformFloatCount = result.uniformFloatCount; + _samplerCount = result.samplerCount; } late final int _uniformFloatCount; + late final int _samplerCount; - void _constructor() native 'FragmentShader_constructor'; - void _init(String sksl, bool debugPrint) native 'FragmentShader_init'; + void _constructor() native 'FragmentProgram_constructor'; + void _init(String sksl, bool debugPrint) native 'FragmentProgram_init'; - /// Updates the uniform values that are supplied to the [FragmentShader] - /// and refreshes the shader. + /// Constructs a [Shader] object suitable for use by [Paint.shader] with + /// the given uniforms. + /// + /// This method is suitable to be called synchronously within a widget's + /// `build` method or from [CustomPainter.paint]. + /// + /// `floatUniforms` can be passed optionally to initialize the shader's + /// uniforms. If they are not set they will each default to 0. + /// + /// When initializing `floatUniforms`, the length of float uniforms must match + /// the total number of floats defined as uniforms in the shader, or an + /// [ArgumentError] will be thrown. Details are below. /// - /// `floatUniforms` must be sized correctly, or an [ArgumentError] will - /// be thrown. See [FragmentShader] docs for details. + /// Consider the following snippit of GLSL code. /// - /// This method will aquire additional fields as [FragmentShader] is - /// implemented further. - void update({ - required Float32List floatUniforms, + /// ``` + /// layout (location = 0) uniform float a; + /// layout (location = 1) uniform vec2 b; + /// layout (location = 2) uniform vec3 c; + /// layout (location = 3) uniform mat2x2 d; + /// ``` + /// + /// When compiled to SPIR-V and provided to the constructor, `floatUniforms` + /// must have a length of 10. One per float-component of each uniform. + /// + /// `program.shader(floatUniforms: Float32List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));` + /// + /// The uniforms will be set as follows: + /// + /// a: 1 + /// b: [2, 3] + /// c: [4, 5, 6] + /// d: [7, 8, 9, 10] // 2x2 matrix in column-major order + /// + /// `imageSamplers` must also be sized correctly, matching the number of UniformConstant + /// variables of type SampledImage specified in the SPIR-V code. + /// + /// Consider the following snippit of GLSL code. + /// + /// ``` + /// layout (location = 0) uniform sampler2D a; + /// layout (location = 1) uniform sampler2D b; + /// ``` + /// + /// After being compiled to SPIR-V `imageSamplers` must have a length + /// of 2. + /// + /// Once a [Shader] is built, uniform values cannot be changed. Instead, + /// [shader] must be called again with new uniform values. + Shader shader({ + Float32List? floatUniforms, + List? samplerUniforms, }) { + if (floatUniforms == null) { + floatUniforms = Float32List(_uniformFloatCount); + } if (floatUniforms.length != _uniformFloatCount) { throw ArgumentError( - 'FragmentShader floatUniforms size: ${floatUniforms.length} must match given shader uniform count: $_uniformFloatCount.'); + 'floatUniforms size: ${floatUniforms.length} must match given shader uniform count: $_uniformFloatCount.'); + } + if (_samplerCount > 0 && (samplerUniforms == null || samplerUniforms.length != _samplerCount)) { + throw ArgumentError('samplerUniforms must have length $_samplerCount'); + } + if (samplerUniforms == null) { + samplerUniforms = []; + } else { + samplerUniforms = [...samplerUniforms]; } - _update(floatUniforms); + final _FragmentShader shader = _FragmentShader( + this, Float32List.fromList(floatUniforms), samplerUniforms); + _shader(shader, floatUniforms, samplerUniforms); + return shader; } - void _update(Float32List floatUniforms) native 'FragmentShader_update'; + void _shader( + _FragmentShader shader, + Float32List floatUniforms, + List samplerUniforms, + ) native 'FragmentProgram_shader'; +} + +@pragma('vm:entry-point') +class _FragmentShader extends Shader { + /// This class is created by the engine and should not be instantiated + /// or extended directly. + /// + /// To create a [_FragmentShader], use a [FragmentProgram]. + _FragmentShader( + this._builder, + this._floatUniforms, + this._samplerUniforms, + ) : super._(); + + final FragmentProgram _builder; + final Float32List _floatUniforms; + final List _samplerUniforms; + + @override + bool operator ==(Object other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + return other is _FragmentShader + && other._builder == _builder + && _listEquals(other._floatUniforms, _floatUniforms) + && _listEquals(other._samplerUniforms, _samplerUniforms); + } + + @override + int get hashCode => hashValues(_builder, hashList(_floatUniforms), hashList(_samplerUniforms)); } /// Defines how a list of points is interpreted when drawing a set of triangles. diff --git a/lib/ui/painting/canvas.cc b/lib/ui/painting/canvas.cc index 66c1bcdd2ba1b..6e31a060caaba 100644 --- a/lib/ui/painting/canvas.cc +++ b/lib/ui/painting/canvas.cc @@ -82,6 +82,15 @@ fml::RefPtr Canvas::Create(PictureRecorder* recorder, ToDart("Canvas constructor called with non-genuine PictureRecorder.")); return nullptr; } + + // This call will implicitly initialize the |canvas_| field with an SkCanvas + // whether or not we are using display_list. Now that all of the code here + // in canvas.cc will direct calls to the DisplayListBuilder we could almost + // stop initializing that field for the display list case. Unfortunately, + // the text code in paragraph.cc still needs to present its output to an + // SkCanvas* which means without significant work to the internals of the + // paragraph code, we are going to continue to need the canvas adapter and + // field and getter. fml::RefPtr canvas = fml::MakeRefCounted( recorder->BeginRecording(SkRect::MakeLTRB(left, top, right, bottom))); recorder->set_canvas(canvas); @@ -94,18 +103,27 @@ Canvas::Canvas(SkCanvas* canvas) : canvas_(canvas) {} Canvas::~Canvas() {} void Canvas::save() { - if (!canvas_) { - return; + if (display_list_recorder_) { + builder()->save(); + } else if (canvas_) { + canvas_->save(); } - canvas_->save(); } void Canvas::saveLayerWithoutBounds(const Paint& paint, const PaintData& paint_data) { - if (!canvas_) { - return; + FML_DCHECK(paint.isNotNull()); + if (display_list_recorder_) { + bool restore_with_paint = + paint.sync_to(builder(), kSaveLayerWithPaintFlags); + FML_DCHECK(restore_with_paint); + TRACE_EVENT0("flutter", "ui.Canvas::saveLayer (Recorded)"); + builder()->saveLayer(nullptr, restore_with_paint); + } else if (canvas_) { + SkPaint sk_paint; + TRACE_EVENT0("flutter", "ui.Canvas::saveLayer (Recorded)"); + canvas_->saveLayer(nullptr, paint.paint(sk_paint)); } - canvas_->saveLayer(nullptr, paint.paint()); } void Canvas::saveLayer(double left, @@ -114,63 +132,88 @@ void Canvas::saveLayer(double left, double bottom, const Paint& paint, const PaintData& paint_data) { - if (!canvas_) { - return; - } + FML_DCHECK(paint.isNotNull()); SkRect bounds = SkRect::MakeLTRB(left, top, right, bottom); - canvas_->saveLayer(&bounds, paint.paint()); + if (display_list_recorder_) { + bool restore_with_paint = + paint.sync_to(builder(), kSaveLayerWithPaintFlags); + FML_DCHECK(restore_with_paint); + TRACE_EVENT0("flutter", "ui.Canvas::saveLayer (Recorded)"); + builder()->saveLayer(&bounds, restore_with_paint); + } else if (canvas_) { + SkPaint sk_paint; + TRACE_EVENT0("flutter", "ui.Canvas::saveLayer (Recorded)"); + canvas_->saveLayer(&bounds, paint.paint(sk_paint)); + } } void Canvas::restore() { - if (!canvas_) { - return; + if (display_list_recorder_) { + builder()->restore(); + } else if (canvas_) { + canvas_->restore(); } - canvas_->restore(); } int Canvas::getSaveCount() { - if (!canvas_) { + if (display_list_recorder_) { + return builder()->getSaveCount(); + } else if (canvas_) { + return canvas_->getSaveCount(); + } else { return 0; } - return canvas_->getSaveCount(); } void Canvas::translate(double dx, double dy) { - if (!canvas_) { - return; + if (display_list_recorder_) { + builder()->translate(dx, dy); + } else if (canvas_) { + canvas_->translate(dx, dy); } - canvas_->translate(dx, dy); } void Canvas::scale(double sx, double sy) { - if (!canvas_) { - return; + if (display_list_recorder_) { + builder()->scale(sx, sy); + } else if (canvas_) { + canvas_->scale(sx, sy); } - canvas_->scale(sx, sy); } void Canvas::rotate(double radians) { - if (!canvas_) { - return; + if (display_list_recorder_) { + builder()->rotate(radians * 180.0 / M_PI); + } else if (canvas_) { + canvas_->rotate(radians * 180.0 / M_PI); } - canvas_->rotate(radians * 180.0 / M_PI); } void Canvas::skew(double sx, double sy) { - if (!canvas_) { - return; + if (display_list_recorder_) { + builder()->skew(sx, sy); + } else if (canvas_) { + canvas_->skew(sx, sy); } - canvas_->skew(sx, sy); } void Canvas::transform(const tonic::Float64List& matrix4) { - if (!canvas_) { - return; + // The Float array stored by Dart Matrix4 is in column-major order + // Both DisplayList and SkM44 constructor take row-major matrix order + if (display_list_recorder_) { + // clang-format off + builder()->transformFullPerspective( + matrix4[ 0], matrix4[ 4], matrix4[ 8], matrix4[12], + matrix4[ 1], matrix4[ 5], matrix4[ 9], matrix4[13], + matrix4[ 2], matrix4[ 6], matrix4[10], matrix4[14], + matrix4[ 3], matrix4[ 7], matrix4[11], matrix4[15]); + // clang-format on + } else if (canvas_) { + canvas_->concat(SkM44(matrix4[0], matrix4[4], matrix4[8], matrix4[12], + matrix4[1], matrix4[5], matrix4[9], matrix4[13], + matrix4[2], matrix4[6], matrix4[10], matrix4[14], + matrix4[3], matrix4[7], matrix4[11], matrix4[15])); } - canvas_->concat(SkM44(matrix4[0], matrix4[4], matrix4[8], matrix4[12], - matrix4[1], matrix4[5], matrix4[9], matrix4[13], - matrix4[2], matrix4[6], matrix4[10], matrix4[14], - matrix4[3], matrix4[7], matrix4[11], matrix4[15])); } void Canvas::clipRect(double left, @@ -179,37 +222,42 @@ void Canvas::clipRect(double left, double bottom, SkClipOp clipOp, bool doAntiAlias) { - if (!canvas_) { - return; + if (display_list_recorder_) { + builder()->clipRect(SkRect::MakeLTRB(left, top, right, bottom), clipOp, + doAntiAlias); + } else if (canvas_) { + canvas_->clipRect(SkRect::MakeLTRB(left, top, right, bottom), clipOp, + doAntiAlias); } - canvas_->clipRect(SkRect::MakeLTRB(left, top, right, bottom), clipOp, - doAntiAlias); } void Canvas::clipRRect(const RRect& rrect, bool doAntiAlias) { - if (!canvas_) { - return; + if (display_list_recorder_) { + builder()->clipRRect(rrect.sk_rrect, SkClipOp::kIntersect, doAntiAlias); + } else if (canvas_) { + canvas_->clipRRect(rrect.sk_rrect, doAntiAlias); } - canvas_->clipRRect(rrect.sk_rrect, doAntiAlias); } void Canvas::clipPath(const CanvasPath* path, bool doAntiAlias) { - if (!canvas_) { - return; - } if (!path) { Dart_ThrowException( ToDart("Canvas.clipPath called with non-genuine Path.")); return; } - canvas_->clipPath(path->path(), doAntiAlias); + if (display_list_recorder_) { + builder()->clipPath(path->path(), SkClipOp::kIntersect, doAntiAlias); + } else if (canvas_) { + canvas_->clipPath(path->path(), doAntiAlias); + } } void Canvas::drawColor(SkColor color, SkBlendMode blend_mode) { - if (!canvas_) { - return; + if (display_list_recorder_) { + builder()->drawColor(color, blend_mode); + } else if (canvas_) { + canvas_->drawColor(color, blend_mode); } - canvas_->drawColor(color, blend_mode); } void Canvas::drawLine(double x1, @@ -218,17 +266,38 @@ void Canvas::drawLine(double x1, double y2, const Paint& paint, const PaintData& paint_data) { - if (!canvas_) { - return; + FML_DCHECK(paint.isNotNull()); + if (display_list_recorder_) { + paint.sync_to(builder(), kDrawLineFlags); + builder()->drawLine(SkPoint::Make(x1, y1), SkPoint::Make(x2, y2)); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawLine(x1, y1, x2, y2, *paint.paint(sk_paint)); } - canvas_->drawLine(x1, y1, x2, y2, *paint.paint()); } void Canvas::drawPaint(const Paint& paint, const PaintData& paint_data) { - if (!canvas_) { - return; + FML_DCHECK(paint.isNotNull()); + if (display_list_recorder_) { + paint.sync_to(builder(), kDrawPaintFlags); + sk_sp filter = builder()->getImageFilter(); + if (filter && !filter->asColorFilter(nullptr)) { + // drawPaint does an implicit saveLayer if an SkImageFilter is + // present that cannot be replaced by an SkColorFilter. + TRACE_EVENT0("flutter", "ui.Canvas::saveLayer (Recorded)"); + } + builder()->drawPaint(); + } else if (canvas_) { + SkPaint sk_paint; + paint.paint(sk_paint); + SkImageFilter* filter = sk_paint.getImageFilter(); + if (filter && !filter->asColorFilter(nullptr)) { + // drawPaint does an implicit saveLayer if an SkImageFilter is + // present that cannot be replaced by an SkColorFilter. + TRACE_EVENT0("flutter", "ui.Canvas::saveLayer (Recorded)"); + } + canvas_->drawPaint(sk_paint); } - canvas_->drawPaint(*paint.paint()); } void Canvas::drawRect(double left, @@ -237,29 +306,42 @@ void Canvas::drawRect(double left, double bottom, const Paint& paint, const PaintData& paint_data) { - if (!canvas_) { - return; + FML_DCHECK(paint.isNotNull()); + if (display_list_recorder_) { + paint.sync_to(builder(), kDrawRectFlags); + builder()->drawRect(SkRect::MakeLTRB(left, top, right, bottom)); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawRect(SkRect::MakeLTRB(left, top, right, bottom), + *paint.paint(sk_paint)); } - canvas_->drawRect(SkRect::MakeLTRB(left, top, right, bottom), *paint.paint()); } void Canvas::drawRRect(const RRect& rrect, const Paint& paint, const PaintData& paint_data) { - if (!canvas_) { - return; + FML_DCHECK(paint.isNotNull()); + if (display_list_recorder_) { + paint.sync_to(builder(), kDrawRRectFlags); + builder()->drawRRect(rrect.sk_rrect); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawRRect(rrect.sk_rrect, *paint.paint(sk_paint)); } - canvas_->drawRRect(rrect.sk_rrect, *paint.paint()); } void Canvas::drawDRRect(const RRect& outer, const RRect& inner, const Paint& paint, const PaintData& paint_data) { - if (!canvas_) { - return; + FML_DCHECK(paint.isNotNull()); + if (display_list_recorder_) { + paint.sync_to(builder(), kDrawDRRectFlags); + builder()->drawDRRect(outer.sk_rrect, inner.sk_rrect); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawDRRect(outer.sk_rrect, inner.sk_rrect, *paint.paint(sk_paint)); } - canvas_->drawDRRect(outer.sk_rrect, inner.sk_rrect, *paint.paint()); } void Canvas::drawOval(double left, @@ -268,10 +350,15 @@ void Canvas::drawOval(double left, double bottom, const Paint& paint, const PaintData& paint_data) { - if (!canvas_) { - return; + FML_DCHECK(paint.isNotNull()); + if (display_list_recorder_) { + paint.sync_to(builder(), kDrawOvalFlags); + builder()->drawOval(SkRect::MakeLTRB(left, top, right, bottom)); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawOval(SkRect::MakeLTRB(left, top, right, bottom), + *paint.paint(sk_paint)); } - canvas_->drawOval(SkRect::MakeLTRB(left, top, right, bottom), *paint.paint()); } void Canvas::drawCircle(double x, @@ -279,10 +366,14 @@ void Canvas::drawCircle(double x, double radius, const Paint& paint, const PaintData& paint_data) { - if (!canvas_) { - return; + FML_DCHECK(paint.isNotNull()); + if (display_list_recorder_) { + paint.sync_to(builder(), kDrawCircleFlags); + builder()->drawCircle(SkPoint::Make(x, y), radius); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawCircle(x, y, radius, *paint.paint(sk_paint)); } - canvas_->drawCircle(x, y, radius, *paint.paint()); } void Canvas::drawArc(double left, @@ -294,26 +385,39 @@ void Canvas::drawArc(double left, bool useCenter, const Paint& paint, const PaintData& paint_data) { - if (!canvas_) { - return; + FML_DCHECK(paint.isNotNull()); + if (display_list_recorder_) { + paint.sync_to(builder(), + useCenter // + ? kDrawArcWithCenterFlags + : kDrawArcNoCenterFlags); + builder()->drawArc(SkRect::MakeLTRB(left, top, right, bottom), + startAngle * 180.0 / M_PI, sweepAngle * 180.0 / M_PI, + useCenter); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawArc(SkRect::MakeLTRB(left, top, right, bottom), + startAngle * 180.0 / M_PI, sweepAngle * 180.0 / M_PI, + useCenter, *paint.paint(sk_paint)); } - canvas_->drawArc(SkRect::MakeLTRB(left, top, right, bottom), - startAngle * 180.0 / M_PI, sweepAngle * 180.0 / M_PI, - useCenter, *paint.paint()); } void Canvas::drawPath(const CanvasPath* path, const Paint& paint, const PaintData& paint_data) { - if (!canvas_) { - return; - } + FML_DCHECK(paint.isNotNull()); if (!path) { Dart_ThrowException( ToDart("Canvas.drawPath called with non-genuine Path.")); return; } - canvas_->drawPath(path->path(), *paint.paint()); + if (display_list_recorder_) { + paint.sync_to(builder(), kDrawPathFlags); + builder()->drawPath(path->path()); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawPath(path->path(), *paint.paint(sk_paint)); + } } void Canvas::drawImage(const CanvasImage* image, @@ -322,16 +426,21 @@ void Canvas::drawImage(const CanvasImage* image, const Paint& paint, const PaintData& paint_data, int filterQualityIndex) { - if (!canvas_) { - return; - } + FML_DCHECK(paint.isNotNull()); if (!image) { Dart_ThrowException( ToDart("Canvas.drawImage called with non-genuine Image.")); return; } auto sampling = ImageFilter::SamplingFromIndex(filterQualityIndex); - canvas_->drawImage(image->image(), x, y, sampling, paint.paint()); + if (display_list_recorder_) { + bool with_attributes = paint.sync_to(builder(), kDrawImageWithPaintFlags); + builder()->drawImage(image->image(), SkPoint::Make(x, y), sampling, + with_attributes); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawImage(image->image(), x, y, sampling, paint.paint(sk_paint)); + } } void Canvas::drawImageRect(const CanvasImage* image, @@ -346,9 +455,7 @@ void Canvas::drawImageRect(const CanvasImage* image, const Paint& paint, const PaintData& paint_data, int filterQualityIndex) { - if (!canvas_) { - return; - } + FML_DCHECK(paint.isNotNull()); if (!image) { Dart_ThrowException( ToDart("Canvas.drawImageRect called with non-genuine Image.")); @@ -357,8 +464,18 @@ void Canvas::drawImageRect(const CanvasImage* image, SkRect src = SkRect::MakeLTRB(src_left, src_top, src_right, src_bottom); SkRect dst = SkRect::MakeLTRB(dst_left, dst_top, dst_right, dst_bottom); auto sampling = ImageFilter::SamplingFromIndex(filterQualityIndex); - canvas_->drawImageRect(image->image(), src, dst, sampling, paint.paint(), - SkCanvas::kFast_SrcRectConstraint); + if (display_list_recorder_) { + bool with_attributes = + paint.sync_to(builder(), kDrawImageRectWithPaintFlags); + builder()->drawImageRect(image->image(), src, dst, sampling, + with_attributes, + SkCanvas::kFast_SrcRectConstraint); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawImageRect(image->image(), src, dst, sampling, + paint.paint(sk_paint), + SkCanvas::kFast_SrcRectConstraint); + } } void Canvas::drawImageNine(const CanvasImage* image, @@ -373,9 +490,7 @@ void Canvas::drawImageNine(const CanvasImage* image, const Paint& paint, const PaintData& paint_data, int bitmapSamplingIndex) { - if (!canvas_) { - return; - } + FML_DCHECK(paint.isNotNull()); if (!image) { Dart_ThrowException( ToDart("Canvas.drawImageNine called with non-genuine Image.")); @@ -388,35 +503,33 @@ void Canvas::drawImageNine(const CanvasImage* image, SkRect dst = SkRect::MakeLTRB(dst_left, dst_top, dst_right, dst_bottom); auto filter = ImageFilter::FilterModeFromIndex(bitmapSamplingIndex); if (display_list_recorder_) { - // SkCanvas turns a simple 2-rect DrawImageNine operation into a - // drawImageLattice operation which has arrays to allocate and - // pass along. For simplicity, we will bypass the canvas and ask - // the recorder to record our paint attributes and record a much - // simpler DrawImageNineOp record directly. - display_list_recorder_->RecordPaintAttributes( - paint.paint(), DisplayListCanvasRecorder::DrawType::kImageOpType); - builder()->drawImageNine(image->image(), icenter, dst, filter, true); - } else { + bool with_attributes = + paint.sync_to(builder(), kDrawImageNineWithPaintFlags); + builder()->drawImageNine(image->image(), icenter, dst, filter, + with_attributes); + } else if (canvas_) { + SkPaint sk_paint; canvas_->drawImageNine(image->image().get(), icenter, dst, filter, - paint.paint()); + paint.paint(sk_paint)); } } void Canvas::drawPicture(Picture* picture) { - if (!canvas_) { - return; - } if (!picture) { Dart_ThrowException( ToDart("Canvas.drawPicture called with non-genuine Picture.")); return; } if (picture->picture()) { - canvas_->drawPicture(picture->picture().get()); + if (display_list_recorder_) { + builder()->drawPicture(picture->picture(), nullptr, false); + } else if (canvas_) { + canvas_->drawPicture(picture->picture().get()); + } } else if (picture->display_list()) { if (display_list_recorder_) { builder()->drawDisplayList(picture->display_list()); - } else { + } else if (canvas_) { picture->display_list()->RenderTo(canvas_); } } else { @@ -428,32 +541,52 @@ void Canvas::drawPoints(const Paint& paint, const PaintData& paint_data, SkCanvas::PointMode point_mode, const tonic::Float32List& points) { - if (!canvas_) { - return; - } - static_assert(sizeof(SkPoint) == sizeof(float) * 2, "SkPoint doesn't use floats."); - canvas_->drawPoints(point_mode, - points.num_elements() / 2, // SkPoints have two floats. - reinterpret_cast(points.data()), - *paint.paint()); + FML_DCHECK(paint.isNotNull()); + if (display_list_recorder_) { + switch (point_mode) { + case SkCanvas::kPoints_PointMode: + paint.sync_to(builder(), kDrawPointsAsPointsFlags); + break; + case SkCanvas::kLines_PointMode: + paint.sync_to(builder(), kDrawPointsAsLinesFlags); + break; + case SkCanvas::kPolygon_PointMode: + paint.sync_to(builder(), kDrawPointsAsPolygonFlags); + break; + } + builder()->drawPoints(point_mode, + points.num_elements() / 2, // SkPoints have 2 floats + reinterpret_cast(points.data())); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawPoints(point_mode, + points.num_elements() / 2, // SkPoints have 2 floats + reinterpret_cast(points.data()), + *paint.paint(sk_paint)); + } } void Canvas::drawVertices(const Vertices* vertices, SkBlendMode blend_mode, const Paint& paint, const PaintData& paint_data) { - if (!canvas_) { - return; - } if (!vertices) { Dart_ThrowException( ToDart("Canvas.drawVertices called with non-genuine Vertices.")); return; } - canvas_->drawVertices(vertices->vertices(), blend_mode, *paint.paint()); + FML_DCHECK(paint.isNotNull()); + if (display_list_recorder_) { + paint.sync_to(builder(), kDrawVerticesFlags); + builder()->drawVertices(vertices->vertices(), blend_mode); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawVertices(vertices->vertices(), blend_mode, + *paint.paint(sk_paint)); + } } void Canvas::drawAtlas(const Paint& paint, @@ -465,9 +598,6 @@ void Canvas::drawAtlas(const Paint& paint, const tonic::Int32List& colors, SkBlendMode blend_mode, const tonic::Float32List& cull_rect) { - if (!canvas_) { - return; - } if (!atlas) { Dart_ThrowException( ToDart("Canvas.drawAtlas or Canvas.drawRawAtlas called with " @@ -484,13 +614,26 @@ void Canvas::drawAtlas(const Paint& paint, auto sampling = ImageFilter::SamplingFromIndex(filterQualityIndex); - canvas_->drawAtlas( - skImage.get(), reinterpret_cast(transforms.data()), - reinterpret_cast(rects.data()), - reinterpret_cast(colors.data()), - rects.num_elements() / 4, // SkRect have four floats. - blend_mode, sampling, reinterpret_cast(cull_rect.data()), - paint.paint()); + FML_DCHECK(paint.isNotNull()); + if (display_list_recorder_) { + bool with_attributes = paint.sync_to(builder(), kDrawAtlasWithPaintFlags); + builder()->drawAtlas( + skImage, reinterpret_cast(transforms.data()), + reinterpret_cast(rects.data()), + reinterpret_cast(colors.data()), + rects.num_elements() / 4, // SkRect have four floats. + blend_mode, sampling, reinterpret_cast(cull_rect.data()), + with_attributes); + } else if (canvas_) { + SkPaint sk_paint; + canvas_->drawAtlas( + skImage.get(), reinterpret_cast(transforms.data()), + reinterpret_cast(rects.data()), + reinterpret_cast(colors.data()), + rects.num_elements() / 4, // SkRect have four floats. + blend_mode, sampling, reinterpret_cast(cull_rect.data()), + paint.paint(sk_paint)); + } } void Canvas::drawShadow(const CanvasPath* path, @@ -517,7 +660,7 @@ void Canvas::drawShadow(const CanvasPath* path, // See: https://bugs.chromium.org/p/skia/issues/detail?id=12125 builder()->drawShadow(path->path(), color, elevation, transparentOccluder, dpr); - } else { + } else if (canvas_) { flutter::PhysicalShapeLayer::DrawShadow( canvas_, path->path(), color, elevation, transparentOccluder, dpr); } @@ -525,6 +668,7 @@ void Canvas::drawShadow(const CanvasPath* path, void Canvas::Invalidate() { canvas_ = nullptr; + display_list_recorder_ = nullptr; if (dart_wrapper()) { ClearDartWrapper(); } diff --git a/lib/ui/painting/canvas.h b/lib/ui/painting/canvas.h index ca328d310dbb6..8e8fd107f9377 100644 --- a/lib/ui/painting/canvas.h +++ b/lib/ui/painting/canvas.h @@ -23,7 +23,7 @@ class DartLibraryNatives; namespace flutter { class CanvasImage; -class Canvas : public RefCountedDartWrappable { +class Canvas : public RefCountedDartWrappable, DisplayListOpFlags { DEFINE_WRAPPERTYPEINFO(); FML_FRIEND_MAKE_REF_COUNTED(Canvas); @@ -188,8 +188,8 @@ class Canvas : public RefCountedDartWrappable { // paint attributes from an SkPaint and an operation type as well as access // to the raw DisplayListBuilder for emitting custom rendering operations. sk_sp display_list_recorder_; - sk_sp builder() { - return display_list_recorder_->builder(); + DisplayListBuilder* builder() { + return display_list_recorder_->builder().get(); } }; diff --git a/lib/ui/painting/fragment_program.cc b/lib/ui/painting/fragment_program.cc new file mode 100644 index 0000000000000..b166a497d1d22 --- /dev/null +++ b/lib/ui/painting/fragment_program.cc @@ -0,0 +1,96 @@ +// 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 "flutter/lib/ui/painting/fragment_program.h" + +#include "flutter/lib/ui/dart_wrapper.h" +#include "flutter/lib/ui/ui_dart_state.h" +#include "third_party/skia/include/core/SkString.h" +#include "third_party/tonic/converter/dart_converter.h" +#include "third_party/tonic/dart_args.h" +#include "third_party/tonic/dart_binding_macros.h" +#include "third_party/tonic/dart_library_natives.h" +#include "third_party/tonic/typed_data/typed_list.h" + +using tonic::ToDart; + +namespace flutter { + +static void FragmentProgram_constructor(Dart_NativeArguments args) { + DartCallConstructor(&FragmentProgram::Create, args); +} + +IMPLEMENT_WRAPPERTYPEINFO(ui, FragmentProgram); + +#define FOR_EACH_BINDING(V) \ + V(FragmentProgram, init) \ + V(FragmentProgram, shader) + +FOR_EACH_BINDING(DART_NATIVE_CALLBACK) + +void FragmentProgram::RegisterNatives(tonic::DartLibraryNatives* natives) { + natives->Register( + {{"FragmentProgram_constructor", FragmentProgram_constructor, 1, true}, + FOR_EACH_BINDING(DART_REGISTER_NATIVE)}); +} + +void FragmentProgram::init(std::string sksl, bool debugPrintSksl) { + SkRuntimeEffect::Result result = + SkRuntimeEffect::MakeForShader(SkString(sksl)); + runtime_effect_ = result.effect; + + if (runtime_effect_ == nullptr) { + Dart_ThrowException(tonic::ToDart( + std::string("Invalid SkSL:\n") + sksl.c_str() + + std::string("\nSkSL Error:\n") + result.errorText.c_str())); + return; + } + if (debugPrintSksl) { + FML_DLOG(INFO) << std::string("debugPrintSksl:\n") + sksl.c_str(); + } +} + +fml::RefPtr FragmentProgram::shader( + Dart_Handle shader, + const tonic::Float32List& uniforms, + Dart_Handle samplers) { + auto sampler_shaders = + tonic::DartConverter>::FromDart(samplers); + size_t uniform_count = uniforms.num_elements(); + size_t uniform_data_size = + (uniform_count + 2 * sampler_shaders.size()) * sizeof(float); + sk_sp uniform_data = SkData::MakeUninitialized(uniform_data_size); + // uniform_floats must only be referenced BEFORE the call to makeShader below. + auto* uniform_floats = + reinterpret_cast(uniform_data->writable_data()); + for (size_t i = 0; i < uniform_count; i++) { + uniform_floats[i] = uniforms[i]; + } + std::vector> sk_samplers(sampler_shaders.size()); + for (size_t i = 0; i < sampler_shaders.size(); i++) { + ImageShader* image_shader = sampler_shaders[i]; + // The default value for SkSamplingOptions is used because ImageShader + // uses a cached value set by the user in the Dart constructor. + // Users are instructed to make use of this in the Dart docs. + sk_samplers[i] = image_shader->shader(SkSamplingOptions()); + uniform_floats[uniform_count + 2 * i] = image_shader->width(); + uniform_floats[uniform_count + 2 * i + 1] = image_shader->height(); + } + auto sk_shader = + runtime_effect_->makeShader(std::move(uniform_data), sk_samplers.data(), + sk_samplers.size(), nullptr, false); + return FragmentShader::Create(shader, std::move(sk_shader)); +} + +fml::RefPtr FragmentProgram::Create() { + return fml::MakeRefCounted(); +} + +FragmentProgram::FragmentProgram() = default; + +FragmentProgram::~FragmentProgram() = default; + +} // namespace flutter diff --git a/lib/ui/painting/fragment_program.h b/lib/ui/painting/fragment_program.h new file mode 100644 index 0000000000000..06cf429b32e42 --- /dev/null +++ b/lib/ui/painting/fragment_program.h @@ -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. + +#ifndef FLUTTER_LIB_UI_PAINTING_FRAGMENT_PROGRAM_H_ +#define FLUTTER_LIB_UI_PAINTING_FRAGMENT_PROGRAM_H_ + +#include "flutter/lib/ui/dart_wrapper.h" +#include "flutter/lib/ui/painting/fragment_shader.h" +#include "third_party/skia/include/effects/SkRuntimeEffect.h" +#include "third_party/tonic/dart_library_natives.h" +#include "third_party/tonic/typed_data/typed_list.h" + +#include +#include + +namespace tonic { +class DartLibraryNatives; +} // namespace tonic + +namespace flutter { + +class FragmentProgram : public RefCountedDartWrappable { + DEFINE_WRAPPERTYPEINFO(); + FML_FRIEND_MAKE_REF_COUNTED(FragmentProgram); + + public: + ~FragmentProgram() override; + static fml::RefPtr Create(); + + void init(std::string sksl, bool debugPrintSksl); + + fml::RefPtr shader(Dart_Handle shader, + const tonic::Float32List& uniforms, + Dart_Handle samplers); + + static void RegisterNatives(tonic::DartLibraryNatives* natives); + + private: + FragmentProgram(); + sk_sp runtime_effect_; +}; + +} // namespace flutter + +#endif // FLUTTER_LIB_UI_PAINTING_FRAGMENT_PROGRAM_H_ diff --git a/lib/ui/painting/fragment_shader.cc b/lib/ui/painting/fragment_shader.cc index a9a59e399b408..c72460f54fc00 100644 --- a/lib/ui/painting/fragment_shader.cc +++ b/lib/ui/painting/fragment_shader.cc @@ -19,60 +19,35 @@ using tonic::ToDart; namespace flutter { -static void FragmentShader_constructor(Dart_NativeArguments args) { - DartCallConstructor(&FragmentShader::Create, args); -} - -IMPLEMENT_WRAPPERTYPEINFO(ui, FragmentShader); - -#define FOR_EACH_BINDING(V) \ - V(FragmentShader, init) \ - V(FragmentShader, update) - -FOR_EACH_BINDING(DART_NATIVE_CALLBACK) +// Since _FragmentShader is a private class, we can't use +// IMPLEMENT_WRAPPERTYPEINFO +static const tonic::DartWrapperInfo kDartWrapperInfo_ui_FragmentShader = { + "ui", + "_FragmentShader", + sizeof(FragmentShader), +}; +const tonic::DartWrapperInfo& FragmentShader::dart_wrapper_info_ = + kDartWrapperInfo_ui_FragmentShader; void FragmentShader::RegisterNatives(tonic::DartLibraryNatives* natives) { - natives->Register( - {{"FragmentShader_constructor", FragmentShader_constructor, 1, true}, - FOR_EACH_BINDING(DART_REGISTER_NATIVE)}); + natives->Register({}); } sk_sp FragmentShader::shader(SkSamplingOptions sampling) { - // TODO(antrob): Use sampling? - // https://github.com/flutter/flutter/issues/88303 + // Sampling options are ignored, since sampling options don't make sense for + // generative shaders. return shader_; } -void FragmentShader::init(std::string sksl, bool debugPrintSksl) { - SkRuntimeEffect::Result result = - SkRuntimeEffect::MakeForShader(SkString(sksl)); - runtime_effect_ = result.effect; - - if (runtime_effect_ == nullptr) { - Dart_ThrowException(tonic::ToDart( - std::string("Invalid SkSL:\n") + sksl.c_str() + - std::string("\nSkSL Error:\n") + result.errorText.c_str())); - return; - } - if (debugPrintSksl) { - FML_DLOG(INFO) << std::string("debugPrintSksl:\n") + sksl.c_str(); - } -} - -// TODO(https://github.com/flutter/flutter/issues/85240): -// Add `Dart_Handle children` as a paramter. -void FragmentShader::update(const tonic::Float32List& uniforms) { - shader_ = runtime_effect_->makeShader( - SkData::MakeWithCopy(uniforms.data(), - uniforms.num_elements() * sizeof(float)), - 0, 0, nullptr, false); -} - -fml::RefPtr FragmentShader::Create() { - return fml::MakeRefCounted(); +fml::RefPtr FragmentShader::Create(Dart_Handle dart_handle, + sk_sp shader) { + auto fragment_shader = fml::MakeRefCounted(std::move(shader)); + fragment_shader->AssociateWithDartWrapper(dart_handle); + return fragment_shader; } -FragmentShader::FragmentShader() = default; +FragmentShader::FragmentShader(sk_sp shader) + : shader_(std::move(shader)) {} FragmentShader::~FragmentShader() = default; diff --git a/lib/ui/painting/fragment_shader.h b/lib/ui/painting/fragment_shader.h index 8a1a12e4e2959..446116a2edf7a 100644 --- a/lib/ui/painting/fragment_shader.h +++ b/lib/ui/painting/fragment_shader.h @@ -29,26 +29,16 @@ class FragmentShader : public Shader { public: ~FragmentShader() override; - static fml::RefPtr Create(); + static fml::RefPtr Create(Dart_Handle dart_handle, + sk_sp shader); sk_sp shader(SkSamplingOptions) override; - void init(std::string sksl, bool debugPrintSksl); - - void update(const tonic::Float32List& uniforms); - static void RegisterNatives(tonic::DartLibraryNatives* natives); private: - FragmentShader(); - - void setShader(); - - // Since the shader source cannot be updated, the effect can be - // created once and re-used. - sk_sp runtime_effect_; + explicit FragmentShader(sk_sp shader); - // A new shader is created every time update is called. sk_sp shader_; }; diff --git a/lib/ui/painting/image_decoder.h b/lib/ui/painting/image_decoder.h index e5f2fba6df1ad..74a1ed0a05381 100644 --- a/lib/ui/painting/image_decoder.h +++ b/lib/ui/painting/image_decoder.h @@ -57,7 +57,6 @@ class ImageDecoder { std::shared_ptr concurrent_task_runner_; fml::WeakPtr io_manager_; fml::WeakPtrFactory weak_factory_; - FML_DISALLOW_COPY_AND_ASSIGN(ImageDecoder); }; diff --git a/lib/ui/painting/image_filter.cc b/lib/ui/painting/image_filter.cc index 2b20b22ac5f6e..d4c2974172e9f 100644 --- a/lib/ui/painting/image_filter.cc +++ b/lib/ui/painting/image_filter.cc @@ -50,7 +50,8 @@ static const std::array filter_qualities = { SkSamplingOptions ImageFilter::SamplingFromIndex(int filterQualityIndex) { if (filterQualityIndex < 0) { return filter_qualities.front(); - } else if (((size_t)filterQualityIndex) >= filter_qualities.size()) { + } else if (static_cast(filterQualityIndex) >= + filter_qualities.size()) { return filter_qualities.back(); } else { return filter_qualities[filterQualityIndex]; diff --git a/lib/ui/painting/image_generator.cc b/lib/ui/painting/image_generator.cc index 015713dcd453d..59ee40768fe8f 100644 --- a/lib/ui/painting/image_generator.cc +++ b/lib/ui/painting/image_generator.cc @@ -102,7 +102,7 @@ unsigned int BuiltinSkiaCodecImageGenerator::GetPlayCount() const { const ImageGenerator::FrameInfo BuiltinSkiaCodecImageGenerator::GetFrameInfo( unsigned int frame_index) const { - SkCodec::FrameInfo info; + SkCodec::FrameInfo info = {}; codec_generator_->getFrameInfo(frame_index, &info); return { .required_frame = info.fRequiredFrame == SkCodec::kNoFrame diff --git a/lib/ui/painting/image_generator.h b/lib/ui/painting/image_generator.h index ae5816374427e..c3ed972259f3e 100644 --- a/lib/ui/painting/image_generator.h +++ b/lib/ui/painting/image_generator.h @@ -133,7 +133,8 @@ class BuiltinSkiaImageGenerator : public ImageGenerator { public: ~BuiltinSkiaImageGenerator(); - BuiltinSkiaImageGenerator(std::unique_ptr generator); + explicit BuiltinSkiaImageGenerator( + std::unique_ptr generator); // |ImageGenerator| const SkImageInfo& GetInfo() override; @@ -171,9 +172,9 @@ class BuiltinSkiaCodecImageGenerator : public ImageGenerator { public: ~BuiltinSkiaCodecImageGenerator(); - BuiltinSkiaCodecImageGenerator(std::unique_ptr codec); + explicit BuiltinSkiaCodecImageGenerator(std::unique_ptr codec); - BuiltinSkiaCodecImageGenerator(sk_sp buffer); + explicit BuiltinSkiaCodecImageGenerator(sk_sp buffer); // |ImageGenerator| const SkImageInfo& GetInfo() override; diff --git a/lib/ui/painting/image_generator_registry.cc b/lib/ui/painting/image_generator_registry.cc index f48cfd5f8bb50..3d5d4cf5ecb98 100644 --- a/lib/ui/painting/image_generator_registry.cc +++ b/lib/ui/painting/image_generator_registry.cc @@ -4,7 +4,6 @@ #include -#include "flutter/fml/trace_event.h" #include "flutter/lib/ui/painting/image_generator_registry.h" #include "third_party/skia/include/codec/SkCodec.h" #include "third_party/skia/include/core/SkImageGenerator.h" @@ -48,8 +47,7 @@ ImageGeneratorRegistry::~ImageGeneratorRegistry() = default; void ImageGeneratorRegistry::AddFactory(ImageGeneratorFactory factory, int32_t priority) { - image_generator_factories_.insert( - {factory, priority, fml::tracing::TraceNonce()}); + image_generator_factories_.insert({factory, priority, ++nonce_}); } std::shared_ptr diff --git a/lib/ui/painting/image_generator_registry.h b/lib/ui/painting/image_generator_registry.h index 8c0d7d0f136b5..b04dc1cc24eba 100644 --- a/lib/ui/painting/image_generator_registry.h +++ b/lib/ui/painting/image_generator_registry.h @@ -84,6 +84,7 @@ class ImageGeneratorRegistry { using FactorySet = std::set; FactorySet image_generator_factories_; + size_t nonce_; fml::WeakPtrFactory weak_factory_; }; diff --git a/lib/ui/painting/image_generator_registry_unittests.cc b/lib/ui/painting/image_generator_registry_unittests.cc index 925eb919ba231..ea3fb2254c469 100644 --- a/lib/ui/painting/image_generator_registry_unittests.cc +++ b/lib/ui/painting/image_generator_registry_unittests.cc @@ -48,7 +48,7 @@ TEST_F(ShellTest, CreateCompatibleReturnsNullptrForInvalidImage) { class FakeImageGenerator : public ImageGenerator { public: - FakeImageGenerator(int identifiableFakeWidth) + explicit FakeImageGenerator(int identifiableFakeWidth) : info_(SkImageInfo::Make(identifiableFakeWidth, identifiableFakeWidth, SkColorType::kRGBA_8888_SkColorType, @@ -111,5 +111,42 @@ TEST_F(ShellTest, DefaultGeneratorsTakePrecedentOverNegativePriority) { ASSERT_EQ(result->GetInfo().width(), 3024); } +TEST_F(ShellTest, DefaultGeneratorsTakePrecedentOverZeroPriority) { + ImageGeneratorRegistry registry; + + registry.AddFactory( + [](sk_sp buffer) { + return std::make_unique(1337); + }, + 0); + + // Fetch the generator and query for basic info. + auto result = registry.CreateCompatibleGenerator(LoadValidImageFixture()); + // If the real width of the image pops out, then the default generator was + // returned rather than the fake one. + ASSERT_EQ(result->GetInfo().width(), 3024); +} + +TEST_F(ShellTest, ImageGeneratorsWithSamePriorityCascadeChronologically) { + ImageGeneratorRegistry registry; + + // Add 2 factories with the same high priority. + registry.AddFactory( + [](sk_sp buffer) { + return std::make_unique(1337); + }, + 5); + registry.AddFactory( + [](sk_sp buffer) { + return std::make_unique(7777); + }, + 5); + + // Feed empty data so that Skia's image generators will reject it, but ours + // won't. + auto result = registry.CreateCompatibleGenerator(SkData::MakeEmpty()); + ASSERT_EQ(result->GetInfo().width(), 1337); +} + } // namespace testing } // namespace flutter diff --git a/lib/ui/painting/image_shader.cc b/lib/ui/painting/image_shader.cc index 2b56ee08fd91d..60d08af12a8c9 100644 --- a/lib/ui/painting/image_shader.cc +++ b/lib/ui/painting/image_shader.cc @@ -70,6 +70,14 @@ sk_sp ImageShader::shader(SkSamplingOptions sampling) { return cached_shader_.skia_object(); } +int ImageShader::width() { + return sk_image_.skia_object()->width(); +} + +int ImageShader::height() { + return sk_image_.skia_object()->height(); +} + ImageShader::ImageShader() = default; ImageShader::~ImageShader() = default; diff --git a/lib/ui/painting/image_shader.h b/lib/ui/painting/image_shader.h index f7d4891c39ccc..6383f5ebce4de 100644 --- a/lib/ui/painting/image_shader.h +++ b/lib/ui/painting/image_shader.h @@ -38,6 +38,9 @@ class ImageShader : public Shader { static void RegisterNatives(tonic::DartLibraryNatives* natives); + int width(); + int height(); + private: ImageShader(); diff --git a/lib/ui/painting/immutable_buffer.h b/lib/ui/painting/immutable_buffer.h index a3a382e63bdfb..6306dd99f0a73 100644 --- a/lib/ui/painting/immutable_buffer.h +++ b/lib/ui/painting/immutable_buffer.h @@ -30,7 +30,7 @@ class ImmutableBuffer : public RefCountedDartWrappable { /// Initializes a new ImmutableData from a Dart Uint8List. /// - /// The zero indexed argument is the the caller that will be registered as the + /// The zero indexed argument is the caller that will be registered as the /// Dart peer of the native ImmutableBuffer object. /// /// The first indexed argumented is a tonic::Uint8List of bytes to copy. diff --git a/lib/ui/painting/multi_frame_codec.h b/lib/ui/painting/multi_frame_codec.h index 187bd59f7246f..0072131ad42e2 100644 --- a/lib/ui/painting/multi_frame_codec.h +++ b/lib/ui/painting/multi_frame_codec.h @@ -13,7 +13,7 @@ namespace flutter { class MultiFrameCodec : public Codec { public: - MultiFrameCodec(std::shared_ptr generator); + explicit MultiFrameCodec(std::shared_ptr generator); ~MultiFrameCodec() override; @@ -37,7 +37,7 @@ class MultiFrameCodec : public Codec { // shares it with the IO task runner's decoding work, and sets the live_ // member to false when it is destructed. struct State { - State(std::shared_ptr generator); + explicit State(std::shared_ptr generator); const std::shared_ptr generator_; const int frameCount_; diff --git a/lib/ui/painting/paint.cc b/lib/ui/painting/paint.cc index ee3c322418729..25e3913cfc323 100644 --- a/lib/ui/painting/paint.cc +++ b/lib/ui/painting/paint.cc @@ -65,28 +65,31 @@ constexpr float invert_colors[20] = { // Must be kept in sync with the MaskFilter private constants in painting.dart. enum MaskFilterType { Null, Blur }; -Paint::Paint(Dart_Handle paint_objects, Dart_Handle paint_data) { - is_null_ = Dart_IsNull(paint_data); - if (is_null_) { - return; +Paint::Paint(Dart_Handle paint_objects, Dart_Handle paint_data) + : paint_objects_(paint_objects), paint_data_(paint_data) {} + +const SkPaint* Paint::paint(SkPaint& paint) const { + if (isNull()) { + return nullptr; } + FML_DCHECK(paint == SkPaint()); - tonic::DartByteData byte_data(paint_data); + tonic::DartByteData byte_data(paint_data_); FML_CHECK(byte_data.length_in_bytes() == kDataByteCount); const uint32_t* uint_data = static_cast(byte_data.data()); const float* float_data = static_cast(byte_data.data()); Dart_Handle values[kObjectCount]; - if (!Dart_IsNull(paint_objects)) { - FML_DCHECK(Dart_IsList(paint_objects)); + if (!Dart_IsNull(paint_objects_)) { + FML_DCHECK(Dart_IsList(paint_objects_)); intptr_t length = 0; - Dart_ListLength(paint_objects, &length); + Dart_ListLength(paint_objects_, &length); FML_CHECK(length == kObjectCount); if (Dart_IsError( - Dart_ListGetRange(paint_objects, 0, kObjectCount, values))) { - return; + Dart_ListGetRange(paint_objects_, 0, kObjectCount, values))) { + return nullptr; } Dart_Handle shader = values[kShaderIndex]; @@ -94,75 +97,75 @@ Paint::Paint(Dart_Handle paint_objects, Dart_Handle paint_data) { Shader* decoded = tonic::DartConverter::FromDart(shader); auto sampling = ImageFilter::SamplingFromIndex(uint_data[kFilterQualityIndex]); - paint_.setShader(decoded->shader(sampling)); + paint.setShader(decoded->shader(sampling)); } Dart_Handle color_filter = values[kColorFilterIndex]; if (!Dart_IsNull(color_filter)) { ColorFilter* decoded_color_filter = tonic::DartConverter::FromDart(color_filter); - paint_.setColorFilter(decoded_color_filter->filter()); + paint.setColorFilter(decoded_color_filter->filter()); } Dart_Handle image_filter = values[kImageFilterIndex]; if (!Dart_IsNull(image_filter)) { ImageFilter* decoded = tonic::DartConverter::FromDart(image_filter); - paint_.setImageFilter(decoded->filter()); + paint.setImageFilter(decoded->filter()); } } - paint_.setAntiAlias(uint_data[kIsAntiAliasIndex] == 0); + paint.setAntiAlias(uint_data[kIsAntiAliasIndex] == 0); uint32_t encoded_color = uint_data[kColorIndex]; if (encoded_color) { SkColor color = encoded_color ^ kColorDefault; - paint_.setColor(color); + paint.setColor(color); } uint32_t encoded_blend_mode = uint_data[kBlendModeIndex]; if (encoded_blend_mode) { uint32_t blend_mode = encoded_blend_mode ^ kBlendModeDefault; - paint_.setBlendMode(static_cast(blend_mode)); + paint.setBlendMode(static_cast(blend_mode)); } uint32_t style = uint_data[kStyleIndex]; if (style) { - paint_.setStyle(static_cast(style)); + paint.setStyle(static_cast(style)); } float stroke_width = float_data[kStrokeWidthIndex]; if (stroke_width != 0.0) { - paint_.setStrokeWidth(stroke_width); + paint.setStrokeWidth(stroke_width); } uint32_t stroke_cap = uint_data[kStrokeCapIndex]; if (stroke_cap) { - paint_.setStrokeCap(static_cast(stroke_cap)); + paint.setStrokeCap(static_cast(stroke_cap)); } uint32_t stroke_join = uint_data[kStrokeJoinIndex]; if (stroke_join) { - paint_.setStrokeJoin(static_cast(stroke_join)); + paint.setStrokeJoin(static_cast(stroke_join)); } float stroke_miter_limit = float_data[kStrokeMiterLimitIndex]; if (stroke_miter_limit != 0.0) { - paint_.setStrokeMiter(stroke_miter_limit + kStrokeMiterLimitDefault); + paint.setStrokeMiter(stroke_miter_limit + kStrokeMiterLimitDefault); } if (uint_data[kInvertColorIndex]) { sk_sp invert_filter = ColorFilter::MakeColorMatrixFilter255(invert_colors); - sk_sp current_filter = paint_.refColorFilter(); + sk_sp current_filter = paint.refColorFilter(); if (current_filter) { invert_filter = invert_filter->makeComposed(current_filter); } - paint_.setColorFilter(invert_filter); + paint.setColorFilter(invert_filter); } if (uint_data[kDitherIndex]) { - paint_.setDither(true); + paint.setDither(true); } switch (uint_data[kMaskFilterIndex]) { @@ -172,9 +175,138 @@ Paint::Paint(Dart_Handle paint_objects, Dart_Handle paint_data) { SkBlurStyle blur_style = static_cast(uint_data[kMaskFilterBlurStyleIndex]); double sigma = float_data[kMaskFilterSigmaIndex]; - paint_.setMaskFilter(SkMaskFilter::MakeBlur(blur_style, sigma)); + paint.setMaskFilter(SkMaskFilter::MakeBlur(blur_style, sigma)); break; } + + return &paint; +} + +bool Paint::sync_to(DisplayListBuilder* builder, + const DisplayListAttributeFlags& flags) const { + if (isNull()) { + return false; + } + tonic::DartByteData byte_data(paint_data_); + FML_CHECK(byte_data.length_in_bytes() == kDataByteCount); + + const uint32_t* uint_data = static_cast(byte_data.data()); + const float* float_data = static_cast(byte_data.data()); + + Dart_Handle values[kObjectCount]; + if (Dart_IsNull(paint_objects_)) { + if (flags.applies_shader()) { + builder->setShader(nullptr); + } + if (flags.applies_color_filter()) { + builder->setColorFilter(nullptr); + } + if (flags.applies_image_filter()) { + builder->setImageFilter(nullptr); + } + } else { + FML_DCHECK(Dart_IsList(paint_objects_)); + intptr_t length = 0; + Dart_ListLength(paint_objects_, &length); + + FML_CHECK(length == kObjectCount); + if (Dart_IsError( + Dart_ListGetRange(paint_objects_, 0, kObjectCount, values))) { + return false; + } + + if (flags.applies_shader()) { + Dart_Handle shader = values[kShaderIndex]; + if (Dart_IsNull(shader)) { + builder->setShader(nullptr); + } else { + Shader* decoded = tonic::DartConverter::FromDart(shader); + auto sampling = + ImageFilter::SamplingFromIndex(uint_data[kFilterQualityIndex]); + builder->setShader(decoded->shader(sampling)); + } + } + + if (flags.applies_color_filter()) { + Dart_Handle color_filter = values[kColorFilterIndex]; + if (Dart_IsNull(color_filter)) { + builder->setColorFilter(nullptr); + } else { + ColorFilter* decoded_color_filter = + tonic::DartConverter::FromDart(color_filter); + builder->setColorFilter(decoded_color_filter->filter()); + } + } + + if (flags.applies_image_filter()) { + Dart_Handle image_filter = values[kImageFilterIndex]; + if (Dart_IsNull(image_filter)) { + builder->setImageFilter(nullptr); + } else { + ImageFilter* decoded = + tonic::DartConverter::FromDart(image_filter); + builder->setImageFilter(decoded->filter()); + } + } + } + + if (flags.applies_anti_alias()) { + builder->setAntiAlias(uint_data[kIsAntiAliasIndex] == 0); + } + + if (flags.applies_alpha_or_color()) { + uint32_t encoded_color = uint_data[kColorIndex]; + builder->setColor(encoded_color ^ kColorDefault); + } + + if (flags.applies_blend()) { + uint32_t encoded_blend_mode = uint_data[kBlendModeIndex]; + uint32_t blend_mode = encoded_blend_mode ^ kBlendModeDefault; + builder->setBlendMode(static_cast(blend_mode)); + } + + if (flags.applies_style()) { + uint32_t style = uint_data[kStyleIndex]; + builder->setStyle(static_cast(style)); + } + + if (flags.is_stroked(builder->getStyle())) { + float stroke_width = float_data[kStrokeWidthIndex]; + builder->setStrokeWidth(stroke_width); + + float stroke_miter_limit = float_data[kStrokeMiterLimitIndex]; + builder->setStrokeMiter(stroke_miter_limit + kStrokeMiterLimitDefault); + + uint32_t stroke_cap = uint_data[kStrokeCapIndex]; + builder->setStrokeCap(static_cast(stroke_cap)); + + uint32_t stroke_join = uint_data[kStrokeJoinIndex]; + builder->setStrokeJoin(static_cast(stroke_join)); + } + + if (flags.applies_color_filter()) { + builder->setInvertColors(uint_data[kInvertColorIndex] != 0); + } + + if (flags.applies_dither()) { + builder->setDither(uint_data[kDitherIndex] != 0); + } + + if (flags.applies_mask_filter()) { + switch (uint_data[kMaskFilterIndex]) { + case Null: + builder->setMaskFilter(nullptr); + break; + case Blur: + SkBlurStyle blur_style = + static_cast(uint_data[kMaskFilterBlurStyleIndex]); + double sigma = float_data[kMaskFilterSigmaIndex]; + builder->setMaskBlurFilter(blur_style, sigma); + break; + } + } + + return true; } } // namespace flutter diff --git a/lib/ui/painting/paint.h b/lib/ui/painting/paint.h index 2c6eef8455af2..63943003ef28d 100644 --- a/lib/ui/painting/paint.h +++ b/lib/ui/painting/paint.h @@ -8,6 +8,8 @@ #include "third_party/skia/include/core/SkPaint.h" #include "third_party/tonic/converter/dart_converter.h" +#include "flow/display_list.h" + namespace flutter { class Paint { @@ -15,13 +17,25 @@ class Paint { Paint() = default; Paint(Dart_Handle paint_objects, Dart_Handle paint_data); - const SkPaint* paint() const { return is_null_ ? nullptr : &paint_; } + const SkPaint* paint(SkPaint& paint) const; + + /// Synchronize the Dart properties to the display list according + /// to the attribute flags that indicate which properties are needed. + /// The return value indicates if the paint was non-null and can + /// either be DCHECKed or used to indicate to the DisplayList + /// draw operation whether or not to use the synchronized attributes + /// (mainly the drawImage and saveLayer methods). + bool sync_to(DisplayListBuilder* builder, + const DisplayListAttributeFlags& flags) const; + + bool isNull() const { return Dart_IsNull(paint_data_); } + bool isNotNull() const { return !Dart_IsNull(paint_data_); } private: friend struct tonic::DartConverter; - SkPaint paint_; - bool is_null_ = true; + Dart_Handle paint_objects_; + Dart_Handle paint_data_; }; // The PaintData argument is a placeholder to receive encoded data for Paint diff --git a/lib/ui/painting/picture.h b/lib/ui/painting/picture.h index bcaf4ebbc8314..8752cc83bde94 100644 --- a/lib/ui/painting/picture.h +++ b/lib/ui/painting/picture.h @@ -58,8 +58,8 @@ class Picture : public RefCountedDartWrappable { Dart_Handle raw_image_callback); private: - Picture(flutter::SkiaGPUObject picture); - Picture(flutter::SkiaGPUObject display_list); + explicit Picture(flutter::SkiaGPUObject picture); + explicit Picture(flutter::SkiaGPUObject display_list); flutter::SkiaGPUObject picture_; flutter::SkiaGPUObject display_list_; diff --git a/lib/ui/platform_dispatcher.dart b/lib/ui/platform_dispatcher.dart index cc0bdcdfb14b9..7ddc906bae1f6 100644 --- a/lib/ui/platform_dispatcher.dart +++ b/lib/ui/platform_dispatcher.dart @@ -28,9 +28,6 @@ typedef TimingsCallback = void Function(List timings); /// Signature for [PlatformDispatcher.onPointerDataPacket]. typedef PointerDataPacketCallback = void Function(PointerDataPacket packet); -// Signature for the response to KeyDataCallback. -typedef _KeyDataResponseCallback = void Function(int responseId, bool handled); - /// Signature for [PlatformDispatcher.onKeyData]. /// /// The callback should return true if the key event has been handled by the @@ -59,6 +56,11 @@ typedef PlatformConfigurationChangedCallback = void Function(PlatformConfigurati // A gesture setting value that indicates it has not been set by the engine. const double _kUnsetGestureSetting = -1.0; +// A message channel to receive KeyData from the platform. +// +// See embedder.cc::kFlutterKeyDataChannel for more information. +const String _kFlutterKeyDataChannel = 'flutter/keydata'; + /// Platform event dispatcher singleton. /// /// The most basic interface to the host operating system's interface. @@ -349,9 +351,19 @@ class PlatformDispatcher { return PointerDataPacket(data: data); } - /// Called by [_dispatchKeyData]. - void _respondToKeyData(int responseId, bool handled) - native 'PlatformConfiguration_respondToKeyData'; + static ChannelCallback _keyDataListener(KeyDataCallback onKeyData, Zone zone) => + (ByteData? packet, PlatformMessageResponseCallback callback) { + _invoke1( + (KeyData keyData) { + final bool handled = onKeyData(keyData); + final Uint8List response = Uint8List(1); + response[0] = handled ? 1 : 0; + callback(response.buffer.asByteData()); + }, + zone, + _unpackKeyData(packet!), + ); + }; /// A callback that is invoked when key data is available. /// @@ -362,22 +374,13 @@ class PlatformDispatcher { /// framework and should not be propagated further. KeyDataCallback? get onKeyData => _onKeyData; KeyDataCallback? _onKeyData; - Zone _onKeyDataZone = Zone.root; set onKeyData(KeyDataCallback? callback) { _onKeyData = callback; - _onKeyDataZone = Zone.current; - } - - // Called from the engine, via hooks.dart - void _dispatchKeyData(ByteData packet, int responseId) { - _invoke2( - (KeyData data, _KeyDataResponseCallback callback) { - callback(responseId, onKeyData != null && onKeyData!(data)); - }, - _onKeyDataZone, - _unpackKeyData(packet), - _respondToKeyData, - ); + if (callback != null) { + channelBuffers.setListener(_kFlutterKeyDataChannel, _keyDataListener(callback, Zone.current)); + } else { + channelBuffers.clearListener(_kFlutterKeyDataChannel); + } } // If this value changes, update the encoding code in the following files: diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 08c539c4a2f27..57615f7b57360 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -278,7 +278,7 @@ class SemanticsAction { case _kSetText: return 'SemanticsAction.setText'; } - assert(false, 'Unhandled index: $index'); + assert(false, 'Unhandled index: $index (0x${index.toRadixString(8).padLeft(4, "0")})'); return ''; } } @@ -289,6 +289,13 @@ class SemanticsAction { // `lib/ui/semantics/semantics_node.h` and in each of the embedders *must* be // updated. class SemanticsFlag { + const SemanticsFlag._(this.index) : assert(index != null); + + /// The numerical value for this flag. + /// + /// Each flag has one bit set in this bit field. + final int index; + static const int _kHasCheckedStateIndex = 1 << 0; static const int _kIsCheckedIndex = 1 << 1; static const int _kIsSelectedIndex = 1 << 2; @@ -300,7 +307,7 @@ class SemanticsFlag { static const int _kIsInMutuallyExclusiveGroupIndex = 1 << 8; static const int _kIsHeaderIndex = 1 << 9; static const int _kIsObscuredIndex = 1 << 10; - static const int _kScopesRouteIndex= 1 << 11; + static const int _kScopesRouteIndex = 1 << 11; static const int _kNamesRouteIndex = 1 << 12; static const int _kIsHiddenIndex = 1 << 13; static const int _kIsImageIndex = 1 << 14; @@ -320,13 +327,6 @@ class SemanticsFlag { // flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java, // and the SemanticsFlag class in lib/web_ui/lib/src/ui/semantics.dart. - const SemanticsFlag._(this.index) : assert(index != null); - - /// The numerical value for this flag. - /// - /// Each flag has one bit set in this bit field. - final int index; - /// The semantics node has the quality of either being "checked" or "unchecked". /// /// This flag is mutually exclusive with [hasToggledState]. @@ -580,7 +580,7 @@ class SemanticsFlag { _kIsLinkIndex: isLink, _kIsSliderIndex: isSlider, _kIsKeyboardKeyIndex: isKeyboardKey, -}; + }; @override String toString() { @@ -636,7 +636,7 @@ class SemanticsFlag { case _kIsKeyboardKeyIndex: return 'SemanticsFlag.isKeyboardKey'; } - assert(false, 'Unhandled index: $index'); + assert(false, 'Unhandled index: $index (0x${index.toRadixString(8).padLeft(4, "0")})'); return ''; } } @@ -646,6 +646,8 @@ class SemanticsFlag { // * engine/src/flutter/lib/web_ui/lib/src/ui/semantics.dart // * engine/src/flutter/lib/ui/semantics/string_attribute.h // * engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java +// * engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_api_test.dart +// * engine/src/flutter/testing/dart/semantics_test.dart /// An abstract interface for string attributes that affects how assistive /// technologies, e.g. VoiceOver or TalkBack, treat the text. diff --git a/lib/ui/semantics/string_attribute.h b/lib/ui/semantics/string_attribute.h index 5fbcf84c0e5fb..4af4fd3118c12 100644 --- a/lib/ui/semantics/string_attribute.h +++ b/lib/ui/semantics/string_attribute.h @@ -20,6 +20,8 @@ using StringAttributes = std::vector; // * engine/src/flutter/lib/ui/semantics.dart // * engine/src/flutter/lib/web_ui/lib/src/ui/semantics.dart // * engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java +// * engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_api_test.dart +// * engine/src/flutter/testing/dart/semantics_test.dart enum class StringAttributeType : int32_t { kSpellOut, diff --git a/lib/ui/text.dart b/lib/ui/text.dart index 4dc0de493b8cb..7d3b406955d41 100644 --- a/lib/ui/text.dart +++ b/lib/ui/text.dart @@ -1157,7 +1157,7 @@ class FontFeature { /// /// {@tool sample --template=stateless_widget} /// - /// The Piazzolla font supports the `ssXX` feature for for more + /// The Piazzolla font supports the `ssXX` feature for more /// elaborate stylistic effects. Set 1 turns some Latin characters /// into Roman numerals, set 2 enables some ASCII characters to be /// used to create pretty arrows, and so forth. @@ -3479,7 +3479,7 @@ class ParagraphBuilder extends NativeFieldWrapperClass1 { /// [PlaceholderAlignment.aboveBaseline], and [PlaceholderAlignment.belowBaseline] /// alignment modes are used, the baseline needs to be set with the `baseline`. /// When using [PlaceholderAlignment.baseline], `baselineOffset` indicates the distance - /// of the baseline down from the top of of the rectangle. The default `baselineOffset` + /// of the baseline down from the top of the rectangle. The default `baselineOffset` /// is the `height`. /// /// Examples: diff --git a/lib/ui/text/asset_manager_font_provider.h b/lib/ui/text/asset_manager_font_provider.h index ef4d272664203..5ffbf897cdbfd 100644 --- a/lib/ui/text/asset_manager_font_provider.h +++ b/lib/ui/text/asset_manager_font_provider.h @@ -44,7 +44,7 @@ class AssetManagerFontStyleSet : public SkFontStyleSet { std::string family_name_; struct TypefaceAsset { - TypefaceAsset(std::string a); + explicit TypefaceAsset(std::string a); TypefaceAsset(const TypefaceAsset& other); @@ -60,7 +60,8 @@ class AssetManagerFontStyleSet : public SkFontStyleSet { class AssetManagerFontProvider : public txt::FontAssetProvider { public: - AssetManagerFontProvider(std::shared_ptr asset_manager); + explicit AssetManagerFontProvider( + std::shared_ptr asset_manager); ~AssetManagerFontProvider() override; diff --git a/lib/ui/text/paragraph_builder.cc b/lib/ui/text/paragraph_builder.cc index 2ef379a2fe8c7..793af6601c699 100644 --- a/lib/ui/text/paragraph_builder.cc +++ b/lib/ui/text/paragraph_builder.cc @@ -452,17 +452,19 @@ void ParagraphBuilder::pushStyle(tonic::Int32List& encoded, if (mask & tsBackgroundMask) { Paint background(background_objects, background_data); - if (background.paint()) { + if (background.isNotNull()) { + SkPaint sk_paint; style.has_background = true; - style.background = *background.paint(); + style.background = *background.paint(sk_paint); } } if (mask & tsForegroundMask) { Paint foreground(foreground_objects, foreground_data); - if (foreground.paint()) { + if (foreground.isNotNull()) { + SkPaint sk_paint; style.has_foreground = true; - style.foreground = *foreground.paint(); + style.foreground = *foreground.paint(sk_paint); } } diff --git a/lib/ui/ui_dart_state.h b/lib/ui/ui_dart_state.h index f79d584ad0090..df0460b914cb1 100644 --- a/lib/ui/ui_dart_state.h +++ b/lib/ui/ui_dart_state.h @@ -41,7 +41,7 @@ class UIDartState : public tonic::DartState { /// UIDartState, a pointer to the resource can be added to this /// struct with appropriate default construction. struct Context { - Context(const TaskRunners& task_runners); + explicit Context(const TaskRunners& task_runners); Context(const TaskRunners& task_runners, fml::WeakPtr snapshot_delegate, diff --git a/lib/ui/window/platform_configuration.cc b/lib/ui/window/platform_configuration.cc index 47d6331e4064c..3cebfb5d10e24 100644 --- a/lib/ui/window/platform_configuration.cc +++ b/lib/ui/window/platform_configuration.cc @@ -181,15 +181,6 @@ void GetPersistentIsolateData(Dart_NativeArguments args) { persistent_isolate_data->GetSize())); } -void RespondToKeyData(Dart_Handle window, int response_id, bool handled) { - UIDartState::Current()->platform_configuration()->CompleteKeyDataResponse( - response_id, handled); -} - -void _RespondToKeyData(Dart_NativeArguments args) { - tonic::DartCallStatic(&RespondToKeyData, args); -} - Dart_Handle ToByteData(const fml::Mapping& buffer) { return tonic::DartByteData::Create(buffer.GetMapping(), buffer.GetSize()); } @@ -360,13 +351,6 @@ void PlatformConfiguration::DispatchSemanticsAction(int32_t id, args_handle})); } -uint64_t PlatformConfiguration::RegisterKeyDataResponse( - KeyDataResponse callback) { - uint64_t response_id = next_key_response_id_++; - pending_key_responses_[response_id] = std::move(callback); - return response_id; -} - void PlatformConfiguration::BeginFrame(fml::TimePoint frameTime, uint64_t frame_number) { std::shared_ptr dart_state = @@ -444,27 +428,6 @@ void PlatformConfiguration::CompletePlatformMessageResponse( response->Complete(std::make_unique(std::move(data))); } -void PlatformConfiguration::CompleteKeyDataResponse(uint64_t response_id, - bool handled) { - if (response_id == 0) { - return; - } - auto it = pending_key_responses_.find(response_id); - FML_DCHECK(it != pending_key_responses_.end()); - if (it == pending_key_responses_.end()) { - return; - } - KeyDataResponse callback = std::move(it->second); - pending_key_responses_.erase(it); - // The key result callback must be called on the platform thread. This is - // mainly because iOS needs to block the platform message loop with a nested - // loop to respond to key events synchronously, and if the callback is called - // from another thread, it won't stop the nested message loop, causing a - // deadlock. - UIDartState::Current()->GetTaskRunners().GetPlatformTaskRunner()->PostTask( - [handled, callback]() { callback(handled); }); -} - Dart_Handle ComputePlatformResolvedLocale(Dart_Handle supportedLocalesHandle) { std::vector supportedLocales = tonic::DartConverter>::FromDart( @@ -495,7 +458,6 @@ void PlatformConfiguration::RegisterNatives( true}, {"PlatformConfiguration_respondToPlatformMessage", _RespondToPlatformMessage, 3, true}, - {"PlatformConfiguration_respondToKeyData", _RespondToKeyData, 3, true}, {"PlatformConfiguration_render", Render, 3, true}, {"PlatformConfiguration_updateSemantics", UpdateSemantics, 2, true}, {"PlatformConfiguration_setIsolateDebugName", SetIsolateDebugName, 2, diff --git a/lib/ui/window/platform_configuration.h b/lib/ui/window/platform_configuration.h index 8bba4bd675e4c..d42f8639a9bcd 100644 --- a/lib/ui/window/platform_configuration.h +++ b/lib/ui/window/platform_configuration.h @@ -23,8 +23,6 @@ class FontCollection; class PlatformMessage; class Scene; -typedef std::function KeyDataResponse; - //-------------------------------------------------------------------------- /// @brief An enum for defining the different kinds of accessibility features /// that can be enabled by the platform. @@ -327,24 +325,6 @@ class PlatformConfiguration final { SemanticsAction action, fml::MallocMapping args); - //---------------------------------------------------------------------------- - /// @brief Registers a callback to be invoked when the framework has - /// decided whether to handle an event. This callback originates - /// in the platform view and has been forwarded through the engine - /// to here. - /// - /// This method will move and store the `callback`, associate it - /// with a self-incrementing identifier, the response ID, then - /// return the ID, which is typically used by - /// Window::DispatchKeyDataPacket. - /// - /// @param[in] callback The callback to be registered. - /// - /// @return The response ID to be associated with the callback. Using this - /// ID in CompleteKeyDataResponse will invoke the callback. - /// - uint64_t RegisterKeyDataResponse(KeyDataResponse callback); - //---------------------------------------------------------------------------- /// @brief Notifies the framework that it is time to begin working on a /// new frame previously scheduled via a call to @@ -439,21 +419,6 @@ class PlatformConfiguration final { /// void CompletePlatformMessageEmptyResponse(int response_id); - //---------------------------------------------------------------------------- - /// @brief Responds to a previously registered key data message from the - /// framework to the engine. - /// - /// For each response_id, this method should be called exactly - /// once. Responding to a response_id that has not been registered - /// or has been invoked will lead to a fatal error. - /// - /// @param[in] response_id The unique id that identifies the original platform - /// message to respond to, created by - /// RegisterKeyDataResponse. - /// @param[in] handled Whether the key data is handled. - /// - void CompleteKeyDataResponse(uint64_t response_id, bool handled); - private: PlatformConfigurationClient* client_; tonic::DartPersistentValue update_locales_; @@ -462,7 +427,6 @@ class PlatformConfiguration final { tonic::DartPersistentValue update_semantics_enabled_; tonic::DartPersistentValue update_accessibility_features_; tonic::DartPersistentValue dispatch_platform_message_; - tonic::DartPersistentValue dispatch_key_message_; tonic::DartPersistentValue dispatch_semantics_action_; tonic::DartPersistentValue begin_frame_; tonic::DartPersistentValue draw_frame_; @@ -474,10 +438,6 @@ class PlatformConfiguration final { int next_response_id_ = 1; std::unordered_map> pending_responses_; - - // ID starts at 1 because an ID of 0 indicates that no response is expected. - uint64_t next_key_response_id_ = 1; - std::unordered_map pending_key_responses_; }; } // namespace flutter diff --git a/lib/ui/window/window.cc b/lib/ui/window/window.cc index 66f5e2e7dbef0..f1ffe3ea0bdc2 100644 --- a/lib/ui/window/window.cc +++ b/lib/ui/window/window.cc @@ -36,24 +36,6 @@ void Window::DispatchPointerDataPacket(const PointerDataPacket& packet) { library_.value(), "_dispatchPointerDataPacket", {data_handle})); } -void Window::DispatchKeyDataPacket(const KeyDataPacket& packet, - uint64_t response_id) { - std::shared_ptr dart_state = library_.dart_state().lock(); - if (!dart_state) - return; - tonic::DartState::Scope scope(dart_state); - - const std::vector& buffer = packet.data(); - Dart_Handle data_handle = - tonic::DartByteData::Create(buffer.data(), buffer.size()); - if (Dart_IsError(data_handle)) { - return; - } - tonic::LogIfError( - tonic::DartInvokeField(library_.value(), "_dispatchKeyData", - {data_handle, tonic::ToDart(response_id)})); -} - void Window::UpdateWindowMetrics(const ViewportMetrics& metrics) { viewport_metrics_ = metrics; diff --git a/lib/ui/window/window.h b/lib/ui/window/window.h index 9a6847d3c46a6..aad79f9963f72 100644 --- a/lib/ui/window/window.h +++ b/lib/ui/window/window.h @@ -31,14 +31,6 @@ class Window final { // Dispatch a packet to the framework that indicates one or a few pointer // events. void DispatchPointerDataPacket(const PointerDataPacket& packet); - // Dispatch a packet to the framework that indicates a key event. - // - // The `response_id` is used to label the response of whether the key event - // is handled by the framework, typically the return value of - // PlatformConfiguration::RegisterKeyDataResponse. - // It should be used later in - // PlatformConfiguration::CompleteKeyDataResponse. - void DispatchKeyDataPacket(const KeyDataPacket& packet, uint64_t response_id); void UpdateWindowMetrics(const ViewportMetrics& metrics); private: diff --git a/lib/web_ui/dev/README.md b/lib/web_ui/dev/README.md index 940ef99917214..e2bab8eef51c6 100644 --- a/lib/web_ui/dev/README.md +++ b/lib/web_ui/dev/README.md @@ -86,7 +86,7 @@ To run tests on Firefox (this will work only on a Linux device): felt test --browser=firefox ``` -For Chrome and Firefox, the tests run on a version locked on the [browser_lock.yaml](https://github.com/flutter/engine/blob/master/lib/web_ui/dev/browser_lock.yaml). In order to use another version, add the version argument: +For Chrome and Firefox, the tests run on a version locked on the [browser_lock.yaml](https://github.com/flutter/engine/blob/main/lib/web_ui/dev/browser_lock.yaml). In order to use another version, add the version argument: ``` felt test --browser=firefox --firefox-version=70.0.1 @@ -184,8 +184,8 @@ Some useful links: 1. For Chrome downloads [link](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html) 2. Browser and driver CIPD [packages](https://chrome-infra-packages.appspot.com/p/flutter_internal) (note: access is restricted for these packages) -3. LUCI web [recipe](https://flutter.googlesource.com/recipes/+/refs/heads/master/recipes/web_engine.py) -4. More general reading on CIPD packages [link](https://chromium.googlesource.com/chromium/src.git/+/master/docs/cipd.md#What-is-CIPD) +3. LUCI web [recipe](https://flutter.googlesource.com/recipes/+/refs/heads/main/recipes/web_engine.py) +4. More general reading on CIPD packages [link](https://chromium.googlesource.com/chromium/src.git/+/main/docs/cipd_and_3pp.md#What-is-CIPD) ## Troubleshooting diff --git a/lib/web_ui/dev/browser.dart b/lib/web_ui/dev/browser.dart index 0b8c79255c6d4..4c46d64bac0e3 100644 --- a/lib/web_ui/dev/browser.dart +++ b/lib/web_ui/dev/browser.dart @@ -26,12 +26,12 @@ abstract class BrowserEnvironment { /// Prepares the OS environment to run tests for this browser. /// - /// This may include things like staring web drivers, iOS Simulators, and/or - /// Android emulators. + /// This may include things like installing browsers, and starting web drivers, + /// iOS Simulators, and/or Android emulators. /// /// Typically the browser environment is prepared once and supports multiple /// browser instances. - Future prepareEnvironment(); + Future prepare(); /// Launches a browser instance. /// diff --git a/lib/web_ui/dev/canvaskit_lock.yaml b/lib/web_ui/dev/canvaskit_lock.yaml index 79d8bdbc59851..f66b919c09cfc 100644 --- a/lib/web_ui/dev/canvaskit_lock.yaml +++ b/lib/web_ui/dev/canvaskit_lock.yaml @@ -1,4 +1,4 @@ # Specifies the version of CanvasKit to use for Flutter Web apps. # # See `lib/web_ui/README.md` for how to update this file. -canvaskit_version: "0.30.0" +canvaskit_version: "0.31.0" diff --git a/lib/web_ui/dev/canvaskit_roller.dart b/lib/web_ui/dev/canvaskit_roller.dart index bb6c7e3299eb0..3aa804d207cdc 100644 --- a/lib/web_ui/dev/canvaskit_roller.dart +++ b/lib/web_ui/dev/canvaskit_roller.dart @@ -132,11 +132,11 @@ Future _updateDepsFile(String cipdInstanceId) async { } Future _updateCanvaskitInitializationCode(String canvaskitVersion) async { - const String kCanvasKitVersionKey = 'const String canvaskitVersion'; - const String kPathToInitializationCode = 'lib/src/engine/canvaskit/initialization.dart'; + const String kCanvasKitVersionKey = 'const String _canvaskitVersion'; + const String kPathToConfigurationCode = 'lib/src/engine/configuration.dart'; final File initializationFile = File(pathlib.join( environment.webUiRootDir.path, - kPathToInitializationCode, + kPathToConfigurationCode, )); final String originalInitializationCode = await initializationFile.readAsString(); @@ -146,7 +146,7 @@ Future _updateCanvaskitInitializationCode(String canvaskitVersion) async { if (line.trim().startsWith(kCanvasKitVersionKey)) { canvaskitVersionFound = true; rewrittenCode.add( - "const String canvaskitVersion = '$canvaskitVersion';", + "const String _canvaskitVersion = '$canvaskitVersion';", ); } else { rewrittenCode.add(line); @@ -155,11 +155,11 @@ Future _updateCanvaskitInitializationCode(String canvaskitVersion) async { if (!canvaskitVersionFound) { stderr.writeln( - 'Failed to update CanvasKit version in $kPathToInitializationCode.\n' + 'Failed to update CanvasKit version in $kPathToConfigurationCode.\n' 'Could not to locate the constant that defines the version. Make sure the ' - '$kPathToInitializationCode file contains a line like this:\n' + '$kPathToConfigurationCode file contains a line like this:\n' '\n' - 'const String canvaskitVersion = \'VERSION\';' + 'const String _canvaskitVersion = \'VERSION\';' ); exit(1); } diff --git a/lib/web_ui/dev/chrome.dart b/lib/web_ui/dev/chrome.dart index 152bdb5d29828..f793153cb8ecf 100644 --- a/lib/web_ui/dev/chrome.dart +++ b/lib/web_ui/dev/chrome.dart @@ -21,17 +21,23 @@ import 'environment.dart'; /// Provides an environment for desktop Chrome. class ChromeEnvironment implements BrowserEnvironment { + late final BrowserInstallation _installation; + @override Browser launchBrowserInstance(Uri url, {bool debug = false}) { - return Chrome(url, debug: debug); + return Chrome(url, _installation, debug: debug); } @override Runtime get packageTestRuntime => Runtime.chrome; @override - Future prepareEnvironment() async { - // Chrome doesn't need any special prep. + Future prepare() async { + final String version = browserLock.chromeLock.versionForCurrentPlatform; + _installation = await getOrInstallChrome( + version, + infoLog: isCirrus ? stdout : DevNull(), + ); } @override @@ -64,15 +70,9 @@ class Chrome extends Browser { /// Starts a new instance of Chrome open to the given [url], which may be a /// [Uri] or a [String]. - factory Chrome(Uri url, {bool debug = false}) { - final String version = browserLock.chromeLock.versionForCurrentPlatform; + factory Chrome(Uri url, BrowserInstallation installation, {bool debug = false}) { final Completer remoteDebuggerCompleter = Completer.sync(); return Chrome._(() async { - final BrowserInstallation installation = await getOrInstallChrome( - version, - infoLog: isCirrus ? stdout : DevNull(), - ); - // A good source of various Chrome CLI options: // https://peter.sh/experiments/chromium-command-line-switches/ // @@ -93,7 +93,14 @@ class Chrome extends Browser { '--headless', if (isChromeNoSandbox) '--no-sandbox', - '--window-size=$kMaxScreenshotWidth,$kMaxScreenshotHeight', // When headless, this is the actual size of the viewport + // When headless, this is the actual size of the viewport. + if (!debug) + '--window-size=$kMaxScreenshotWidth,$kMaxScreenshotHeight', + // When debugging, run in maximized mode so there's enough room for DevTools. + if (debug) + '--start-maximized', + if (debug) + '--auto-open-devtools-for-tabs', '--disable-extensions', '--disable-popup-blocking', // Indicates that the browser is in "browse without sign-in" (Guest session) mode. diff --git a/lib/web_ui/dev/chrome_installer.dart b/lib/web_ui/dev/chrome_installer.dart index 46fec58a99ea7..72d432e206253 100644 --- a/lib/web_ui/dev/chrome_installer.dart +++ b/lib/web_ui/dev/chrome_installer.dart @@ -11,7 +11,6 @@ import 'package:archive/archive_io.dart'; import 'package:http/http.dart'; import 'package:path/path.dart' as path; -import 'browser_lock.dart'; import 'common.dart'; import 'environment.dart'; import 'exceptions.dart'; @@ -140,25 +139,21 @@ class ChromeInstaller { } Future install() async { - if (versionDir.existsSync() && !isLuci) { - versionDir.deleteSync(recursive: true); - versionDir.createSync(recursive: true); - } else if (versionDir.existsSync() && isLuci) { - print('INFO: Chrome version directory in LUCI: ' - '${versionDir.path}'); - } else if (!versionDir.existsSync() && isLuci) { - // Chrome should have been deployed as a CIPD package on LUCI. - // Throw if it does not exists. - throw StateError('Failed to locate Chrome on LUCI on path:' - '${versionDir.path}'); - } else { - // If the directory does not exists and felt is not running on LUCI. - versionDir.createSync(recursive: true); + if (isLuci) { + throw StateError( + 'Rejecting attempt to install Chromium on LUCI. LUCI is expected to ' + 'provide Chromium as a CIPD dependency managed using .ci.yaml.', + ); } - print('INFO: Starting Chrome download.'); + if (versionDir.existsSync()) { + versionDir.deleteSync(recursive: true); + } + versionDir.createSync(recursive: true); final String url = PlatformBinding.instance.getChromeDownloadUrl(version); + print('Downloading Chrome from $url'); + final StreamedResponse download = await client.send(Request( 'GET', Uri.parse(url), @@ -256,24 +251,3 @@ Future fetchLatestChromeVersion() async { client.close(); } } - -/// Make sure LUCI bot has the pinned Chrome version and return the executable. -/// -/// We are using CIPD packages in LUCI. The pinned chrome version from the -/// `browser_lock.yaml` file will already be installed in the LUCI bot. -/// Verify if Chrome is installed and use it for the integration tests. -String preinstalledChromeExecutable() { - // Note that build number and major version is different for Chrome. - // For example for a build number `753189`, major version is 83. - final String buildNumber = browserLock.chromeLock.versionForCurrentPlatform; - final ChromeInstaller chromeInstaller = ChromeInstaller(version: buildNumber); - if (chromeInstaller.isInstalled) { - final String executable = chromeInstaller.getInstallation()!.executable; - print('INFO: Found chrome executable for LUCI: $executable'); - return executable; - } else { - throw StateError( - 'Failed to locate pinned Chrome build: $buildNumber on LUCI.', - ); - } -} diff --git a/lib/web_ui/dev/clean.dart b/lib/web_ui/dev/clean.dart index b31567c15cc12..5e47309733f45 100644 --- a/lib/web_ui/dev/clean.dart +++ b/lib/web_ui/dev/clean.dart @@ -38,20 +38,11 @@ class CleanCommand extends Command with ArgUtils { @override FutureOr run() async { - final io.Directory assetsDir = io.Directory(path.join( - environment.webUiRootDir.path, 'lib', 'assets' - )); - final Iterable fontFiles = assetsDir - .listSync() - .whereType() - .where((io.File file) => file.path.endsWith('.ttf')); - final List thingsToBeCleaned = [ environment.webUiDartToolDir, environment.webUiBuildDir, io.File(path.join(environment.webUiRootDir.path, '.packages')), io.File(path.join(environment.webUiRootDir.path, 'pubspec.lock')), - ...fontFiles, if (_alsoCleanNinja) environment.outDir, if(_alsoCleanFlutterRepo) diff --git a/lib/web_ui/dev/edge.dart b/lib/web_ui/dev/edge.dart index 0b810bccca922..bf3acc7159400 100644 --- a/lib/web_ui/dev/edge.dart +++ b/lib/web_ui/dev/edge.dart @@ -23,7 +23,7 @@ class EdgeEnvironment implements BrowserEnvironment { Runtime get packageTestRuntime => Runtime.internetExplorer; @override - Future prepareEnvironment() async { + Future prepare() async { // Edge doesn't need any special prep. } diff --git a/lib/web_ui/dev/environment.dart b/lib/web_ui/dev/environment.dart index ec3f42bd420bb..8bba0f53d674d 100644 --- a/lib/web_ui/dev/environment.dart +++ b/lib/web_ui/dev/environment.dart @@ -92,10 +92,6 @@ class Environment { /// The "pub" executable file. String get pubExecutable => pathlib.join(dartSdkDir.path, 'bin', 'pub'); - /// The "dart2js" executable file. - String get dart2jsExecutable => - pathlib.join(dartSdkDir.path, 'bin', 'dart2js'); - /// Path to where github.com/flutter/engine is checked out inside the engine workspace. io.Directory get flutterDirectory => io.Directory(pathlib.join(engineSrcDir.path, 'flutter')); @@ -155,10 +151,16 @@ class Environment { /// Path to the clone of the flutter/goldens repository. io.Directory get webUiGoldensRepositoryDirectory => io.Directory(pathlib.join( - webUiDartToolDir.path, + webUiBuildDir.path, 'goldens', )); + /// Path to the base directory to be used by Skia Gold. + io.Directory get webUiSkiaGoldDirectory => io.Directory(pathlib.join( + webUiDartToolDir.path, + 'skia_gold', + )); + /// Directory to add test results which would later be uploaded to a gcs /// bucket by LUCI. io.Directory get webUiTestResultsDirectory => io.Directory(pathlib.join( diff --git a/lib/web_ui/dev/felt b/lib/web_ui/dev/felt index b1c3c383550f4..1aef3b436490a 100755 --- a/lib/web_ui/dev/felt +++ b/lib/web_ui/dev/felt @@ -33,14 +33,13 @@ HOST_DEBUG_UNOPT_DIR="${ENGINE_SRC_DIR}/out/host_debug_unopt" DART_SDK_DIR="${ENGINE_SRC_DIR}/out/host_debug_unopt/dart-sdk" GN="${FLUTTER_DIR}/tools/gn" DART_TOOL_DIR="${WEB_UI_DIR}/.dart_tool" -PUB_PATH="$DART_SDK_DIR/bin/pub" -DART2JS_PATH="$DART_SDK_DIR/bin/dart2js" +DART_PATH="$DART_SDK_DIR/bin/dart" SNAPSHOT_PATH="${DART_TOOL_DIR}/felt.snapshot" STAMP_PATH="${DART_TOOL_DIR}/felt.snapshot.stamp" SCRIPT_PATH="${DEV_DIR}/felt.dart" REVISION="$(cd "$FLUTTER_DIR"; git rev-parse HEAD)" -if [ ! -f "${PUB_PATH}" -o ! -f "${DART2JS_PATH}" ] +if [ ! -f "${DART_PATH}" ] then echo "Compiling the Dart SDK." gclient sync @@ -49,11 +48,11 @@ then fi install_deps() { - echo "Running \`pub get\` in 'engine/src/flutter/lib/web_ui'" - (cd "$WEB_UI_DIR"; $PUB_PATH get) + echo "Running \`dart pub get\` in 'engine/src/flutter/lib/web_ui'" + (cd "$WEB_UI_DIR"; $DART_PATH pub get) - echo "Running \`pub get\` in 'engine/src/flutter/web_sdk/web_engine_tester'" - (cd "$FLUTTER_DIR/web_sdk/web_engine_tester"; $PUB_PATH get) + echo "Running \`dart pub get\` in 'engine/src/flutter/web_sdk/web_engine_tester'" + (cd "$FLUTTER_DIR/web_sdk/web_engine_tester"; $DART_PATH pub get) } KERNEL_NAME=`uname` diff --git a/lib/web_ui/dev/firefox.dart b/lib/web_ui/dev/firefox.dart index 61c02da271c88..c661378600163 100644 --- a/lib/web_ui/dev/firefox.dart +++ b/lib/web_ui/dev/firefox.dart @@ -18,17 +18,22 @@ import 'firefox_installer.dart'; /// Provides an environment for the desktop Firefox. class FirefoxEnvironment implements BrowserEnvironment { + late final BrowserInstallation _installation; + @override Browser launchBrowserInstance(Uri url, {bool debug = false}) { - return Firefox(url, debug: debug); + return Firefox(url, this, debug: debug); } @override Runtime get packageTestRuntime => Runtime.firefox; @override - Future prepareEnvironment() async { - // Firefox doesn't need any special prep. + Future prepare() async { + _installation = await getOrInstallFirefox( + browserLock.firefoxLock.version, + infoLog: isCirrus ? stdout : DevNull(), + ); } @override @@ -54,14 +59,10 @@ class Firefox extends Browser { /// Starts a new instance of Firefox open to the given [url], which may be a /// [Uri] or a [String]. - factory Firefox(Uri url, {bool debug = false}) { + factory Firefox(Uri url, FirefoxEnvironment firefoxEnvironment, {bool debug = false}) { + final BrowserInstallation installation = firefoxEnvironment._installation; final Completer remoteDebuggerCompleter = Completer.sync(); return Firefox._(() async { - final BrowserInstallation installation = await getOrInstallFirefox( - browserLock.firefoxLock.version, - infoLog: isCirrus ? stdout : DevNull(), - ); - // Using a profile on opening will prevent popups related to profiles. const String _profile = ''' user_pref("browser.shell.checkDefaultBrowser", false); @@ -79,9 +80,9 @@ user_pref("dom.max_script_run_time", 0); temporaryProfileDirectory.deleteSync(recursive: true); } temporaryProfileDirectory.createSync(recursive: true); - File(path.join(temporaryProfileDirectory.path, 'prefs.js')) .writeAsStringSync(_profile); + final bool isMac = Platform.isMacOS; final List args = [ url.toString(), diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index 216e25c043ba2..b2ba801a9d4c5 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: 9c36f57f1a673a7ab444f4f20df16601dde15335 +revision: 0dd2e82050422c05e9daf652fa4267fb7c01f260 diff --git a/lib/web_ui/dev/run.dart b/lib/web_ui/dev/run.dart index f45770ec22cf6..4592e95e24f0a 100644 --- a/lib/web_ui/dev/run.dart +++ b/lib/web_ui/dev/run.dart @@ -49,9 +49,10 @@ class RunCommand extends Command with ArgUtils { 'compile_tests': CompileTestsStep(), for (final String browserName in kAllBrowserNames) 'run_tests_$browserName': RunTestsStep( - browserEnvironment: getBrowserEnvironment(browserName), + browserName: browserName, isDebug: false, doUpdateScreenshotGoldens: false, + overridePathToCanvasKit: null, ), }; diff --git a/lib/web_ui/dev/safari_ios.dart b/lib/web_ui/dev/safari_ios.dart index cd68ec0fc7a9a..0dd41c08c8b9e 100644 --- a/lib/web_ui/dev/safari_ios.dart +++ b/lib/web_ui/dev/safari_ios.dart @@ -28,7 +28,7 @@ class SafariIosEnvironment implements BrowserEnvironment { Runtime get packageTestRuntime => Runtime.safari; @override - Future prepareEnvironment() async { + Future prepare() async { await initIosSimulator(); } diff --git a/lib/web_ui/dev/safari_macos.dart b/lib/web_ui/dev/safari_macos.dart index 5bfc70ef8c1bf..1cbbed65f2d22 100644 --- a/lib/web_ui/dev/safari_macos.dart +++ b/lib/web_ui/dev/safari_macos.dart @@ -23,7 +23,7 @@ class SafariMacOsEnvironment implements BrowserEnvironment { Runtime get packageTestRuntime => Runtime.safari; @override - Future prepareEnvironment() async { + Future prepare() async { // Nothing extra to prepare for desktop Safari. } diff --git a/lib/web_ui/dev/steps/compile_tests_step.dart b/lib/web_ui/dev/steps/compile_tests_step.dart index dcd688039d325..86088aa7253eb 100644 --- a/lib/web_ui/dev/steps/compile_tests_step.dart +++ b/lib/web_ui/dev/steps/compile_tests_step.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert' show JsonEncoder; import 'dart:io' as io; import 'package:path/path.dart' as pathlib; @@ -13,12 +14,16 @@ import '../exceptions.dart'; import '../pipeline.dart'; import '../utils.dart'; -/// Compiles web tests and their dependencies. +/// Compiles web tests and their dependencies into web_ui/build/. /// -/// Includes: -/// * compile the test code itself -/// * compile the page that hosts the tests -/// * fetch the golden repo for screenshot comparison +/// Outputs of this step: +/// +/// * canvaskit/ - CanvasKit artifacts +/// * assets/ - test fonts +/// * goldens/ - the goldens fetched from flutter/goldens +/// * host/ - compiled test host page and static artifacts +/// * test/ - compiled test code +/// * test_images/ - test images copied from Skis sources. class CompileTestsStep implements PipelineStep { CompileTestsStep({ this.skipGoldensRepoFetch = false, @@ -41,14 +46,118 @@ class CompileTestsStep implements PipelineStep { @override Future run() async { + await environment.webUiBuildDir.create(); if (!skipGoldensRepoFetch) { await fetchGoldensRepo(); } + await copyCanvasKitFiles(); await buildHostPage(); + await copyTestFonts(); + await copySkiaTestImages(); await compileTests(testFiles ?? findAllTests()); } } +const Map _kTestFonts = { + 'Ahem': 'ahem.ttf', + 'Roboto': 'Roboto-Regular.ttf', + 'Noto Naskh Arabic UI': 'NotoNaskhArabic-Regular.ttf', + 'Noto Color Emoji': 'NotoColorEmoji.ttf', +}; + +Future copyTestFonts() async { + final String fontsPath = pathlib.join( + environment.flutterDirectory.path, + 'third_party', + 'txt', + 'third_party', + 'fonts', + ); + + final List fontManifest = []; + for (final MapEntry fontEntry in _kTestFonts.entries) { + final String family = fontEntry.key; + final String fontFile = fontEntry.value; + + fontManifest.add({ + 'family': family, + 'fonts': [ + { + 'asset': 'fonts/$fontFile', + }, + ], + }); + + final io.File sourceTtf = io.File(pathlib.join(fontsPath, fontFile)); + final io.File destinationTtf = io.File(pathlib.join( + environment.webUiBuildDir.path, + 'assets', + 'fonts', + fontFile, + )); + await destinationTtf.create(recursive: true); + await sourceTtf.copy(destinationTtf.path); + } + + final io.File fontManifestFile = io.File(pathlib.join( + environment.webUiBuildDir.path, + 'assets', + 'FontManifest.json', + )); + await fontManifestFile.create(recursive: true); + await fontManifestFile.writeAsString( + const JsonEncoder.withIndent(' ').convert(fontManifest), + ); +} + +Future copySkiaTestImages() async { + final io.Directory testImagesDir = io.Directory(pathlib.join( + environment.engineSrcDir.path, + 'third_party', + 'skia', + 'resources', + 'images', + )); + + for (final io.File imageFile in testImagesDir.listSync(recursive: true).whereType()) { + final io.File destination = io.File(pathlib.join( + environment.webUiBuildDir.path, + 'test_images', + pathlib.relative(imageFile.path, from: testImagesDir.path), + )); + destination.createSync(recursive: true); + await imageFile.copy(destination.path); + } +} + +Future copyCanvasKitFiles() async { + final io.Directory canvasKitDir = io.Directory(pathlib.join( + environment.engineSrcDir.path, + 'third_party', + 'web_dependencies', + 'canvaskit', + )); + + final Iterable canvasKitFiles = canvasKitDir + .listSync(recursive: true, followLinks: true) + .whereType(); + + final io.Directory targetDir = io.Directory(pathlib.join( + environment.webUiBuildDir.path, + 'canvaskit', + )); + + for (final io.File file in canvasKitFiles) { + final String relativePath = pathlib.relative(file.path, from: canvasKitDir.path); + final io.File targetFile = io.File(pathlib.join( + targetDir.path, + relativePath, + )); + await targetFile.create(recursive: true); + await file.copy(targetFile.path); + } +} + /// Compiles the specified unit tests. Future compileTests(List testFiles) async { final Stopwatch stopwatch = Stopwatch()..start(); @@ -136,10 +245,11 @@ Future compileUnitTest(FilePath input, { required bool forCanvasKit }) asy } final List arguments = [ + 'compile', + 'js', '--no-minify', '--disable-inlining', '--enable-asserts', - '--enable-experiment=non-nullable', '--no-sound-null-safety', // We do not want to auto-select a renderer in tests. As of today, tests @@ -155,7 +265,7 @@ Future compileUnitTest(FilePath input, { required bool forCanvasKit }) asy ]; final int exitCode = await runProcess( - environment.dart2jsExecutable, + environment.dartExecutable, arguments, workingDirectory: environment.webUiRootDir.path, ); @@ -175,9 +285,38 @@ Future buildHostPage() async { environment.webEngineTesterRootDir.path, hostDartPath, )); + final String targetDirectoryPath = pathlib.join( + environment.webUiBuildDir.path, + 'host', + ); + io.Directory(targetDirectoryPath).createSync(recursive: true); + final String targetFilePath = pathlib.join( + targetDirectoryPath, + 'host.dart', + ); + + const List staticFiles = [ + 'favicon.ico', + 'host.css', + 'index.html', + ]; + for (final String staticFilePath in staticFiles) { + final io.File source = io.File(pathlib.join( + environment.webEngineTesterRootDir.path, + 'lib', + 'static', + staticFilePath, + )); + final io.File destination = io.File(pathlib.join( + targetDirectoryPath, + staticFilePath, + )); + await source.copy(destination.path); + } + final io.File timestampFile = io.File(pathlib.join( environment.webEngineTesterRootDir.path, - '$hostDartPath.js.timestamp', + '$targetFilePath.js.timestamp', )); final String timestamp = @@ -196,11 +335,13 @@ Future buildHostPage() async { } final int exitCode = await runProcess( - environment.dart2jsExecutable, + environment.dartExecutable, [ + 'compile', + 'js', hostDartPath, '-o', - '$hostDartPath.js', + '$targetFilePath.js', ], workingDirectory: environment.webEngineTesterRootDir.path, ); diff --git a/lib/web_ui/dev/steps/run_tests_step.dart b/lib/web_ui/dev/steps/run_tests_step.dart index ce47068050985..185ae8e811d2b 100644 --- a/lib/web_ui/dev/steps/run_tests_step.dart +++ b/lib/web_ui/dev/steps/run_tests_step.dart @@ -15,8 +15,10 @@ import 'package:test_core/src/runner/configuration/reporters.dart' as hack; import 'package:test_core/src/runner/engine.dart' as hack; import 'package:test_core/src/runner/hack_register_platform.dart' as hack; import 'package:test_core/src/runner/reporter.dart' as hack; +import 'package:web_test_utils/skia_client.dart'; import '../browser.dart'; +import '../common.dart'; import '../environment.dart'; import '../exceptions.dart'; import '../pipeline.dart'; @@ -33,16 +35,20 @@ const int _testConcurrency = int.fromEnvironment('FELT_TEST_CONCURRENCY', defaul /// them from another bot. class RunTestsStep implements PipelineStep { RunTestsStep({ - required this.browserEnvironment, + required this.browserName, required this.isDebug, required this.doUpdateScreenshotGoldens, this.testFiles, - }); + required this.overridePathToCanvasKit, + }) : _browserEnvironment = getBrowserEnvironment(browserName); - final BrowserEnvironment browserEnvironment; + final String browserName; final List? testFiles; final bool isDebug; final bool doUpdateScreenshotGoldens; + final String? overridePathToCanvasKit; + + final BrowserEnvironment _browserEnvironment; /// Global list of shards that failed. /// @@ -66,9 +72,10 @@ class RunTestsStep implements PipelineStep { @override Future run() async { - _copyTestFontsIntoWebUi(); await _prepareTestResultsDirectory(); - await browserEnvironment.prepareEnvironment(); + await _browserEnvironment.prepare(); + + final SkiaGoldClient? skiaClient = await _createSkiaClient(); final List testFiles = this.testFiles ?? findAllTests(); @@ -109,11 +116,13 @@ class RunTestsStep implements PipelineStep { if (testFiles.contains(failureSmokeTestPath)) { await _runTestBatch( testFiles: [failureSmokeTestPath], - browserEnvironment: browserEnvironment, + browserEnvironment: _browserEnvironment, concurrency: 1, expectFailure: true, isDebug: isDebug, doUpdateScreenshotGoldens: doUpdateScreenshotGoldens, + skiaClient: skiaClient, + overridePathToCanvasKit: overridePathToCanvasKit, ); } @@ -121,11 +130,13 @@ class RunTestsStep implements PipelineStep { if (unitTestFiles.isNotEmpty) { await _runTestBatch( testFiles: unitTestFiles, - browserEnvironment: browserEnvironment, + browserEnvironment: _browserEnvironment, concurrency: _testConcurrency, expectFailure: false, isDebug: isDebug, doUpdateScreenshotGoldens: doUpdateScreenshotGoldens, + skiaClient: skiaClient, + overridePathToCanvasKit: overridePathToCanvasKit, ); _checkExitCode('Unit tests'); } @@ -135,11 +146,13 @@ class RunTestsStep implements PipelineStep { if (screenshotTestFiles.isNotEmpty) { await _runTestBatch( testFiles: screenshotTestFiles, - browserEnvironment: browserEnvironment, + browserEnvironment: _browserEnvironment, concurrency: 1, expectFailure: false, isDebug: isDebug, doUpdateScreenshotGoldens: doUpdateScreenshotGoldens, + skiaClient: skiaClient, + overridePathToCanvasKit: overridePathToCanvasKit, ); _checkExitCode('Golden tests'); } @@ -164,6 +177,47 @@ class RunTestsStep implements PipelineStep { } return message.toString(); } + + Future _createSkiaClient() async { + final SkiaGoldClient skiaClient = SkiaGoldClient( + environment.webUiSkiaGoldDirectory, + browserName: browserName, + ); + + if (!await _checkSkiaClient(skiaClient)) { + print('WARNING: Unable to use Skia Client in this environment.'); + return null; + } + + return skiaClient; + } + + /// Checks whether the Skia Client is usable in this environment. + Future _checkSkiaClient(SkiaGoldClient skiaClient) async { + // Now let's check whether Skia Gold is reachable or not. + if (isLuci) { + if (SkiaGoldClient.isAvailable) { + try { + await skiaClient.auth(); + return true; + } catch (e) { + print(e); + } + } + } else { + try { + // Check if we can reach Gold. + await skiaClient.getExpectationForTest(''); + return true; + } on io.OSError catch (_) { + print('OSError occurred, could not reach Gold.'); + } on io.SocketException catch (_) { + print('SocketException occurred, could not reach Gold.'); + } + } + + return false; + } } Future _prepareTestResultsDirectory() async { @@ -173,30 +227,6 @@ Future _prepareTestResultsDirectory() async { environment.webUiTestResultsDirectory.createSync(recursive: true); } -const List _kTestFonts = [ - 'ahem.ttf', - 'Roboto-Regular.ttf', - 'NotoNaskhArabic-Regular.ttf', - 'NotoColorEmoji.ttf', -]; - -void _copyTestFontsIntoWebUi() { - final String fontsPath = pathlib.join( - environment.flutterDirectory.path, - 'third_party', - 'txt', - 'third_party', - 'fonts', - ); - - for (final String fontFile in _kTestFonts) { - final io.File sourceTtf = io.File(pathlib.join(fontsPath, fontFile)); - final String destinationTtfPath = - pathlib.join(environment.webUiRootDir.path, 'lib', 'assets', fontFile); - sourceTtf.copySync(destinationTtfPath); - } -} - /// Runs a batch of tests. /// /// Unless [expectFailure] is set to false, sets [io.exitCode] to a non-zero @@ -208,6 +238,8 @@ Future _runTestBatch({ required bool doUpdateScreenshotGoldens, required int concurrency, required bool expectFailure, + required SkiaGoldClient? skiaClient, + required String? overridePathToCanvasKit, }) async { final String configurationFilePath = pathlib.join( environment.webUiRootDir.path, @@ -244,6 +276,8 @@ Future _runTestBatch({ // It doesn't make sense to update a screenshot for a test that is // expected to fail. doUpdateScreenshotGoldens: !expectFailure && doUpdateScreenshotGoldens, + skiaClient: skiaClient, + overridePathToCanvasKit: overridePathToCanvasKit, ); }); diff --git a/lib/web_ui/dev/test_platform.dart b/lib/web_ui/dev/test_platform.dart index b7e1c11fe6189..d9fd05ccd6efd 100644 --- a/lib/web_ui/dev/test_platform.dart +++ b/lib/web_ui/dev/test_platform.dart @@ -35,6 +35,7 @@ import 'package:test_core/src/util/stack_trace_mapper.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_test_utils/goldens.dart'; import 'package:web_test_utils/image_compare.dart'; +import 'package:web_test_utils/skia_client.dart'; import 'browser.dart'; import 'common.dart'; @@ -51,16 +52,18 @@ class BrowserPlatform extends PlatformPlugin { static Future start({ required BrowserEnvironment browserEnvironment, required bool doUpdateScreenshotGoldens, + required SkiaGoldClient? skiaClient, + required String? overridePathToCanvasKit, }) async { final shelf_io.IOServer server = shelf_io.IOServer(await HttpMultiServer.loopback(0)); return BrowserPlatform._( browserEnvironment: browserEnvironment, server: server, isDebug: Configuration.current.pauseAfterLoad, - faviconPath: p.fromUri(await Isolate.resolvePackageUri( - Uri.parse('package:test/src/runner/browser/static/favicon.ico'))), doUpdateScreenshotGoldens: doUpdateScreenshotGoldens, packageConfig: await loadPackageConfigUri((await Isolate.packageConfig)!), + skiaClient: skiaClient, + overridePathToCanvasKit: overridePathToCanvasKit, ); } @@ -100,13 +103,20 @@ class BrowserPlatform extends PlatformPlugin { final PackageConfig packageConfig; + /// A client for communicating with the Skia Gold backend to fetch, compare + /// and update images. + final SkiaGoldClient? skiaClient; + + final String? overridePathToCanvasKit; + BrowserPlatform._({ required this.browserEnvironment, required this.server, required this.isDebug, - required String faviconPath, required this.doUpdateScreenshotGoldens, required this.packageConfig, + required this.skiaClient, + required this.overridePathToCanvasKit, }) : _screenshotManager = browserEnvironment.getScreenshotManager() { // The cascade of request handlers. final shelf.Cascade cascade = shelf.Cascade() @@ -115,9 +125,6 @@ class BrowserPlatform extends PlatformPlugin { // for details on how the channels are established. .add(_webSocketHandler.handler) - // Serves /favicon.ico - .add(createFileHandler(faviconPath)) - // Serves /packages/* requests; fetches files and sources from // pubspec dependencies. // @@ -126,12 +133,12 @@ class BrowserPlatform extends PlatformPlugin { // * Assets that are part of the engine sources, such as Ahem.ttf .add(_packageUrlHandler) + .add(_canvasKitOverrideHandler) + // Serves files from the web_ui/build/ directory at the root (/) URL path. - // - // Includes: - // * Precompiles .js files for tests - // * Sourcemaps - .add(createStaticHandler(env.environment.webUiBuildDir.path)) + .add(buildDirectoryHandler) + + .add(_testImageListingHandler) // Serves the initial HTML for the test. .add(_testBootstrapHandler) @@ -149,11 +156,87 @@ class BrowserPlatform extends PlatformPlugin { // Serves absolute package URLs (i.e. not /packages/* but /Users/user/*/hosted/pub.dartlang.org/*). // This handler goes last, after all more specific handlers failed to handle the request. .add(_createAbsolutePackageUrlHandler()) - .add(_screeshotHandler); + .add(_screeshotHandler) + .add(_fileNotFoundCatcher); server.mount(cascade.handler); } + /// If a path to a custom local build of CanvasKit was specified, serve from + /// there instead of serving the default CanvasKit in the build/ directory. + Future _canvasKitOverrideHandler(shelf.Request request) async { + final String? pathOverride = overridePathToCanvasKit; + + if (pathOverride == null || !request.url.path.startsWith('canvaskit/')) { + return shelf.Response.notFound('Not a request for CanvasKit.'); + } + + final File file = File(p.joinAll([ + pathOverride, + ...p.split(request.url.path).skip(1), + ])); + + if (!file.existsSync()) { + return shelf.Response.notFound('File not found: ${request.url.path}'); + } + + final String extension = p.extension(file.path); + final String? contentType = contentTypes[extension]; + + if (contentType == null) { + final String error = 'Failed to determine Content-Type for "${request.url.path}".'; + stderr.writeln(error); + return shelf.Response.internalServerError(body: error); + } + + return shelf.Response.ok( + file.readAsBytesSync(), + headers: { + HttpHeaders.contentTypeHeader: contentType, + }, + ); + } + + /// Lists available test images under `web_ui/build/test_images`. + Future _testImageListingHandler(shelf.Request request) async { + const Map supportedImageTypes = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.bmp': 'image/bmp', + }; + + if (request.url.path != 'test_images/') { + return shelf.Response.notFound('Not found.'); + } + + final Directory testImageDirectory = Directory(p.join( + env.environment.webUiBuildDir.path, + 'test_images', + )); + + final List testImageFiles = testImageDirectory + .listSync(recursive: true) + .whereType() + .map((File file) => p.relative(file.path, from: testImageDirectory.path)) + .where((String path) => supportedImageTypes.containsKey(p.extension(path))) + .toList(); + + return shelf.Response.ok( + json.encode(testImageFiles), + headers: { + HttpHeaders.contentTypeHeader: 'application/json', + }, + ); + } + + Future _fileNotFoundCatcher(shelf.Request request) async { + print('HTTP 404: ${request.url}'); + return shelf.Response.notFound('File not found'); + } + /// Handles URLs pointing to Dart sources using absolute URI paths. /// /// Dart source paths that dart2js puts in source maps for pub packages are @@ -271,9 +354,6 @@ class BrowserPlatform extends PlatformPlugin { write = true; } - filename = - filename.replaceAll('.png', '${_screenshotManager!.filenameSuffix}.png'); - String goldensDirectory; if (filename.startsWith('__local__')) { filename = filename.substring('__local__/'.length); @@ -301,13 +381,67 @@ class BrowserPlatform extends PlatformPlugin { final Image screenshot = await _screenshotManager!.capture(regionAsRectange); return compareImage( - screenshot, - doUpdateScreenshotGoldens, - filename, - pixelComparison, - maxDiffRateFailure, - goldensDirectory: goldensDirectory, - write: write); + screenshot, + doUpdateScreenshotGoldens, + filename, + pixelComparison, + maxDiffRateFailure, + skiaClient, + goldensDirectory: goldensDirectory, + filenameSuffix: _screenshotManager!.filenameSuffix, + write: write, + ); + } + + static const Map contentTypes = { + '.js': 'text/javascript', + '.wasm': 'application/wasm', + '.html': 'text/html', + '.htm': 'text/html', + '.css': 'text/css', + '.ico': 'image/icon-x', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.bmp': 'image/bmp', + '.svg': 'image/svg+xml', + '.json': 'application/json', + '.ttf': 'font/ttf', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + }; + + /// A simple file handler that serves files whose URLs and paths are + /// statically known. + /// + /// This is used for trivial use-cases, such as `favicon.ico`, host pages, etc. + shelf.Response buildDirectoryHandler(shelf.Request request) { + final File fileInBuild = File(p.join( + env.environment.webUiBuildDir.path, + request.url.path, + )); + + if (!fileInBuild.existsSync()) { + return shelf.Response.notFound('File not found: ${request.url.path}'); + } + + final String extension = p.extension(fileInBuild.path); + final String? contentType = contentTypes[extension]; + + if (contentType == null) { + final String error = 'Failed to determine Content-Type for "${request.url.path}".'; + stderr.writeln(error); + return shelf.Response.internalServerError(body: error); + } + + return shelf.Response.ok( + fileInBuild.readAsBytesSync(), + headers: { + HttpHeaders.contentTypeHeader: contentType, + }, + ); } /// Serves the HTML file that bootstraps the test. @@ -326,6 +460,12 @@ class BrowserPlatform extends PlatformPlugin { ${htmlEscape.convert(test)} Test + + $link @@ -394,7 +534,7 @@ class BrowserPlatform extends PlatformPlugin { final String path = _webSocketHandler.create(webSocketHandler(completer.complete)); final Uri webSocketUrl = url.replace(scheme: 'ws').resolve(path); final Uri hostUrl = url - .resolve('packages/web_engine_tester/static/index.html') + .resolve('host/index.html') .replace(queryParameters: { 'managerUrl': webSocketUrl.toString(), 'debug': isDebug.toString() diff --git a/lib/web_ui/dev/test_runner.dart b/lib/web_ui/dev/test_runner.dart index 1022f4c2d0829..543cd80f4cb0b 100644 --- a/lib/web_ui/dev/test_runner.dart +++ b/lib/web_ui/dev/test_runner.dart @@ -10,8 +10,6 @@ import 'package:path/path.dart' as path; import 'package:watcher/src/watch_event.dart'; -import 'browser.dart'; -import 'common.dart'; import 'pipeline.dart'; import 'steps/compile_tests_step.dart'; import 'steps/run_tests_step.dart'; @@ -71,8 +69,8 @@ class TestCommand extends Command with ArgUtils { ..addOption( 'browser', defaultsTo: 'chrome', - help: 'An option to choose a browser to run the tests. Tests only work ' - ' on Chrome for now.', + help: 'An option to choose a browser to run the tests. By default ' + 'tests run in Chrome.', ) ..addFlag( 'fail-early', @@ -82,6 +80,12 @@ class TestCommand extends Command with ArgUtils { 'failure. If not set, the test runner will continue running ' 'test despite failures and will report them after all tests ' 'finish.', + ) + ..addOption( + 'canvaskit-path', + help: 'Optional. The path to a local build of CanvasKit to use in ' + 'tests. If omitted, the test runner uses the default CanvasKit ' + 'build.', ); } @@ -111,7 +115,7 @@ class TestCommand extends Command with ArgUtils { bool get runAllTests => targets.isEmpty; /// The name of the browser to run tests in. - String get browser => stringArg('browser'); + String get browserName => stringArg('browser'); /// When running screenshot tests writes them to the file system into /// ".dart_tool/goldens". @@ -120,10 +124,11 @@ class TestCommand extends Command with ArgUtils { /// Whether to fetch the goldens repo prior to running tests. bool get skipGoldensRepoFetch => boolArg('skip-goldens-repo-fetch'); + /// Path to a CanvasKit build. Overrides the default CanvasKit. + String? get overridePathToCanvasKit => argResults!['canvaskit-path'] as String?; + @override Future run() async { - final BrowserEnvironment browserEnvironment = getBrowserEnvironment(browser); - final List testFiles = runAllTests ? findAllTests() : targetFiles; @@ -135,10 +140,11 @@ class TestCommand extends Command with ArgUtils { testFiles: testFiles, ), RunTestsStep( - browserEnvironment: browserEnvironment, + browserName: browserName, testFiles: testFiles, isDebug: isDebug, doUpdateScreenshotGoldens: doUpdateScreenshotGoldens, + overridePathToCanvasKit: overridePathToCanvasKit, ), ]); await testPipeline.run(); diff --git a/lib/web_ui/dev/web_engine_analysis.sh b/lib/web_ui/dev/web_engine_analysis.sh index 76a387a739efa..fd1224d727a34 100755 --- a/lib/web_ui/dev/web_engine_analysis.sh +++ b/lib/web_ui/dev/web_engine_analysis.sh @@ -5,7 +5,7 @@ set -x # web_analysis: a command-line utility for running 'dart analyze' on Flutter Web # Engine. Used/Called by LUCI recipes: # -# See: https://flutter.googlesource.com/recipes/+/refs/heads/master/recipes/web_engine.py +# See: https://flutter.googlesource.com/recipes/+/refs/heads/main/recipes/web_engine.py echo "Engine path $ENGINE_PATH" WEB_UI_DIR="$ENGINE_PATH/src/flutter/lib/web_ui" diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 15c8d53889a9e..75fe3b0b65d6e 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -12,7 +12,7 @@ library engine; // 2. Exports of engine/* files are replaced with a part directive. // // The code that performs the transformations lives in: -// - https://github.com/flutter/engine/blob/master/web_sdk/sdk_rewriter.dart +// - https://github.com/flutter/engine/blob/main/web_sdk/sdk_rewriter.dart import 'dart:async'; // Some of these names are used in services/buffers.dart for example. @@ -37,6 +37,8 @@ import 'package:meta/meta.dart'; import '../ui.dart' as ui; +// ignore: unused_import +import 'engine/configuration.dart'; import 'engine/dom_renderer.dart'; import 'engine/keyboard.dart'; import 'engine/mouse_cursor.dart'; @@ -74,6 +76,10 @@ export 'engine/canvaskit/image.dart'; export 'engine/canvaskit/image_filter.dart'; +export 'engine/canvaskit/image_wasm_codecs.dart'; + +export 'engine/canvaskit/image_web_codecs.dart'; + export 'engine/canvaskit/initialization.dart'; export 'engine/canvaskit/interval_tree.dart'; @@ -120,6 +126,8 @@ export 'engine/clipboard.dart'; export 'engine/color_filter.dart'; +export 'engine/configuration.dart'; + export 'engine/dom_renderer.dart'; export 'engine/engine_canvas.dart'; diff --git a/lib/web_ui/lib/src/engine/assets.dart b/lib/web_ui/lib/src/engine/assets.dart index a1bbfcc5eb938..93913ca770286 100644 --- a/lib/web_ui/lib/src/engine/assets.dart +++ b/lib/web_ui/lib/src/engine/assets.dart @@ -83,7 +83,7 @@ class AssetManager { class AssetManagerException implements Exception { /// Http request url for asset. final String url; - /// Http status of of response. + /// Http status of response. final int httpStatus; /// Initializes exception with request url and http status. diff --git a/lib/web_ui/lib/src/engine/browser_detection.dart b/lib/web_ui/lib/src/engine/browser_detection.dart index b34bfad9d4314..3251da99cf331 100644 --- a/lib/web_ui/lib/src/engine/browser_detection.dart +++ b/lib/web_ui/lib/src/engine/browser_detection.dart @@ -227,6 +227,18 @@ bool get isMacOrIOS => operatingSystem == OperatingSystem.iOs || operatingSystem == OperatingSystem.macOs; +/// Detect iOS 15. +bool get isIOS15 { + if (debugIsIOS15 != null) { + return debugIsIOS15!; + } + return operatingSystem == OperatingSystem.iOs && + html.window.navigator.userAgent.contains('OS 15_'); +} + +/// Use in tests to simulate the detection of iOS 15. +bool? debugIsIOS15; + int? _cachedWebGLVersion; /// The highest WebGL version supported by the current browser, or -1 if WebGL diff --git a/lib/web_ui/lib/src/engine/canvas_pool.dart b/lib/web_ui/lib/src/engine/canvas_pool.dart index be03c284e9bc2..d59078a181cf0 100644 --- a/lib/web_ui/lib/src/engine/canvas_pool.dart +++ b/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -207,6 +207,7 @@ class CanvasPool extends _SaveStackTracking { html.CanvasElement? _allocCanvas(int width, int height) { final dynamic canvas = + // ignore: implicit_dynamic_function js_util.callMethod(html.document, 'createElement', ['CANVAS']); if (canvas != null) { try { diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index 7b8a93eb7f115..e228e81ff98ed 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -104,6 +104,7 @@ class CanvasKit { external SkFontMgrNamespace get FontMgr; external TypefaceFontProviderNamespace get TypefaceFontProvider; + external SkTypefaceFactory get Typeface; external int GetWebGLContext( html.CanvasElement canvas, SkWebGLContextOptions options); external SkGrContext MakeGrContext(int glContext); @@ -122,11 +123,15 @@ class CanvasKit { /// Typically pixel data is obtained using [SkImage.readPixels]. The /// parameters specified in [SkImageInfo] passed [SkImage.readPixels] must /// match [info]. - external SkImage MakeImage( + external SkImage? MakeImage( SkImageInfo info, Uint8List pixels, int bytesPerRow, ); + external SkImage? MakeLazyImageFromTextureSource( + Object src, + SkPartialImageInfo info, + ); } @JS('window.CanvasKitInit') @@ -1869,7 +1874,6 @@ class SkTonalColors { class SkFontMgrNamespace { // TODO(yjbanov): can this be made non-null? It returns null in our unit-tests right now. external SkFontMgr? FromData(List fonts); - external SkFontMgr RefDefault(); } @JS() @@ -1877,6 +1881,12 @@ class TypefaceFontProviderNamespace { external TypefaceFontProvider Make(); } +@JS() +@anonymous +class SkTypefaceFactory { + external SkTypeface? MakeFreeTypeFaceFromData(ByteBuffer fontData); +} + /// Collects Skia objects that are no longer necessary. abstract class Collector { /// The production collector implementation. @@ -2118,9 +2128,9 @@ class SkImageInfo { external factory SkImageInfo({ required int width, required int height, - SkAlphaType alphaType, - ColorSpace colorSpace, - SkColorType colorType, + required SkColorType colorType, + required SkAlphaType alphaType, + required ColorSpace colorSpace, }); external SkAlphaType get alphaType; external ColorSpace get colorSpace; @@ -2135,3 +2145,20 @@ class SkImageInfo { external SkImageInfo makeColorType(SkColorType colorType); external SkImageInfo makeWH(int width, int height); } + +@JS() +@anonymous +class SkPartialImageInfo { + external factory SkPartialImageInfo({ + required int width, + required int height, + required SkColorType colorType, + required SkAlphaType alphaType, + required ColorSpace colorSpace, + }); + external SkAlphaType get alphaType; + external ColorSpace get colorSpace; + external SkColorType get colorType; + external int get height; + external int get width; +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index 35e307df77b13..00ee90a3540c4 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -8,6 +8,7 @@ import 'dart:math' as math; import 'package:ui/ui.dart' as ui; import '../../engine.dart' show NullTreeSanitizer, platformViewManager; +import '../configuration.dart'; import '../html/path_to_svg_clip.dart'; import '../platform_views/slots.dart'; import '../util.dart'; @@ -27,19 +28,13 @@ class HtmlViewEmbedder { HtmlViewEmbedder._(); - /// The maximum number of overlay surfaces that can be live at once. - static const int maximumSurfaces = int.fromEnvironment( - 'FLUTTER_WEB_MAXIMUM_SURFACES', - defaultValue: 8, - ); - /// If `true`, overlay canvases are disabled. /// /// This causes all drawing to go to a single canvas, with all of the platform /// views rendered over top. This may result in incorrect rendering with /// platform views. static bool get disableOverlays => - debugDisableOverlays || maximumSurfaces <= 1; + debugDisableOverlays || configuration.canvasKitMaximumSurfaces <= 1; /// Force the view embedder to disable overlays. /// @@ -702,7 +697,7 @@ class HtmlViewEmbedder { // Try reusing a cached overlay created for another platform view. final Surface overlay = SurfaceFactory.instance.getOverlay()!; - overlay.createOrUpdateSurfaces(_frameSize); + overlay.createOrUpdateSurface(_frameSize); _overlays[viewId] = overlay; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart index 85dc7938e9df3..44d7fd01bfbe0 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -222,7 +222,7 @@ class FontFallbackData { void registerFallbackFont(String family, Uint8List bytes) { final SkTypeface? typeface = - canvasKit.FontMgr.RefDefault().MakeTypefaceFromData(bytes); + canvasKit.Typeface.MakeFreeTypeFaceFromData(bytes.buffer); if (typeface == null) { printWarning('Failed to parse fallback font $family as a font.'); return; diff --git a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart index 3bd5eae920365..e2e8e2804fe40 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -20,7 +20,7 @@ const String _robotoUrl = 'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf'; // URL for the Ahem font, only used in tests. -const String _ahemUrl = 'packages/ui/assets/ahem.ttf'; +const String _ahemUrl = '/assets/fonts/ahem.ttf'; /// Manages the fonts used in the Skia-based backend. class SkiaFontCollection { @@ -84,7 +84,7 @@ class SkiaFontCollection { } final SkTypeface? typeface = - canvasKit.FontMgr.RefDefault().MakeTypefaceFromData(list); + canvasKit.Typeface.MakeFreeTypeFaceFromData(list.buffer); if (typeface != null) { _registeredFonts.add(RegisteredFont(list, fontFamily, typeface)); await ensureFontsLoaded(); @@ -160,7 +160,7 @@ class SkiaFontCollection { final Uint8List bytes = buffer.asUint8List(); final SkTypeface? typeface = - canvasKit.FontMgr.RefDefault().MakeTypefaceFromData(bytes); + canvasKit.Typeface.MakeFreeTypeFaceFromData(bytes.buffer); if (typeface != null) { return RegisteredFont(bytes, family, typeface); } else { diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart index 9796ac88193e4..e6ad228641ea8 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -11,12 +11,61 @@ import 'package:ui/ui.dart' as ui; import '../html_image_codec.dart'; import '../util.dart'; import 'canvaskit_api.dart'; +import 'image_wasm_codecs.dart'; +import 'image_web_codecs.dart'; import 'skia_object_cache.dart'; /// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia. -ui.Codec skiaInstantiateImageCodec(Uint8List list, - [int? width, int? height, int? format, int? rowBytes]) { - return CkAnimatedImage.decodeFromBytes(list, 'encoded image bytes'); +// TODO(yjbanov): Implement targetWidth and targetHeight support. +// https://github.com/flutter/flutter/issues/34075 +FutureOr skiaInstantiateImageCodec(Uint8List list, + [int? targetWidth, int? targetHeight]) { + if (browserSupportsImageDecoder) { + return CkBrowserImageDecoder.create( + data: list, + debugSource: 'encoded image bytes', + targetWidth: targetWidth, + targetHeight: targetHeight, + ); + } else { + return CkAnimatedImage.decodeFromBytes(list, 'encoded image bytes'); + } +} + +// TODO(yjbanov): add support for targetWidth/targetHeight (https://github.com/flutter/flutter/issues/34075) +void skiaDecodeImageFromPixels( + Uint8List pixels, + int width, + int height, + ui.PixelFormat format, + ui.ImageDecoderCallback callback, { + int? rowBytes, + int? targetWidth, + int? targetHeight, + bool allowUpscaling = true, +}) { + // Run in a timer to avoid janking the current frame by moving the decoding + // work outside the frame event. + Timer.run(() { + final SkImage? skImage = canvasKit.MakeImage( + SkImageInfo( + width: width, + height: height, + colorType: format == ui.PixelFormat.rgba8888 ? canvasKit.ColorType.RGBA_8888 : canvasKit.ColorType.BGRA_8888, + alphaType: canvasKit.AlphaType.Premul, + colorSpace: SkColorSpaceSRGB, + ), + pixels, + rowBytes ?? 4 * width, + ); + + if (skImage == null) { + html.window.console.warn('Failed to create image from pixels.'); + return; + } + + return callback(CkImage(skImage)); + }); } /// Thrown when the web engine fails to decode an image, either due to a @@ -42,8 +91,19 @@ void debugRestoreHttpRequestFactory() { /// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after /// requesting from URI. Future skiaInstantiateWebImageCodec( + String url, WebOnlyImageCodecChunkCallback? chunkCallback) async { + final Uint8List list = await fetchImage(url, chunkCallback); + if (browserSupportsImageDecoder) { + return CkBrowserImageDecoder.create(data: list, debugSource: url.toString()); + } else { + return CkAnimatedImage.decodeFromBytes(list, url); + } +} + +/// Sends a request to fetch image data. +Future fetchImage( String url, WebOnlyImageCodecChunkCallback? chunkCallback) { - final Completer completer = Completer(); + final Completer completer = Completer(); final html.HttpRequest request = httpRequestFactory(); request.open('GET', url, async: true); @@ -78,110 +138,13 @@ Future skiaInstantiateWebImageCodec( return; } - try { - final Uint8List list = - Uint8List.view(request.response as ByteBuffer); - final CkAnimatedImage codec = CkAnimatedImage.decodeFromBytes(list, url); - completer.complete(codec); - } catch (error, stackTrace) { - completer.completeError(error, stackTrace); - } + completer.complete(Uint8List.view(request.response as ByteBuffer)); }); request.send(); return completer.future; } -/// The CanvasKit implementation of [ui.Codec]. -/// -/// Wraps `SkAnimatedImage`. -class CkAnimatedImage extends ManagedSkiaObject - implements ui.Codec { - /// Decodes an image from a list of encoded bytes. - CkAnimatedImage.decodeFromBytes(this._bytes, this.src); - - final String src; - final Uint8List _bytes; - int _frameCount = 0; - int _repetitionCount = -1; - - /// The index to the next frame to be decoded. - int _nextFrameIndex = 0; - - @override - SkAnimatedImage createDefault() { - final SkAnimatedImage? animatedImage = - canvasKit.MakeAnimatedImageFromEncoded(_bytes); - if (animatedImage == null) { - throw ImageCodecException( - 'Failed to decode image data.\n' - 'Image source: $src', - ); - } - - _frameCount = animatedImage.getFrameCount(); - _repetitionCount = animatedImage.getRepetitionCount(); - - // If the object has been deleted then resurrected, it may already have - // iterated over some frames. We need to skip over them. - for (int i = 0; i < _nextFrameIndex; i++) { - animatedImage.decodeNextFrame(); - } - return animatedImage; - } - - @override - SkAnimatedImage resurrect() => createDefault(); - - @override - bool get isResurrectionExpensive => true; - - @override - void delete() { - rawSkiaObject?.delete(); - } - - bool _disposed = false; - bool get debugDisposed => _disposed; - - bool _debugCheckIsNotDisposed() { - assert(!_disposed, 'This image has been disposed.'); - return true; - } - - @override - void dispose() { - assert( - !_disposed, - 'Cannot dispose a codec that has already been disposed.', - ); - _disposed = true; - delete(); - } - - @override - int get frameCount { - assert(_debugCheckIsNotDisposed()); - return _frameCount; - } - - @override - int get repetitionCount { - assert(_debugCheckIsNotDisposed()); - return _repetitionCount; - } - - @override - Future getNextFrame() { - assert(_debugCheckIsNotDisposed()); - final int durationMillis = skiaObject.decodeNextFrame(); - final Duration duration = Duration(milliseconds: durationMillis); - final CkImage image = CkImage(skiaObject.makeImageAtCurrentFrame()); - _nextFrameIndex = (_nextFrameIndex + 1) % _frameCount; - return Future.value(AnimatedImageFrameInfo(duration, image)); - } -} - /// A [ui.Image] backed by an `SkImage` from Skia. class CkImage implements ui.Image, StackTraceDebugger { CkImage(SkImage skImage) { @@ -214,16 +177,23 @@ class CkImage implements ui.Image, StackTraceDebugger { final int originalWidth = skImage.width(); final int originalHeight = skImage.height(); box = SkiaObjectBox.resurrectable(this, skImage, () { - return canvasKit.MakeImage( - SkImageInfo( - alphaType: canvasKit.AlphaType.Premul, - colorType: canvasKit.ColorType.RGBA_8888, - colorSpace: SkColorSpaceSRGB, - width: originalWidth, - height: originalHeight, - ), - originalBytes.buffer.asUint8List(), - 4 * originalWidth); + final SkImage? skImage = canvasKit.MakeImage( + SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + width: originalWidth, + height: originalHeight, + ), + originalBytes.buffer.asUint8List(), + 4 * originalWidth, + ); + if (skImage == null) { + throw ImageCodecException( + 'Failed to resurrect image from pixels.' + ); + } + return skImage; }); } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart b/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart new file mode 100644 index 0000000000000..2467e2a32a89b --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart @@ -0,0 +1,109 @@ +// 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. + +/// Uses image codecs supplied by the CanvasKit WASM bundle. +/// +/// See also: +/// +/// * `image_web_codecs.dart`, which uses the `ImageDecoder` supplied by the browser. +library image_wasm_codecs; + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:ui/ui.dart' as ui; + +import 'canvaskit_api.dart'; +import 'image.dart'; +import 'skia_object_cache.dart'; + +/// The CanvasKit implementation of [ui.Codec]. +/// +/// Wraps `SkAnimatedImage`. +class CkAnimatedImage extends ManagedSkiaObject + implements ui.Codec { + /// Decodes an image from a list of encoded bytes. + CkAnimatedImage.decodeFromBytes(this._bytes, this.src); + + final String src; + final Uint8List _bytes; + int _frameCount = 0; + int _repetitionCount = -1; + + /// The index to the next frame to be decoded. + int _nextFrameIndex = 0; + + @override + SkAnimatedImage createDefault() { + final SkAnimatedImage? animatedImage = + canvasKit.MakeAnimatedImageFromEncoded(_bytes); + if (animatedImage == null) { + throw ImageCodecException( + 'Failed to decode image data.\n' + 'Image source: $src', + ); + } + + _frameCount = animatedImage.getFrameCount(); + _repetitionCount = animatedImage.getRepetitionCount(); + + // If the object has been deleted then resurrected, it may already have + // iterated over some frames. We need to skip over them. + for (int i = 0; i < _nextFrameIndex; i++) { + animatedImage.decodeNextFrame(); + } + return animatedImage; + } + + @override + SkAnimatedImage resurrect() => createDefault(); + + @override + bool get isResurrectionExpensive => true; + + @override + void delete() { + rawSkiaObject?.delete(); + } + + bool _disposed = false; + bool get debugDisposed => _disposed; + + bool _debugCheckIsNotDisposed() { + assert(!_disposed, 'This image has been disposed.'); + return true; + } + + @override + void dispose() { + assert( + !_disposed, + 'Cannot dispose a codec that has already been disposed.', + ); + _disposed = true; + delete(); + } + + @override + int get frameCount { + assert(_debugCheckIsNotDisposed()); + return _frameCount; + } + + @override + int get repetitionCount { + assert(_debugCheckIsNotDisposed()); + return _repetitionCount; + } + + @override + Future getNextFrame() { + assert(_debugCheckIsNotDisposed()); + final int durationMillis = skiaObject.decodeNextFrame(); + final Duration duration = Duration(milliseconds: durationMillis); + final CkImage image = CkImage(skiaObject.makeImageAtCurrentFrame()); + _nextFrameIndex = (_nextFrameIndex + 1) % _frameCount; + return Future.value(AnimatedImageFrameInfo(duration, image)); + } +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart b/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart new file mode 100644 index 0000000000000..02d06cb550f07 --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart @@ -0,0 +1,433 @@ +// 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. + +/// Uses the `ImageDecoder` class supplied by the browser. +/// +/// See also: +/// +/// * `image_wasm_codecs.dart`, which uses codecs supplied by the CanvasKit WASM bundle. +@JS() +library image_web_codecs; + +import 'dart:async'; +import 'dart:html' as html; +import 'dart:js_util' as js_util; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:js/js.dart'; +import 'package:ui/ui.dart' as ui; + +import '../browser_detection.dart'; +import '../util.dart'; +import 'canvaskit_api.dart'; +import 'image.dart'; + +@JS('window.ImageDecoder') +external Object? get _imageDecoderConstructor; + +/// Whether the current browser supports `ImageDecoder`. +bool browserSupportsImageDecoder = + _imageDecoderConstructor != null && browserEngine == BrowserEngine.blink; + +/// Sets the value of [browserSupportsImageDecoder] to its default value. +void debugResetBrowserSupportsImageDecoder() { + browserSupportsImageDecoder = + _imageDecoderConstructor != null; +} + +/// Image decoder backed by the browser's `ImageDecoder`. +class CkBrowserImageDecoder implements ui.Codec { + static Future create({ + required Uint8List data, + required String debugSource, + int? targetWidth, + int? targetHeight, + }) async { + // ImageDecoder does not detect image type automatically. It requires us to + // tell it what the image type is. + final String? contentType = detectContentType(data); + + if (contentType == null) { + final String fileHeader; + if (data.isNotEmpty) { + fileHeader = '[' + bytesToHexString(data.sublist(0, math.min(10, data.length))) + ']'; + } else { + fileHeader = 'empty'; + } + throw ImageCodecException( + 'Failed to detect image file format using the file header.\n' + 'File header was $fileHeader.\n' + 'Image source: $debugSource' + ); + } + + try { + final _ImageDecoder webDecoder = _ImageDecoder(_ImageDecoderOptions( + type: contentType, + data: data, + + // Flutter always uses premultiplied alpha. + premultiplyAlpha: 'premultiply', + desiredWidth: targetWidth, + desiredHeight: targetHeight, + + // "default" gives the browser the liberty to convert to display-appropriate + // color space, typically SRGB, which is what we want. + colorSpaceConversion: 'default', + + // Flutter doesn't give the developer a way to customize this, so if this + // is an animated image we should prefer the animated track. + preferAnimation: true, + )); + + await js_util.promiseToFuture(webDecoder.tracks.ready); + + // Flutter doesn't have an API for progressive loading of images, so we + // wait until the image is fully decoded. + // package:js bindings don't work with getters that return a Promise, which + // is why js_util is used instead. + await js_util.promiseToFuture(js_util.getProperty(webDecoder, 'completed')); + return CkBrowserImageDecoder._(webDecoder, debugSource); + } catch (error) { + if (error is html.DomException) { + if (error.name == html.DomException.NOT_SUPPORTED) { + throw ImageCodecException( + 'Image file format ($contentType) is not supported by this browser\'s ImageDecoder API.\n' + 'Image source: $debugSource', + ); + } + } + throw ImageCodecException( + 'Failed to decode image using the browser\'s ImageDecoder API.\n' + 'Image source: $debugSource\n' + 'Original browser error: $error' + ); + } + } + + CkBrowserImageDecoder._(this.webDecoder, this.debugSource); + + final _ImageDecoder webDecoder; + final String debugSource; + + /// Whether this decoded has been disposed of. + /// + /// Once this turns true it stays true forever, and this decoder becomes + /// unusable. + bool _isDisposed = false; + + @override + void dispose() { + _isDisposed = true; + + // This releases all resources, including any currently running decoding work. + webDecoder.close(); + } + + void _debugCheckNotDisposed() { + assert( + !_isDisposed, + 'Cannot use this image decoder. It has been disposed of.' + ); + } + + @override + int get frameCount { + _debugCheckNotDisposed(); + return webDecoder.tracks.selectedTrack!.frameCount; + } + + /// The index of the frame that will be decoded on the next call of [getNextFrame]; + int _nextFrameIndex = 0; + + @override + Future getNextFrame() async { + _debugCheckNotDisposed(); + final _DecodeResult result = await js_util.promiseToFuture<_DecodeResult>( + webDecoder.decode(_DecodeOptions(frameIndex: _nextFrameIndex)), + ); + final _VideoFrame frame = result.image; + _nextFrameIndex = (_nextFrameIndex + 1) % frameCount; + + final SkImage? skImage = canvasKit.MakeLazyImageFromTextureSource( + frame, + SkPartialImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + width: frame.displayWidth, + height: frame.displayHeight, + ), + ); + + // Duration can be null if the image is not animated. However, Flutter + // requires a non-null value. 0 indicates that the frame is meant to be + // displayed indefinitely, which is fine for a static image. + final Duration duration = Duration(microseconds: frame.duration ?? 0); + + if (skImage == null) { + throw ImageCodecException( + 'Failed to create image from pixel data decoded using the browser\'s ImageDecoder.', + ); + } + + final CkImage image = CkImage(skImage); + return Future.value(AnimatedImageFrameInfo(duration, image)); + } + + @override + int get repetitionCount { + _debugCheckNotDisposed(); + return webDecoder.tracks.selectedTrack!.repetitionCount; + } +} + +/// Corresponds to JavaScript's `Promise`. +/// +/// This type doesn't need any members. Instead, it should be first converted +/// to Dart's [Future] using [promiseToFuture] then interacted with through the +/// [Future] API. +@JS() +@anonymous +class JsPromise {} + +/// Corresponds to the browser's `ImageDecoder` type. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#imagedecoder-interface +@JS('window.ImageDecoder') +class _ImageDecoder { + external _ImageDecoder(_ImageDecoderOptions options); + external _ImageTrackList get tracks; + external bool get complete; + external JsPromise decode(_DecodeOptions options); + external void close(); +} + +/// The result of [_ImageDecoder.decode]. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#imagedecoderesult-interface +@JS() +@anonymous +class _DecodeResult { + external _VideoFrame get image; + external bool get complete; +} + +/// Options passed to [_ImageDecoder.decode]. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#dictdef-imagedecodeoptions +@JS() +@anonymous +class _DecodeOptions { + external factory _DecodeOptions({ + required int frameIndex, + }); +} + +/// The only frame in a static image, or one of the frames in an animated one. +/// +/// This class maps to the `VideoFrame` type provided by the browser. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#videoframe-interface +@JS() +@anonymous +class _VideoFrame { + external int allocationSize(); + external JsPromise copyTo(Uint8List destination); + external String? get format; + external int get codedWidth; + external int get codedHeight; + external int get displayWidth; + external int get displayHeight; + external int? get duration; + external void close(); +} + +/// Corresponds to the browser's `ImageTrackList` type. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#imagetracklist-interface +@JS() +@anonymous +class _ImageTrackList { + external JsPromise get ready; + external _ImageTrack? get selectedTrack; +} + +/// Corresponds to the browser's `ImageTrack` type. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#imagetrack +@JS() +@anonymous +class _ImageTrack { + external int get repetitionCount; + external int get frameCount; +} + +/// Represents an image file format, such as PNG or JPEG. +class ImageFileFormat { + const ImageFileFormat(this.header, this.contentType); + + /// First few bytes in the file that uniquely identify the image file format. + /// + /// Null elements are treated as wildcard values and are not checked. This is + /// used to detect formats whose header is split up into multiple disjoint + /// parts, such that the first part is not unique enough to identify the + /// format. For example, without this, WebP may be confused with .ani + /// (animated cursor), .cda, and other formats that start with "RIFF". + final List header; + + /// The value that's passed as [_ImageDecoderOptions.type]. + /// + /// The server typically also uses this value as the "Content-Type" header, + /// but servers are not required to correctly detect the type. This value + /// is also known as MIME type. + final String contentType; + + /// All image file formats known to the Flutter Web engine. + /// + /// This list may need to be changed as browsers adopt new formats, and drop + /// support for obsolete ones. + /// + /// This list is checked linearly from top to bottom when detecting an image + /// type. It should therefore contain the most popular file formats at the + /// top, and less popular towards the bottom. + static const List values = [ + // ICO is not supported in Chrome. It is deemed too simple and too specific. See also: + // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/webcodecs/image_decoder_external.cc;l=38;drc=fd8802b593110ea18a97ef044f8a40dd24a622ec + + // PNG + ImageFileFormat([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], 'image/png'), + + // GIF87a + ImageFileFormat([0x47, 0x49, 0x46, 0x38, 0x37, 0x61], 'image/gif'), + + // GIF89a + ImageFileFormat([0x47, 0x49, 0x46, 0x38, 0x39, 0x61], 'image/gif'), + + // JPEG + ImageFileFormat([0xFF, 0xD8, 0xFF, 0xDB], 'image/jpeg'), + ImageFileFormat([0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01], 'image/jpeg'), + ImageFileFormat([0xFF, 0xD8, 0xFF, 0xEE], 'image/jpeg'), + ImageFileFormat([0xFF, 0xD8, 0xFF, 0xE1], 'image/jpeg'), + + // WebP + ImageFileFormat([0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50], 'image/webp'), + + // BMP + ImageFileFormat([0x42, 0x4D], 'image/bmp'), + ]; +} + +/// Function signature of [debugContentTypeDetector], which is the same as the +/// signature of [detectContentType]. +typedef DebugContentTypeDetector = String? Function(Uint8List); + +/// If not null, replaced the functionality of [detectContentType] with its own. +/// +/// This is useful in tests, for example, to test unsupported content types. +DebugContentTypeDetector? debugContentTypeDetector; + +/// Detects the image file format and returns the corresponding "Content-Type" +/// value (a.k.a. MIME type). +/// +/// The returned value can be passed to `ImageDecoder` when decoding an image. +/// +/// Returns null if [data] cannot be mapped to a known content type. +String? detectContentType(Uint8List data) { + if (debugContentTypeDetector != null) { + return debugContentTypeDetector!.call(data); + } + + formatLoop: for (final ImageFileFormat format in ImageFileFormat.values) { + if (data.length < format.header.length) { + continue; + } + + for (int i = 0; i < format.header.length; i++) { + final int? magicByte = format.header[i]; + if (magicByte == null) { + // Wildcard, accepts everything. + continue; + } + + final int headerByte = data[i]; + if (headerByte != magicByte) { + continue formatLoop; + } + } + + return format.contentType; + } + + if (isAvif(data)) { + return 'image/avif'; + } + + return null; +} + +/// A string of bytes that every AVIF image contains somehwere in its first 16 +/// bytes. +/// +/// This signature is necessary but not sufficient, which may lead to false +/// positives. For example, the file may be HEIC or a video. This is OK, +/// because in the worst case, the image decoder fails to decode the file. +/// This is something we must anticipate regardless of this detection logic. +/// The codec must already protect itself from downloaded files lying about +/// their contents. +/// +/// The alternative would be to implement a more precise detection, which would +/// add complexity and code size. This is how Chromium does it: +/// +/// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/image-decoders/avif/avif_image_decoder.cc;l=504;drc=fd8802b593110ea18a97ef044f8a40dd24a622ec +final List _avifSignature = 'ftyp'.codeUnits; + +/// Optimistically detects whether [data] is an AVIF image file. +bool isAvif(Uint8List data) { + firstByteLoop: for (int i = 0; i < 16; i += 1) { + for (int j = 0; j < _avifSignature.length; j += 1) { + if (i + j >= data.length) { + // Reached EOF without finding the signature. + return false; + } + if (data[i + j] != _avifSignature[j]) { + continue firstByteLoop; + } + } + return true; + } + return false; +} + +/// Options passed to the `ImageDecoder` constructor. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#imagedecoderinit-interface +@JS() +@anonymous +class _ImageDecoderOptions { + external factory _ImageDecoderOptions({ + required String type, + required Uint8List data, + required String premultiplyAlpha, + required int? desiredWidth, + required int? desiredHeight, + required String colorSpaceConversion, + required bool preferAnimation, + }); +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart index 4c6fc6a13a4bc..704896a6e062f 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart @@ -12,6 +12,7 @@ import 'package:js/js.dart'; import '../../engine.dart' show kProfileMode; import '../browser_detection.dart'; +import '../configuration.dart'; import '../dom_renderer.dart'; import 'canvaskit_api.dart'; import 'fonts.dart'; @@ -22,7 +23,7 @@ import 'fonts.dart'; external String? get requestedRendererType; /// Whether to use CanvasKit as the rendering backend. -bool get useCanvasKit => flutterWebAutoDetect ? _detectRenderer() : _useSkia; +bool get useCanvasKit => FlutterConfiguration.flutterWebAutoDetect ? _detectRenderer() : FlutterConfiguration.useSkia; /// Returns true if CanvasKit is used. /// @@ -36,66 +37,9 @@ bool _detectRenderer() { return isDesktop; } -/// Auto detect which rendering backend to use. -/// -/// Using flutter tools option "--web-render=auto" or not specifying one -/// would set the value to true. Otherwise, it would be false. -const bool flutterWebAutoDetect = - bool.fromEnvironment('FLUTTER_WEB_AUTO_DETECT', defaultValue: true); - -/// Enable the Skia-based rendering backend. -/// -/// Using flutter tools option "--web-render=canvaskit" would set the value to -/// true. -/// Using flutter tools option "--web-render=html" would set the value to false. -const bool _useSkia = - bool.fromEnvironment('FLUTTER_WEB_USE_SKIA', defaultValue: false); - -/// If set to true, forces CPU-only rendering (i.e. no WebGL). -/// -/// This is mainly used for testing or for apps that want to ensure they -/// run on devices which don't support WebGL. -const bool canvasKitForceCpuOnly = bool.fromEnvironment( - 'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY', - defaultValue: false); - -/// The version of CanvasKit used by the web engine by default. -// DO NOT EDIT THE NEXT LINE OF CODE MANUALLY -// See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. -const String canvaskitVersion = '0.30.0'; - -/// The URL to use when downloading the CanvasKit script and associated wasm. -/// -/// The expected directory structure nested under this URL is as follows: -/// -/// /canvaskit.js - the release build of CanvasKit JS API bindings -/// /canvaskit.wasm - the release build of CanvasKit WASM module -/// /profiling/canvaskit.js - the profile build of CanvasKit JS API bindings -/// /profiling/canvaskit.wasm - the profile build of CanvasKit WASM module -/// -/// The base URL can be overridden using the `FLUTTER_WEB_CANVASKIT_URL` -/// environment variable, which can be set in the Flutter tool using the -/// `--dart-define` option. The value must end with a `/`. -/// -/// Example: -/// -/// ``` -/// flutter run \ -/// -d chrome \ -/// --web-renderer=canvaskit \ -/// --dart-define=FLUTTER_WEB_CANVASKIT_URL=https://example.com/custom-canvaskit-build/ -/// ``` -/// -/// When CanvasKit pushes a new release to NPM, update this URL to reflect the -/// most recent version. For example, if CanvasKit releases version 0.34.0 to -/// NPM, update this URL to `https://unpkg.com/canvaskit-wasm@0.34.0/bin/`. -const String canvasKitBaseUrl = String.fromEnvironment( - 'FLUTTER_WEB_CANVASKIT_URL', - defaultValue: 'https://unpkg.com/canvaskit-wasm@$canvaskitVersion/bin/', -); -const String canvasKitBuildUrl = - canvasKitBaseUrl + (kProfileMode ? 'profiling/' : ''); -const String canvasKitJavaScriptBindingsUrl = +String get canvasKitBuildUrl => + configuration.canvasKitBaseUrl + (kProfileMode ? 'profiling/' : ''); +String get canvasKitJavaScriptBindingsUrl => canvasKitBuildUrl + 'canvaskit.js'; String canvasKitWasmModuleUrl(String file) => _currentCanvasKitBase! + file; diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 4a3a2ac5bd862..20e41adbdb9de 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -7,6 +7,7 @@ import 'dart:html' as html; import 'package:ui/ui.dart' as ui; import '../browser_detection.dart'; +import '../configuration.dart'; import '../platform_dispatcher.dart'; import '../util.dart'; import '../window.dart'; @@ -112,7 +113,7 @@ class Surface { /// /// The given [size] is in physical pixels. SurfaceFrame acquireFrame(ui.Size size) { - final CkSurface surface = createOrUpdateSurfaces(size); + final CkSurface surface = createOrUpdateSurface(size); // ignore: prefer_function_declarations_over_variables final SubmitCallback submitCallback = @@ -135,7 +136,7 @@ class Surface { double _currentDevicePixelRatio = -1; /// Creates a and SkSurface for the given [size]. - CkSurface createOrUpdateSurfaces(ui.Size size) { + CkSurface createOrUpdateSurface(ui.Size size) { if (size.isEmpty) { throw CanvasKitError('Cannot create surfaces of empty size.'); } @@ -295,7 +296,7 @@ class Surface { _forceNewContext = false; _contextLost = false; - if (webGLVersion != -1 && !canvasKitForceCpuOnly) { + if (webGLVersion != -1 && !configuration.canvasKitForceCpuOnly) { final int glContext = canvasKit.GetWebGLContext( htmlCanvas, SkWebGLContextOptions( @@ -328,7 +329,7 @@ class Surface { if (webGLVersion == -1) { return _makeSoftwareCanvasSurface( htmlCanvas!, 'WebGL support not detected'); - } else if (canvasKitForceCpuOnly) { + } else if (configuration.canvasKitForceCpuOnly) { return _makeSoftwareCanvasSurface( htmlCanvas!, 'CPU rendering forced by application'); } else if (_glContext == 0) { @@ -384,31 +385,36 @@ class Surface { /// A Dart wrapper around Skia's CkSurface. class CkSurface { - final SkSurface _surface; - final int? _glContext; - - CkSurface(this._surface, this._glContext); + CkSurface(this.surface, this._glContext); CkCanvas getCanvas() { assert(!_isDisposed, 'Attempting to use the canvas of a disposed surface'); - return CkCanvas(_surface.getCanvas()); + return CkCanvas(surface.getCanvas()); } + /// The underlying CanvasKit surface object. + /// + /// Only borrow this value temporarily. Do not store it as it may be deleted + /// at any moment. Storing it may lead to dangling pointer bugs. + final SkSurface surface; + + final int? _glContext; + /// Flushes the graphics to be rendered on screen. void flush() { - _surface.flush(); + surface.flush(); } int? get context => _glContext; - int width() => _surface.width(); - int height() => _surface.height(); + int width() => surface.width(); + int height() => surface.height(); void dispose() { if (_isDisposed) { return; } - _surface.dispose(); + surface.dispose(); _isDisposed = true; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart b/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart index 31339f204225d..2a5aefecf0ef5 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart @@ -12,7 +12,7 @@ class SurfaceFactory { /// /// [debugClear] causes this singleton to be reinitialized. static SurfaceFactory get instance => - _instance ??= SurfaceFactory(HtmlViewEmbedder.maximumSurfaces); + _instance ??= SurfaceFactory(configuration.canvasKitMaximumSurfaces); /// Returns the raw (potentially uninitialized) value of the singleton. /// diff --git a/lib/web_ui/lib/src/engine/configuration.dart b/lib/web_ui/lib/src/engine/configuration.dart new file mode 100644 index 0000000000000..b0e3f8e202e40 --- /dev/null +++ b/lib/web_ui/lib/src/engine/configuration.dart @@ -0,0 +1,171 @@ +// 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. + +/// JavaScript API a Flutter Web application can use to configure the Web +/// Engine. +/// +/// The configuration is a plain JavaScript object set as the +/// `flutterConfiguration` property of the top-level `window` object. +/// +/// Example: +/// +/// +/// +/// +/// +/// Configuration properties supplied via `window.flutterConfiguration` +/// override those supplied using the corresponding environment variables. For +/// example, if both `window.flutterConfiguration.canvasKitBaseUrl` and the +/// `FLUTTER_WEB_CANVASKIT_URL` environment variables are provided, +/// `window.flutterConfiguration.canvasKitBaseUrl` is used. + +@JS() +library configuration; + +import 'package:js/js.dart'; + +/// The version of CanvasKit used by the web engine by default. +// DO NOT EDIT THE NEXT LINE OF CODE MANUALLY +// See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. +const String _canvaskitVersion = '0.31.0'; + +/// The Web Engine configuration for the current application. +FlutterConfiguration get configuration => _configuration ??= FlutterConfiguration(_jsConfiguration); +FlutterConfiguration? _configuration; + +/// Sets the given configuration as the current one. +/// +/// This must be called before the engine is initialized. Calling it after the +/// engine is initialized will result in some of the properties not taking +/// effect because they are consumed during initialization. +void debugSetConfiguration(FlutterConfiguration configuration) { + _configuration = configuration; +} + +/// Supplies Web Engine configuration properties. +class FlutterConfiguration { + /// Constructs a configuration from a JavaScript object containing + /// runtime-supplied properties. + FlutterConfiguration(this._js); + + final JsFlutterConfiguration? _js; + + // Static constant parameters. + // + // These properties affect tree shaking and therefore cannot be supplied at + // runtime. They must be static constants for the compiler to remove dead + // effectively. + + /// Auto detect which rendering backend to use. + /// + /// Using flutter tools option "--web-render=auto" or not specifying one + /// would set the value to true. Otherwise, it would be false. + static const bool flutterWebAutoDetect = + bool.fromEnvironment('FLUTTER_WEB_AUTO_DETECT', defaultValue: true); + + /// Enable the Skia-based rendering backend. + /// + /// Using flutter tools option "--web-render=canvaskit" would set the value to + /// true. + /// + /// Using flutter tools option "--web-render=html" would set the value to false. + static const bool useSkia = + bool.fromEnvironment('FLUTTER_WEB_USE_SKIA', defaultValue: false); + + + // Runtime parameters. + // + // These parameters can be supplied either as environment variables, or at + // runtime. Runtime-supplied values take precedence over environment + // variables. + + /// The URL to use when downloading the CanvasKit script and associated wasm. + /// + /// The expected directory structure nested under this URL is as follows: + /// + /// /canvaskit.js - the release build of CanvasKit JS API bindings + /// /canvaskit.wasm - the release build of CanvasKit WASM module + /// /profiling/canvaskit.js - the profile build of CanvasKit JS API bindings + /// /profiling/canvaskit.wasm - the profile build of CanvasKit WASM module + /// + /// The base URL can be overridden using the `FLUTTER_WEB_CANVASKIT_URL` + /// environment variable or using the configuration API for JavaScript. + /// + /// When specifying using the environment variable set it in the Flutter tool + /// using the `--dart-define` option. The value must end with a `/`. + /// + /// Example: + /// + /// ``` + /// flutter run \ + /// -d chrome \ + /// --web-renderer=canvaskit \ + /// --dart-define=FLUTTER_WEB_CANVASKIT_URL=https://example.com/custom-canvaskit-build/ + /// ``` + String get canvasKitBaseUrl => _js?.canvasKitBaseUrl ?? _defaultCanvasKitBaseUrl; + static const String _defaultCanvasKitBaseUrl = String.fromEnvironment( + 'FLUTTER_WEB_CANVASKIT_URL', + defaultValue: 'https://unpkg.com/canvaskit-wasm@$_canvaskitVersion/bin/', + ); + + /// If set to true, forces CPU-only rendering in CanvasKit (i.e. the engine + /// won't use WebGL). + /// + /// This is mainly used for testing or for apps that want to ensure they + /// run on devices which don't support WebGL. + bool get canvasKitForceCpuOnly => _js?.canvasKitForceCpuOnly ?? _defaultCanvasKitForceCpuOnly; + static const bool _defaultCanvasKitForceCpuOnly = bool.fromEnvironment( + 'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY', + defaultValue: false, + ); + + /// The maximum number of overlay surfaces that the CanvasKit renderer will use. + /// + /// Overlay surfaces are extra WebGL `` elements used to paint on top + /// of platform views. Too many platform views can cause the browser to run + /// out of resources (memory, CPU, GPU) to handle the content efficiently. + /// The number of overlay surfaces is therefore limited. + /// + /// This value can be specified using either the `FLUTTER_WEB_MAXIMUM_SURFACES` + /// environment variable, or using the runtime configuration. + int get canvasKitMaximumSurfaces => _js?.canvasKitMaximumSurfaces ?? _defaultCanvasKitMaximumSurfaces; + static const int _defaultCanvasKitMaximumSurfaces = int.fromEnvironment( + 'FLUTTER_WEB_MAXIMUM_SURFACES', + defaultValue: 8, + ); + + /// Set this flag to `true` to cause the engine to visualize the semantics tree + /// on the screen for debugging. + /// + /// This only works in profile and release modes. Debug mode does not support + /// passing compile-time constants. + /// + /// Example: + /// + /// ``` + /// flutter run -d chrome --profile --dart-define=FLUTTER_WEB_DEBUG_SHOW_SEMANTICS=true + /// ``` + bool get debugShowSemanticsNodes => _js?.debugShowSemanticsNodes ?? _defaultDebugShowSemanticsNodes; + static const bool _defaultDebugShowSemanticsNodes = bool.fromEnvironment( + 'FLUTTER_WEB_DEBUG_SHOW_SEMANTICS', + defaultValue: false, + ); +} + +@JS('window.flutterConfiguration') +external JsFlutterConfiguration? get _jsConfiguration; + +/// The JS bindings for the object that's set as `window.flutterConfiguration`. +@JS() +@anonymous +class JsFlutterConfiguration { + external String? get canvasKitBaseUrl; + external bool? get canvasKitForceCpuOnly; + external int? get canvasKitMaximumSurfaces; + external bool? get debugShowSemanticsNodes; +} diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart index aaf97b1d89d6d..5013428092d99 100644 --- a/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -11,6 +11,7 @@ import 'package:ui/ui.dart' as ui; import '../engine.dart' show buildMode, registerHotRestartListener; import 'browser_detection.dart'; import 'canvaskit/initialization.dart'; +import 'configuration.dart'; import 'host_node.dart'; import 'keyboard_binding.dart'; import 'platform_dispatcher.dart'; @@ -109,11 +110,13 @@ class DomRenderer { /// See for more details: /// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus bool get windowHasFocus => + // ignore: implicit_dynamic_function js_util.callMethod(html.document, 'hasFocus', []) as bool; void _setupHotRestart() { // This persists across hot restarts to clear stale DOM. _staleHotRestartState = + // ignore: implicit_dynamic_function js_util.getProperty(html.window, _staleHotRestartStore) as List?; if (_staleHotRestartState == null) { _staleHotRestartState = []; @@ -232,6 +235,7 @@ class DomRenderer { static void setElementTransform(html.Element element, String transformValue) { js_util.setProperty( + // ignore: implicit_dynamic_function js_util.getProperty(element, 'style') as Object, 'transform', transformValue, @@ -291,7 +295,7 @@ class DomRenderer { setElementAttribute( bodyElement, 'flt-renderer', - '${useCanvasKit ? 'canvaskit' : 'html'} (${flutterWebAutoDetect ? 'auto-selected' : 'requested explicitly'})', + '${useCanvasKit ? 'canvaskit' : 'html'} (${FlutterConfiguration.flutterWebAutoDetect ? 'auto-selected' : 'requested explicitly'})', ); setElementAttribute(bodyElement, 'flt-build-mode', buildMode); @@ -398,7 +402,7 @@ class DomRenderer { // When debugging semantics, make the scene semi-transparent so that the // semantics tree is visible. - if (debugShowSemanticsNodes) { + if (configuration.debugShowSemanticsNodes) { _sceneHostElement!.style.opacity = '0.3'; } @@ -453,6 +457,7 @@ class DomRenderer { // Creates a [HostNode] into a `root` [html.Element]. HostNode _createHostNode(html.Element root) { + // ignore: implicit_dynamic_function if (js_util.getProperty(root, 'attachShadow') != null) { return ShadowDomHostNode(root); } else { @@ -523,6 +528,7 @@ class DomRenderer { double startAngle, double endAngle, bool antiClockwise) { + // ignore: implicit_dynamic_function _ellipseFeatureDetected ??= js_util.getProperty(context, 'ellipse') != null; if (_ellipseFeatureDetected!) { context.ellipse(centerX, centerY, radiusX, radiusY, rotation, startAngle, @@ -649,6 +655,7 @@ class DomRenderer { void vibrate(int durationMs) { final html.Navigator navigator = html.window.navigator; if (js_util.hasProperty(navigator, 'vibrate')) { + // ignore: implicit_dynamic_function js_util.callMethod(navigator, 'vibrate', [durationMs]); } } diff --git a/lib/web_ui/lib/src/engine/host_node.dart b/lib/web_ui/lib/src/engine/host_node.dart index f8f41c2b3cd9f..cb5a1c34b149a 100644 --- a/lib/web_ui/lib/src/engine/host_node.dart +++ b/lib/web_ui/lib/src/engine/host_node.dart @@ -97,9 +97,11 @@ class ShadowDomHostNode implements HostNode { root.isConnected ?? true, 'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.', ) { - _shadow = root.attachShadow({ + _shadow = root.attachShadow({ 'mode': 'open', - 'delegatesFocus': 'true', + // This needs to stay false to prevent issues like this: + // - https://github.com/flutter/flutter/issues/85759 + 'delegatesFocus': false, }); final html.StyleElement shadowRootStyleElement = html.StyleElement(); diff --git a/lib/web_ui/lib/src/engine/html/offscreen_canvas.dart b/lib/web_ui/lib/src/engine/html/offscreen_canvas.dart index 3fae5d1096ad3..fb7bbfb6c27a0 100644 --- a/lib/web_ui/lib/src/engine/html/offscreen_canvas.dart +++ b/lib/web_ui/lib/src/engine/html/offscreen_canvas.dart @@ -64,6 +64,7 @@ class OffScreenCanvas { void transferImage(Object targetContext) { // Actual size of canvas may be larger than viewport size. Use // source/destination to draw part of the image data. + // ignore: implicit_dynamic_function js_util.callMethod(targetContext, 'drawImage', [offScreenCanvas ?? canvasElement!, 0, 0, width, height, 0, 0, width, height]); @@ -77,6 +78,7 @@ class OffScreenCanvas { final html.FileReader fileReader = html.FileReader(); fileReader.onLoad.listen((html.ProgressEvent event) { completer.complete( + // ignore: implicit_dynamic_function js_util.getProperty(js_util.getProperty(event, 'target') as Object, 'result') as String, ); }); @@ -90,6 +92,7 @@ class OffScreenCanvas { /// Draws an image to canvas for both offscreen canvas canvas context2d. void drawImage(Object image, int x, int y, int width, int height) { + // ignore: implicit_dynamic_function js_util.callMethod( getContext2d()!, 'drawImage', [image, x, y, width, height]); } diff --git a/lib/web_ui/lib/src/engine/html/path/conic.dart b/lib/web_ui/lib/src/engine/html/path/conic.dart index ba5c50d673382..9a385a5969155 100644 --- a/lib/web_ui/lib/src/engine/html/path/conic.dart +++ b/lib/web_ui/lib/src/engine/html/path/conic.dart @@ -15,7 +15,7 @@ import 'path_utils.dart'; /// See "High order approximation of conic sections by quadratic splines" /// by Michael Floater, 1993. /// Skia implementation reference: -/// https://github.com/google/skia/blob/master/src/core/SkGeometry.cpp +/// https://github.com/google/skia/blob/main/src/core/SkGeometry.cpp class Conic { double p0x, p0y, p1x, p1y, p2x, p2y; final double fW; diff --git a/lib/web_ui/lib/src/engine/html/path/path_metrics.dart b/lib/web_ui/lib/src/engine/html/path/path_metrics.dart index 7109391c98447..67965267b2e60 100644 --- a/lib/web_ui/lib/src/engine/html/path/path_metrics.dart +++ b/lib/web_ui/lib/src/engine/html/path/path_metrics.dart @@ -533,7 +533,7 @@ const double _fTolerance = 0.5; /// the path. /// /// Implementation is based on -/// https://github.com/google/skia/blob/master/src/core/SkContourMeasure.cpp +/// https://github.com/google/skia/blob/main/src/core/SkContourMeasure.cpp /// to maintain consistency with native platforms. class SurfacePathMetric implements ui.PathMetric { SurfacePathMetric._(this._measure) diff --git a/lib/web_ui/lib/src/engine/html/path/path_ref.dart b/lib/web_ui/lib/src/engine/html/path/path_ref.dart index 14f14a3756a72..200fe9d45f4fb 100644 --- a/lib/web_ui/lib/src/engine/html/path/path_ref.dart +++ b/lib/web_ui/lib/src/engine/html/path/path_ref.dart @@ -429,7 +429,9 @@ class PathRef { resetToSize(verbCount, pointCount, weightCount, additionalReserveVerbs, additionalReservePoints); + // ignore: implicit_dynamic_function js_util.callMethod(_fVerbs, 'set', [ref._fVerbs]); + // ignore: implicit_dynamic_function js_util.callMethod(fPoints, 'set', [ref.fPoints]); if (ref._conicWeights == null) { _conicWeights = null; @@ -456,6 +458,7 @@ class PathRef { if (newLength > _fPointsCapacity) { _fPointsCapacity = newLength + 10; final Float32List newPoints = Float32List(_fPointsCapacity * 2); + // ignore: implicit_dynamic_function js_util.callMethod(newPoints, 'set', [fPoints]); fPoints = newPoints; } @@ -466,6 +469,7 @@ class PathRef { if (newLength > _fVerbsCapacity) { _fVerbsCapacity = newLength + 8; final Uint8List newVerbs = Uint8List(_fVerbsCapacity); + // ignore: implicit_dynamic_function js_util.callMethod(newVerbs, 'set', [_fVerbs]); _fVerbs = newVerbs; } @@ -477,6 +481,7 @@ class PathRef { _conicWeightsCapacity = newLength + 4; final Float32List newWeights = Float32List(_conicWeightsCapacity); if (_conicWeights != null) { + // ignore: implicit_dynamic_function js_util.callMethod(newWeights, 'set', [_conicWeights]); } _conicWeights = newWeights; diff --git a/lib/web_ui/lib/src/engine/html/render_vertices.dart b/lib/web_ui/lib/src/engine/html/render_vertices.dart index 7034c392a901a..2795443441283 100644 --- a/lib/web_ui/lib/src/engine/html/render_vertices.dart +++ b/lib/web_ui/lib/src/engine/html/render_vertices.dart @@ -204,6 +204,7 @@ class _WebGlRenderer implements GlRenderer { bufferVertexData(gl, positions, 1.0); // Setup data format for attribute. + // ignore: implicit_dynamic_function js_util.callMethod(gl.glContext, 'vertexAttribPointer', [ positionAttributeLocation, 2, @@ -233,6 +234,7 @@ class _WebGlRenderer implements GlRenderer { gl.bufferData(vertices.colors, gl.kStaticDraw); } final Object colorLoc = gl.getAttributeLocation(glProgram.program, 'color'); + // ignore: implicit_dynamic_function js_util.callMethod(gl.glContext, 'vertexAttribPointer', [colorLoc, 4, gl.kUnsignedByte, true, 0, 0]); gl.enableVertexAttribArray(colorLoc); @@ -374,6 +376,7 @@ class _WebGlRenderer implements GlRenderer { gl.bindArrayBuffer(positionsBuffer); gl.bufferData(vertices, gl.kStaticDraw); // Point an attribute to the currently bound vertex buffer object. + // ignore: implicit_dynamic_function js_util.callMethod(gl.glContext, 'vertexAttribPointer', [0, 2, gl.kFloat, false, 0, 0]); gl.enableVertexAttribArray(0); @@ -389,6 +392,7 @@ class _WebGlRenderer implements GlRenderer { 0xFF00FFFF, ]); gl.bufferData(colors, gl.kStaticDraw); + // ignore: implicit_dynamic_function js_util.callMethod(gl.glContext, 'vertexAttribPointer', [1, 4, gl.kUnsignedByte, true, 0, 0]); gl.enableVertexAttribArray(1); diff --git a/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart b/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart index 483e8fdc362e3..a5f85b1dd7b88 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart @@ -95,8 +95,10 @@ class EngineImageShader implements ui.ImageShader { /// To draw image flipped we set translate and scale and pass /// negative width/height to drawImage. if (flipX != 1 || flipY != 1) { + // ignore: implicit_dynamic_function js_util.callMethod(renderContext, 'scale', [flipX, flipY]); } + // ignore: implicit_dynamic_function js_util.callMethod(renderContext, 'drawImage', [ image.imgElement, if (x == 0) 0 else -2 * imageWidth, @@ -104,6 +106,7 @@ class EngineImageShader implements ui.ImageShader { ]); if (flipX != 1 || flipY != 1) { /// Restore transform. This is faster than save/restore on context. + // ignore: implicit_dynamic_function js_util.callMethod(renderContext, 'scale', [flipX, flipY]); } } @@ -205,6 +208,7 @@ class EngineImageShader implements ui.ImageShader { bufferVertexData(gl, vertices, ui.window.devicePixelRatio); /// Setup data format for attribute. + // ignore: implicit_dynamic_function js_util.callMethod(gl.glContext, 'vertexAttribPointer', [ positionAttributeLocation, 2, diff --git a/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart b/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart index 1343647ec6eba..7b346a045b903 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart @@ -81,6 +81,7 @@ class GlContext { double left, double top) { // Actual size of canvas may be larger than viewport size. Use // source/destination to draw part of the image data. + // ignore: implicit_dynamic_function js_util.callMethod(context, 'drawImage', [_canvas, 0, 0, _widthInPixels, _heightInPixels, left, top, _widthInPixels, _heightInPixels]); @@ -111,8 +112,11 @@ class GlContext { if (shader == null) { throw Exception(error); } + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'shaderSource', [shader, source]); + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'compileShader', [shader]); + // ignore: implicit_dynamic_function final bool shaderStatus = js_util.callMethod( glContext, 'getShaderParameter', @@ -123,16 +127,19 @@ class GlContext { } return shader; } - Object createProgram() => + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'createProgram', const []) as Object; void attachShader(Object? program, Object shader) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'attachShader', [program, shader]); } void linkProgram(Object program) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'linkProgram', [program]); + // ignore: implicit_dynamic_function final bool programStatus = js_util.callMethod( glContext, 'getProgramParameter', @@ -144,6 +151,7 @@ class GlContext { } void useProgram(GlProgram program) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'useProgram', [program.program]); } @@ -151,6 +159,7 @@ class GlContext { js_util.callMethod(glContext, 'createBuffer', const []); void bindArrayBuffer(Object? buffer) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'bindBuffer', [kArrayBuffer, buffer]); } @@ -158,16 +167,19 @@ class GlContext { js_util.callMethod(glContext, 'createVertexArray', const []); void bindVertexArray(Object vertexObjectArray) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'bindVertexArray', [vertexObjectArray]); } void unbindVertexArray() { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'bindVertexArray', [null]); } void bindElementArrayBuffer(Object? buffer) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'bindBuffer', [kElementArrayBuffer, buffer]); } @@ -178,10 +190,12 @@ class GlContext { js_util.callMethod(glContext, 'generateMipmap', [target]); void bindTexture(dynamic target, Object? buffer) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'bindTexture', [target, buffer]); } void activeTexture(int textureUnit) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'activeTexture', [textureUnit]); } @@ -189,9 +203,11 @@ class GlContext { dynamic format, dynamic dataType, dynamic pixels, {int? width, int? height, int border = 0}) { if (width == null) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'texImage2D', [ target, level, internalFormat, format, dataType, pixels]); } else { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'texImage2D', [ target, level, internalFormat, width, height, border, format, dataType, pixels]); @@ -199,33 +215,40 @@ class GlContext { } void texParameteri(dynamic target, dynamic parameterName, dynamic value) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'texParameteri', [ target, parameterName, value]); } void deleteBuffer(Object buffer) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'deleteBuffer', [buffer]); } void bufferData(TypedData? data, dynamic type) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'bufferData', [kArrayBuffer, data, type]); } void bufferElementData(TypedData? data, dynamic type) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'bufferData', [kElementArrayBuffer, data, type]); } void enableVertexAttribArray(dynamic index) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'enableVertexAttribArray', [index]); } /// Clear background. void clear() { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'clear', [kColorBufferBit]); } /// Destroys gl context. void dispose() { + // ignore: implicit_dynamic_function js_util.callMethod( _getExtension('WEBGL_lose_context') as Object, 'loseContext', @@ -234,28 +257,34 @@ class GlContext { } void deleteProgram(Object program) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'deleteProgram', [program]); } void deleteShader(Object shader) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'deleteShader', [shader]); } dynamic _getExtension(String extensionName) => + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'getExtension', [extensionName]); void drawTriangles(int triangleCount, ui.VertexMode vertexMode) { final dynamic mode = _triangleTypeFromMode(vertexMode); + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'drawArrays', [mode, 0, triangleCount]); } void drawElements(dynamic type, int indexCount, dynamic indexType) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'drawElements', [type, indexCount, indexType, 0]); } /// Sets affine transformation from normalized device coordinates /// to window coordinates void viewport(double x, double y, double width, double height) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'viewport', [x, y, width, height]); } @@ -271,76 +300,100 @@ class GlContext { } Object? _createShader(String shaderType) => js_util.callMethod( + // ignore: implicit_dynamic_function glContext, 'createShader', [js_util.getProperty(glContext, shaderType)]); /// Error state of gl context. + // ignore: implicit_dynamic_function dynamic get error => js_util.callMethod(glContext, 'getError', const []); /// Shader compiler error, if this returns [kFalse], to get details use /// [getShaderInfoLog]. dynamic get compileStatus => + // ignore: implicit_dynamic_function _kCompileStatus ??= js_util.getProperty(glContext, 'COMPILE_STATUS'); dynamic get kArrayBuffer => + // ignore: implicit_dynamic_function _kArrayBuffer ??= js_util.getProperty(glContext, 'ARRAY_BUFFER'); dynamic get kElementArrayBuffer => + // ignore: implicit_dynamic_function _kElementArrayBuffer ??= js_util.getProperty(glContext, 'ELEMENT_ARRAY_BUFFER'); dynamic get kLinkStatus => + // ignore: implicit_dynamic_function _kLinkStatus ??= js_util.getProperty(glContext, 'LINK_STATUS'); + // ignore: implicit_dynamic_function dynamic get kFloat => _kFloat ??= js_util.getProperty(glContext, 'FLOAT'); + // ignore: implicit_dynamic_function dynamic get kRGBA => _kRGBA ??= js_util.getProperty(glContext, 'RGBA'); dynamic get kUnsignedByte => + // ignore: implicit_dynamic_function _kUnsignedByte ??= js_util.getProperty(glContext, 'UNSIGNED_BYTE'); dynamic get kUnsignedShort => + // ignore: implicit_dynamic_function _kUnsignedShort ??= js_util.getProperty(glContext, 'UNSIGNED_SHORT'); dynamic get kStaticDraw => + // ignore: implicit_dynamic_function _kStaticDraw ??= js_util.getProperty(glContext, 'STATIC_DRAW'); dynamic get kTriangles => + // ignore: implicit_dynamic_function _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLES'); dynamic get kTriangleFan => + // ignore: implicit_dynamic_function _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_FAN'); dynamic get kTriangleStrip => + // ignore: implicit_dynamic_function _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_STRIP'); dynamic get kColorBufferBit => + // ignore: implicit_dynamic_function _kColorBufferBit ??= js_util.getProperty(glContext, 'COLOR_BUFFER_BIT'); dynamic get kTexture2D => + // ignore: implicit_dynamic_function _kTexture2D ??= js_util.getProperty(glContext, 'TEXTURE_2D'); int get kTexture0 => + // ignore: implicit_dynamic_function _kTexture0 ??= js_util.getProperty(glContext, 'TEXTURE0') as int; dynamic get kTextureWrapS => + // ignore: implicit_dynamic_function _kTextureWrapS ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_S'); dynamic get kTextureWrapT => + // ignore: implicit_dynamic_function _kTextureWrapT ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_T'); dynamic get kRepeat => + // ignore: implicit_dynamic_function _kRepeat ??= js_util.getProperty(glContext, 'REPEAT'); dynamic get kClampToEdge => + // ignore: implicit_dynamic_function _kClampToEdge ??= js_util.getProperty(glContext, 'CLAMP_TO_EDGE'); dynamic get kMirroredRepeat => + // ignore: implicit_dynamic_function _kMirroredRepeat ??= js_util.getProperty(glContext, 'MIRRORED_REPEAT'); dynamic get kLinear => + // ignore: implicit_dynamic_function _kLinear ??= js_util.getProperty(glContext, 'LINEAR'); dynamic get kTextureMinFilter => + // ignore: implicit_dynamic_function _kTextureMinFilter ??= js_util.getProperty(glContext, 'TEXTURE_MIN_FILTER'); @@ -375,41 +428,49 @@ class GlContext { /// Sets float uniform value. void setUniform1f(Object uniform, double value) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'uniform1f', [uniform, value]); } /// Sets vec2 uniform values. void setUniform2f(Object uniform, double value1, double value2) { + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'uniform2f', [uniform, value1, value2]); } /// Sets vec4 uniform values. void setUniform4f(Object uniform, double value1, double value2, double value3, double value4) { + // ignore: implicit_dynamic_function js_util.callMethod( glContext, 'uniform4f', [uniform, value1, value2, value3, value4]); } /// Sets mat4 uniform values. void setUniformMatrix4fv(Object uniform, bool transpose, Float32List value) { + // ignore: implicit_dynamic_function js_util.callMethod( glContext, 'uniformMatrix4fv', [uniform, transpose, value]); } /// Shader compile error log. dynamic getShaderInfoLog(Object glShader) { + // ignore: implicit_dynamic_function 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) { + // ignore: implicit_dynamic_function return js_util.callMethod(glContext, 'getProgramInfoLog', [glProgram]) as String?; } int? get drawingBufferWidth => + // ignore: implicit_dynamic_function js_util.getProperty(glContext, 'drawingBufferWidth') as int?; int? get drawingBufferHeight => + // ignore: implicit_dynamic_function js_util.getProperty(glContext, 'drawingBufferWidth') as int?; /// Reads gl contents as image data. @@ -423,6 +484,7 @@ class GlContext { browserEngine == BrowserEngine.firefox) { final Uint8List pixels = Uint8List(bufferWidth * bufferHeight * kBytesPerPixel); + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'readPixels', [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); return html.ImageData( @@ -430,6 +492,7 @@ class GlContext { } else { final Uint8ClampedList pixels = Uint8ClampedList(bufferWidth * bufferHeight * kBytesPerPixel); + // ignore: implicit_dynamic_function js_util.callMethod(glContext, 'readPixels', [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); return html.ImageData(pixels, bufferWidth, bufferHeight); @@ -444,6 +507,7 @@ class GlContext { // allocation. if (_canvas != null && js_util.hasProperty(_canvas!, 'transferToImageBitmap')) { + // ignore: implicit_dynamic_function js_util.callMethod(_canvas!, 'getContext', ['webgl2']); final Object? imageBitmap = js_util.callMethod(_canvas!, 'transferToImageBitmap', []); @@ -552,4 +616,4 @@ dynamic tileModeToGlWrapping(GlContext gl, ui.TileMode tileMode) { case ui.TileMode.repeated: return gl.kRepeat; } -} \ No newline at end of file +} 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 c52c9e4b781cd..234fa1fec8126 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -13,7 +13,9 @@ import 'browser_detection.dart'; import 'util.dart'; Object? get _jsImageDecodeFunction => js_util.getProperty( + // ignore: implicit_dynamic_function js_util.getProperty( + // ignore: implicit_dynamic_function js_util.getProperty(html.window, 'Image') as Object, 'prototype', ) as Object, diff --git a/lib/web_ui/lib/src/engine/navigation.dart b/lib/web_ui/lib/src/engine/navigation.dart index fae9027fa3cb2..e3746d1dc91ec 100644 --- a/lib/web_ui/lib/src/engine/navigation.dart +++ b/lib/web_ui/lib/src/engine/navigation.dart @@ -4,4 +4,4 @@ export 'navigation/history.dart'; export 'navigation/js_url_strategy.dart'; -export 'navigation/url_strategy.dart'; \ No newline at end of file +export 'navigation/url_strategy.dart'; diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 6957a192e6af7..c4d6ead8cad36 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:html' as html; +import 'dart:js_util' as js_util; import 'dart:typed_data'; import 'package:meta/meta.dart'; @@ -53,8 +54,10 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// The current platform configuration. @override ui.PlatformConfiguration get configuration => _configuration; - ui.PlatformConfiguration _configuration = - ui.PlatformConfiguration(locales: parseBrowserLanguages()); + ui.PlatformConfiguration _configuration = ui.PlatformConfiguration( + locales: parseBrowserLanguages(), + textScaleFactor: findBrowserTextScaleFactor(), + ); /// Receives all events related to platform configuration changes. @override @@ -1076,3 +1079,41 @@ void invoke3(void Function(A1 a1, A2 a2, A3 a3)? callback, }); } } + +const double _defaultRootFontSize = 16.0; + +/// Finds the text scale factor of the browser by looking at the computed style +/// of the browser's element. +double findBrowserTextScaleFactor() { + final num fontSize = _parseFontSize(html.document.documentElement!) ?? _defaultRootFontSize; + return fontSize / _defaultRootFontSize; +} + +/// Parses the font size of [element] and returns the value without a unit. +num? _parseFontSize(html.Element element) { + num? fontSize; + + if (js_util.hasProperty(element, 'computedStyleMap')) { + // Use the newer `computedStyleMap` API available on some browsers. + final dynamic computedStyleMap = + // ignore: implicit_dynamic_function + js_util.callMethod(element, 'computedStyleMap', []); + if (computedStyleMap is Object) { + final dynamic fontSizeObject = + // ignore: implicit_dynamic_function + js_util.callMethod(computedStyleMap, 'get', ['font-size']); + if (fontSizeObject is Object) { + // ignore: implicit_dynamic_function + fontSize = js_util.getProperty(fontSizeObject, 'value') as num; + } + } + } + + if (fontSize == null) { + // Fallback to `getComputedStyle`. + final String fontSizeString = element.getComputedStyle().fontSize; + fontSize = parseFloat(fontSizeString); + } + + return fontSize; +} diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index a70e644daf82d..71aec20b0ae7c 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -180,6 +180,7 @@ abstract class _BaseAdapter { // For native listener, we will need to remove it through native javascript // api. _nativeListeners.forEach((String eventName, html.EventListener listener) { + // ignore: implicit_dynamic_function js_util.callMethod( glassPaneElement, 'removeEventListener', [ @@ -293,10 +294,12 @@ mixin _WheelEventListenerMixin on _BaseAdapter { } void _addWheelEventListener(html.EventListener handler) { + // ignore: implicit_dynamic_function final Object eventOptions = js_util.newObject() as Object; final html.EventListener jsHandler = js.allowInterop((html.Event event) => handler(event)); _BaseAdapter._nativeListeners['wheel'] = jsHandler; js_util.setProperty(eventOptions, 'passive', false); + // ignore: implicit_dynamic_function js_util.callMethod( glassPaneElement, 'addEventListener', [ diff --git a/lib/web_ui/lib/src/engine/profiler.dart b/lib/web_ui/lib/src/engine/profiler.dart index b5f14383d3811..59a9466e1f0c8 100644 --- a/lib/web_ui/lib/src/engine/profiler.dart +++ b/lib/web_ui/lib/src/engine/profiler.dart @@ -106,6 +106,7 @@ class Profiler { _checkBenchmarkMode(); final OnBenchmark? onBenchmark = + // ignore: implicit_dynamic_function js_util.getProperty(html.window, '_flutter_internal_on_benchmark') as OnBenchmark?; if (onBenchmark != null) { onBenchmark(name, value); diff --git a/lib/web_ui/lib/src/engine/semantics/label_and_value.dart b/lib/web_ui/lib/src/engine/semantics/label_and_value.dart index e7229b89ba00b..80bf443a968cb 100644 --- a/lib/web_ui/lib/src/engine/semantics/label_and_value.dart +++ b/lib/web_ui/lib/src/engine/semantics/label_and_value.dart @@ -6,6 +6,7 @@ import 'dart:html' as html; import 'package:ui/ui.dart' as ui; +import '../configuration.dart'; import 'semantics.dart'; /// Renders [_label] and [_value] to the semantics DOM. @@ -107,7 +108,7 @@ class LabelAndValue extends RoleManager { // Normally use a small font size so that text doesn't leave the scope // of the semantics node. When debugging semantics, use a font size // that's reasonably visible. - _auxiliaryValueElement!.style.fontSize = debugShowSemanticsNodes ? '12px' : '6px'; + _auxiliaryValueElement!.style.fontSize = configuration.debugShowSemanticsNodes ? '12px' : '6px'; semanticsObject.element.append(_auxiliaryValueElement!); } _auxiliaryValueElement!.text = combinedValue.toString(); diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart index db59176cfa0d1..e0264bd34635c 100644 --- a/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -11,6 +11,7 @@ import 'package:ui/ui.dart' as ui; import '../../engine.dart' show registerHotRestartListener; import '../alarm_clock.dart'; import '../browser_detection.dart'; +import '../configuration.dart'; import '../dom_renderer.dart'; import '../platform_dispatcher.dart'; import '../util.dart'; @@ -25,22 +26,6 @@ import 'semantics_helper.dart'; import 'tappable.dart'; import 'text_field.dart'; -/// Set this flag to `true` to cause the engine to visualize the semantics tree -/// on the screen for debugging. -/// -/// This only works in profile and release modes. Debug mode does not support -/// passing compile-time constants. -/// -/// Example: -/// -/// ``` -/// flutter run -d chrome --profile --dart-define=FLUTTER_WEB_DEBUG_SHOW_SEMANTICS=true -/// ``` -const bool debugShowSemanticsNodes = bool.fromEnvironment( - 'FLUTTER_WEB_DEBUG_SHOW_SEMANTICS', - defaultValue: false, -); - /// Contains updates for the semantics tree. /// /// This class provides private engine-side API that's not available in the @@ -291,7 +276,7 @@ class SemanticsObject { element.style.position = 'absolute'; // The root node has some properties that other nodes do not. - if (id == 0 && !debugShowSemanticsNodes) { + if (id == 0 && !configuration.debugShowSemanticsNodes) { // Make all semantics transparent. We use `filter` instead of `opacity` // attribute because `filter` is stronger. `opacity` does not apply to // some elements, particularly on iOS, such as the slider thumb and track. @@ -308,7 +293,7 @@ class SemanticsObject { // Make semantic elements visible for debugging by outlining them using a // green border. We do not use `border` attribute because it affects layout // (`outline` does not). - if (debugShowSemanticsNodes) { + if (configuration.debugShowSemanticsNodes) { element.style.outline = '1px solid green'; } } diff --git a/lib/web_ui/lib/src/engine/services.dart b/lib/web_ui/lib/src/engine/services.dart index 5dc6d173953fa..651d4e48155c2 100644 --- a/lib/web_ui/lib/src/engine/services.dart +++ b/lib/web_ui/lib/src/engine/services.dart @@ -5,4 +5,4 @@ export 'services/buffers.dart'; export 'services/message_codec.dart'; export 'services/message_codecs.dart'; -export 'services/serialization.dart'; \ No newline at end of file +export 'services/serialization.dart'; diff --git a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart index 45c8023e9ffd3..b1263fdfa573b 100644 --- a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart @@ -185,7 +185,7 @@ class CanvasParagraph implements EngineParagraph { } final EngineLineMetrics line = lines[i]; - final List boxes = line.boxes!; + final List boxes = line.boxes; final StringBuffer buffer = StringBuffer(); int j = 0; diff --git a/lib/web_ui/lib/src/engine/text/font_collection.dart b/lib/web_ui/lib/src/engine/text/font_collection.dart index c56b77d5f057a..39e52148f19e9 100644 --- a/lib/web_ui/lib/src/engine/text/font_collection.dart +++ b/lib/web_ui/lib/src/engine/text/font_collection.dart @@ -14,9 +14,9 @@ import '../util.dart'; import 'layout_service.dart'; const String ahemFontFamily = 'Ahem'; -const String ahemFontUrl = 'packages/ui/assets/ahem.ttf'; +const String ahemFontUrl = '/assets/fonts/ahem.ttf'; const String robotoFontFamily = 'Roboto'; -const String robotoTestFontUrl = 'packages/ui/assets/Roboto-Regular.ttf'; +const String robotoTestFontUrl = '/assets/fonts/Roboto-Regular.ttf'; /// This class is responsible for registering and loading fonts. /// @@ -195,6 +195,7 @@ class FontManager { // // TODO(mdebbar): Revert this once the dart:html type is fixed. // https://github.com/dart-lang/sdk/issues/45676 + // ignore: implicit_dynamic_function js_util.callMethod(html.document.fonts!, 'add', [fontFace]); }, onError: (dynamic e) { printWarning('Error while trying to load font family "$family":\n$e'); diff --git a/lib/web_ui/lib/src/engine/text/layout_service.dart b/lib/web_ui/lib/src/engine/text/layout_service.dart index c4c62dfad48cc..ae9ebce657bc5 100644 --- a/lib/web_ui/lib/src/engine/text/layout_service.dart +++ b/lib/web_ui/lib/src/engine/text/layout_service.dart @@ -273,7 +273,7 @@ class TextLayoutService { List getBoxesForPlaceholders() { final List boxes = []; for (final EngineLineMetrics line in lines) { - for (final RangeBox box in line.boxes!) { + for (final RangeBox box in line.boxes) { if (box is PlaceholderBox) { boxes.add(box.toTextBox(line)); } @@ -303,7 +303,7 @@ class TextLayoutService { for (final EngineLineMetrics line in lines) { if (line.overlapsWith(start, end)) { - for (final RangeBox box in line.boxes!) { + for (final RangeBox box in line.boxes) { if (box is SpanBox && box.overlapsWith(start, end)) { boxes.add(box.intersect(line, start, end)); } @@ -336,7 +336,7 @@ class TextLayoutService { } final double dx = offset.dx - line.left; - for (final RangeBox box in line.boxes!) { + for (final RangeBox box in line.boxes) { if (box.left <= dx && dx <= box.right) { return box.getPositionForX(dx); } @@ -892,6 +892,8 @@ class LineBuilder { /// Whether the end of this line is a prohibited break. bool get isEndProhibited => end.type == LineBreakType.prohibited; + int _spaceBoxCount = 0; + bool get isEmpty => _segments.isEmpty; bool get isNotEmpty => _segments.isNotEmpty; @@ -1131,6 +1133,9 @@ class LineBuilder { if (_currentBoxStart.index > poppedSegment.start.index) { final RangeBox poppedBox = _boxes.removeLast(); _currentBoxStartOffset -= poppedBox.width; + if (poppedBox is SpanBox && poppedBox.isSpaceOnly) { + _spaceBoxCount--; + } } return poppedSegment; @@ -1274,6 +1279,10 @@ class LineBuilder { isSpaceOnly: isSpaceOnly, )); + if (isSpaceOnly) { + _spaceBoxCount++; + } + _currentBoxStartOffset = widthIncludingSpace; } @@ -1308,6 +1317,7 @@ class LineBuilder { ascent: ascent, descent: descent, boxes: _boxes, + spaceBoxCount: _spaceBoxCount, ); } diff --git a/lib/web_ui/lib/src/engine/text/paint_service.dart b/lib/web_ui/lib/src/engine/text/paint_service.dart index 423edef9ef81c..2d57b5e13ff2b 100644 --- a/lib/web_ui/lib/src/engine/text/paint_service.dart +++ b/lib/web_ui/lib/src/engine/text/paint_service.dart @@ -22,60 +22,105 @@ class TextPaintService { // individually. final List lines = paragraph.computeLineMetrics(); + if (lines.isEmpty) { + return; + } + + final EngineLineMetrics lastLine = lines.last; for (final EngineLineMetrics line in lines) { - for (final RangeBox box in line.boxes!) { - _paintBox(canvas, offset, line, box); + if (line.boxes.isEmpty) { + continue; + } + + final RangeBox lastBox = line.boxes.last; + final double justifyPerSpaceBox = + _calculateJustifyPerSpaceBox(paragraph, line, lastLine, lastBox); + + ui.Offset justifiedOffset = offset; + + for (final RangeBox box in line.boxes) { + final bool isTrailingSpaceBox = + box == lastBox && box is SpanBox && box.isSpaceOnly; + + // Don't paint background for the trailing space in the line. + if (!isTrailingSpaceBox) { + _paintBackground(canvas, justifiedOffset, line, box, justifyPerSpaceBox); + } + _paintText(canvas, justifiedOffset, line, box); + + if (box is SpanBox && box.isSpaceOnly && justifyPerSpaceBox != 0.0) { + justifiedOffset = justifiedOffset.translate(justifyPerSpaceBox, 0.0); + } } } } - void _paintBox( + void _paintBackground( BitmapCanvas canvas, ui.Offset offset, EngineLineMetrics line, RangeBox box, + double justifyPerSpaceBox, ) { - // Placeholder spans don't need any painting. Their boxes should remain - // empty so that their underlying widgets do their own painting. if (box is SpanBox) { final FlatTextSpan span = box.span; // Paint the background of the box, if the span has a background. final SurfacePaint? background = span.style.background as SurfacePaint?; if (background != null) { - canvas.drawRect( - box.toTextBox(line).toRect().shift(offset), - background.paintData, - ); + ui.Rect rect = box.toTextBox(line).toRect().shift(offset); + if (box.isSpaceOnly) { + rect = ui.Rect.fromPoints( + rect.topLeft, + rect.bottomRight.translate(justifyPerSpaceBox, 0.0), + ); + } + canvas.drawRect(rect, background.paintData); } + } + } + + void _paintText( + BitmapCanvas canvas, + ui.Offset offset, + EngineLineMetrics line, + RangeBox box, + ) { + // There's no text to paint in placeholder spans. + if (box is SpanBox) { + final FlatTextSpan span = box.span; - // Paint the actual text. _applySpanStyleToCanvas(span, canvas); final double x = offset.dx + line.left + box.left; final double y = offset.dy + line.baseline; - final String text = paragraph.toPlainText().substring( - box.start.index, - box.end.indexWithoutTrailingNewlines, - ); - final double? letterSpacing = span.style.letterSpacing; - if (letterSpacing == null || letterSpacing == 0.0) { - canvas.fillText(text, x, y, shadows: span.style.shadows); - } else { - // TODO(mdebbar): Implement letter-spacing on canvas more efficiently: - // https://github.com/flutter/flutter/issues/51234 - double charX = x; - final int len = text.length; - for (int i = 0; i < len; i++) { - final String char = text[i]; - canvas.fillText(char, charX.roundToDouble(), y, - shadows: span.style.shadows); - charX += letterSpacing + canvas.measureText(char).width!; + + // Don't paint the text for space-only boxes. This is just an + // optimization, it doesn't have any effect on the output. + if (!box.isSpaceOnly) { + final String text = paragraph.toPlainText().substring( + box.start.index, + box.end.indexWithoutTrailingNewlines, + ); + final double? letterSpacing = span.style.letterSpacing; + if (letterSpacing == null || letterSpacing == 0.0) { + canvas.fillText(text, x, y, shadows: span.style.shadows); + } else { + // TODO(mdebbar): Implement letter-spacing on canvas more efficiently: + // https://github.com/flutter/flutter/issues/51234 + double charX = x; + final int len = text.length; + for (int i = 0; i < len; i++) { + final String char = text[i]; + canvas.fillText(char, charX.roundToDouble(), y, + shadows: span.style.shadows); + charX += letterSpacing + canvas.measureText(char).width!; + } } } // Paint the ellipsis using the same span styles. final String? ellipsis = line.ellipsis; - if (ellipsis != null && box == line.boxes!.last) { + if (ellipsis != null && box == line.boxes.last) { final double x = offset.dx + line.left + box.right; canvas.fillText(ellipsis, x, y); } @@ -97,3 +142,31 @@ class TextPaintService { canvas.setUpPaint(paint.paintData, null); } } + +/// Calculates for the given [line], the amount of extra width that needs to be +/// added to each space box in order to align the line with the rest of the +/// paragraph. +double _calculateJustifyPerSpaceBox( + CanvasParagraph paragraph, + EngineLineMetrics line, + EngineLineMetrics lastLine, + RangeBox lastBox, +) { + // Don't apply any justification on the last line. + if (line != lastLine && + paragraph.width.isFinite && + paragraph.paragraphStyle.textAlign == ui.TextAlign.justify) { + final double justifyTotal = paragraph.width - line.width; + + int spaceBoxesToJustify = line.spaceBoxCount; + // If the last box is a space box, we can't use it to justify text. + if (lastBox is SpanBox && lastBox.isSpaceOnly) { + spaceBoxesToJustify--; + } + if (spaceBoxesToJustify > 0) { + return justifyTotal / spaceBoxesToJustify; + } + } + + return 0.0; +} diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index 3ebc48d918b07..78829587a5df6 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -31,33 +31,8 @@ class EngineLineMetrics implements ui.LineMetrics { endIndex = -1, endIndexWithoutNewlines = -1, widthWithTrailingSpaces = width, - boxes = null; - - EngineLineMetrics.withText( - String this.displayText, { - required this.startIndex, - required this.endIndex, - required this.endIndexWithoutNewlines, - required this.hardBreak, - required this.width, - required this.widthWithTrailingSpaces, - required this.left, - required this.lineNumber, - }) : assert(displayText != null), // ignore: unnecessary_null_comparison, - assert(startIndex != null), // ignore: unnecessary_null_comparison - assert(endIndex != null), // ignore: unnecessary_null_comparison - assert(endIndexWithoutNewlines != null), // ignore: unnecessary_null_comparison - assert(hardBreak != null), // ignore: unnecessary_null_comparison - assert(width != null), // ignore: unnecessary_null_comparison - assert(left != null), // ignore: unnecessary_null_comparison - assert(lineNumber != null && lineNumber >= 0), // ignore: unnecessary_null_comparison - ellipsis = null, - ascent = double.infinity, - descent = double.infinity, - unscaledAscent = double.infinity, - height = double.infinity, - baseline = double.infinity, - boxes = null; + boxes = [], + spaceBoxCount = 0; EngineLineMetrics.rich( this.lineNumber, { @@ -74,6 +49,7 @@ class EngineLineMetrics implements ui.LineMetrics { required this.ascent, required this.descent, required this.boxes, + required this.spaceBoxCount, }) : displayText = null, unscaledAscent = double.infinity; @@ -101,7 +77,10 @@ class EngineLineMetrics implements ui.LineMetrics { /// The list of boxes representing the entire line, possibly across multiple /// spans. - final List? boxes; + final List boxes; + + /// The number of boxes that are space-only. + final int spaceBoxCount; @override final bool hardBreak; diff --git a/lib/web_ui/lib/src/engine/ulps.dart b/lib/web_ui/lib/src/engine/ulps.dart index 58e5b2c00bad0..10677dc96ba43 100644 --- a/lib/web_ui/lib/src/engine/ulps.dart +++ b/lib/web_ui/lib/src/engine/ulps.dart @@ -20,7 +20,7 @@ import 'dart:typed_data'; // For some good articles on the topic, see // https://randomascii.wordpress.com/category/floating-point/page/2/ // Port based on: -// https://github.com/google/skia/blob/master/include/private/SkFloatBits.h +// https://github.com/google/skia/blob/main/include/private/SkFloatBits.h // // Here is the 32 bit IEEE representation: // uint32_t mantissa : 23; diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index 026a459a7fca2..48868be64b835 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -2,12 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +@JS() +library util; + import 'dart:async'; import 'dart:html' as html; import 'dart:js_util' as js_util; import 'dart:math' as math; import 'dart:typed_data'; +import 'package:js/js.dart'; import 'package:ui/ui.dart' as ui; import 'browser_detection.dart'; @@ -391,6 +395,7 @@ String colorComponentsToCssString(int r, int g, int b, int a) { /// Firefox exception without interfering with others (potentially useful /// for the programmer). bool isNsErrorFailureException(Object e) { + // ignore: implicit_dynamic_function return js_util.getProperty(e, 'name') == 'NS_ERROR_FAILURE'; } @@ -422,8 +427,20 @@ const Set _genericFontFamilies = { /// /// For iOS, default to -apple-system, where it should be available, otherwise /// default to Arial. BlinkMacSystemFont is used for Chrome on iOS. -final String _fallbackFontFamily = - isMacOrIOS ? '-apple-system, BlinkMacSystemFont' : 'Arial'; +String get _fallbackFontFamily { + if (isIOS15) { + // Remove the "-apple-system" fallback font because it causes a crash in + // iOS 15. + // + // See github issue: https://github.com/flutter/flutter/issues/90705 + // See webkit bug: https://bugs.webkit.org/show_bug.cgi?id=231686 + return 'BlinkMacSystemFont'; + } + if (isMacOrIOS) { + return '-apple-system, BlinkMacSystemFont'; + } + return 'Arial'; +} /// Create a font-family string appropriate for CSS. /// @@ -502,16 +519,6 @@ double convertSigmaToRadius(double sigma) { return sigma * 2.0; } -/// Used to check for null values that are non-nullable. -/// -/// This is useful when some external API (e.g. HTML DOM) disagrees with -/// Dart type declarations (e.g. `dart:html`). Where `dart:html` may believe -/// something to be non-null, it may actually be null (e.g. old browsers do -/// not implement a feature, such as clipboard). -bool isUnsoundNull(dynamic object) { - return object == null; -} - int clampInt(int value, int min, int max) { assert(min <= max); if (value < min) { @@ -640,3 +647,38 @@ extension JsonExtensions on Map { return this[propertyName] as double?; } } + +typedef JsParseFloat = num? Function(String source); + +@JS('parseFloat') +external JsParseFloat get _jsParseFloat; + +/// Parses a string [source] into a double. +/// +/// Uses the JavaScript `parseFloat` function instead of Dart's [double.parse] +/// because the latter can't parse strings like "20px". +/// +/// Returns null if it fails to parse. +num? parseFloat(String source) { + // Using JavaScript's `parseFloat` here because it can parse values + // like "20px", while Dart's `double.tryParse` fails. + final num? result = _jsParseFloat(source); + + if (result == null || result.isNaN) { + return null; + } + return result; +} + +/// Prints a list of bytes in hex format. +/// +/// Bytes are separated by one space and are padded on the left to always show +/// two digits. +/// +/// Example: +/// +/// Input: [0, 1, 2, 3] +/// Output: 0x00 0x01 0x02 0x03 +String bytesToHexString(List data) { + return data.map((int byte) => '0x' + byte.toRadixString(16).padLeft(2, '0')).join(' '); +} diff --git a/lib/web_ui/lib/src/ui/geometry.dart b/lib/web_ui/lib/src/ui/geometry.dart index 06e4655b339a3..0882cfaf83f41 100644 --- a/lib/web_ui/lib/src/ui/geometry.dart +++ b/lib/web_ui/lib/src/ui/geometry.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// See https://github.com/flutter/engine/blob/master/lib/ui/geometry.dart for +// See https://github.com/flutter/engine/blob/main/lib/ui/geometry.dart for // documentation of APIs. // ignore_for_file: public_member_api_docs part of ui; diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 33fb98737879c..98a9213bb1e06 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// For documentation see https://github.com/flutter/engine/blob/master/lib/ui/painting.dart +// For documentation see https://github.com/flutter/engine/blob/main/lib/ui/painting.dart // ignore_for_file: public_member_api_docs part of ui; @@ -463,8 +463,7 @@ Future instantiateImageCodec( bool allowUpscaling = true, }) async { if (engine.useCanvasKit) { - // TODO(hterkelsen): Implement targetWidth and targetHeight support. - return engine.skiaInstantiateImageCodec(list); + return engine.skiaInstantiateImageCodec(list, targetWidth, targetHeight); } else { final html.Blob blob = html.Blob([list.buffer]); return engine.HtmlBlobCodec(blob); @@ -500,6 +499,12 @@ Future _decodeImageFromListAsync(Uint8List list, ImageDecoderCallback call final FrameInfo frameInfo = await codec.getNextFrame(); callback(frameInfo.image); } + +// Encodes the input pixels into a BMP file that supports transparency. +// +// The `pixels` should be the scanlined raw pixels, 4 bytes per pixel, from left +// to right, then from top to down. The order of the 4 bytes of pixels is +// decided by `format`. Future _createBmp( Uint8List pixels, int width, @@ -507,64 +512,70 @@ Future _createBmp( int rowBytes, PixelFormat format, ) { + late bool swapRedBlue; + switch (format) { + case PixelFormat.bgra8888: + swapRedBlue = true; + break; + case PixelFormat.rgba8888: + swapRedBlue = false; + break; + } + // See https://en.wikipedia.org/wiki/BMP_file_format for format examples. - final int bufferSize = 0x36 + (width * height * 4); + // The header is in the 108-byte BITMAPV4HEADER format, or as called by + // Chromium, WindowsV4. Do not use the 56-byte or 52-byte Adobe formats, since + // they're not supported. + const int dibSize = 0x6C /* 108: BITMAPV4HEADER */; + const int headerSize = dibSize + 0x0E; + final int bufferSize = headerSize + (width * height * 4); final ByteData bmpData = ByteData(bufferSize); // 'BM' header - bmpData.setUint8(0x00, 0x42); - bmpData.setUint8(0x01, 0x4D); + bmpData.setUint16(0x00, 0x424D, Endian.big); // Size of data bmpData.setUint32(0x02, bufferSize, Endian.little); // Offset where pixel array begins - bmpData.setUint32(0x0A, 0x36, Endian.little); + bmpData.setUint32(0x0A, headerSize, Endian.little); // Bytes in DIB header - bmpData.setUint32(0x0E, 0x28, Endian.little); - // width + bmpData.setUint32(0x0E, dibSize, Endian.little); + // Width bmpData.setUint32(0x12, width, Endian.little); - // height + // Height bmpData.setUint32(0x16, height, Endian.little); - // Color panes + // Color panes (always 1) bmpData.setUint16(0x1A, 0x01, Endian.little); - // 32 bpp - bmpData.setUint16(0x1C, 0x20, Endian.little); - // no compression - bmpData.setUint32(0x1E, 0x00, Endian.little); - // raw bitmap data size + // bpp: 32 + bmpData.setUint16(0x1C, 32, Endian.little); + // Compression method is BITFIELDS to enable bit fields + bmpData.setUint32(0x1E, 3, Endian.little); + // Raw bitmap data size bmpData.setUint32(0x22, width * height, Endian.little); - // print DPI width + // Print DPI width bmpData.setUint32(0x26, width, Endian.little); - // print DPI height + // Print DPI height bmpData.setUint32(0x2A, height, Endian.little); - // colors in the palette + // Colors in the palette bmpData.setUint32(0x2E, 0x00, Endian.little); - // important colors + // Important colors bmpData.setUint32(0x32, 0x00, Endian.little); - - - int pixelDestinationIndex = 0; - late bool swapRedBlue; - switch (format) { - case PixelFormat.bgra8888: - swapRedBlue = true; - break; - case PixelFormat.rgba8888: - swapRedBlue = false; - break; - } - for (int pixelSourceIndex = 0; pixelSourceIndex < pixels.length; pixelSourceIndex += 4) { - final int r = swapRedBlue ? pixels[pixelSourceIndex + 2] : pixels[pixelSourceIndex]; - final int b = swapRedBlue ? pixels[pixelSourceIndex] : pixels[pixelSourceIndex + 2]; - final int g = pixels[pixelSourceIndex + 1]; - final int a = pixels[pixelSourceIndex + 3]; - - // Set the pixel past the header data. - bmpData.setUint8(pixelDestinationIndex + 0x36, r); - bmpData.setUint8(pixelDestinationIndex + 0x37, g); - bmpData.setUint8(pixelDestinationIndex + 0x38, b); - bmpData.setUint8(pixelDestinationIndex + 0x39, a); - pixelDestinationIndex += 4; - if (rowBytes != width && pixelSourceIndex % width == 0) { - pixelSourceIndex += rowBytes - width; + // Bitmask R + bmpData.setUint32(0x36, swapRedBlue ? 0x00FF0000 : 0x000000FF, Endian.little); + // Bitmask G + bmpData.setUint32(0x3A, 0x0000FF00, Endian.little); + // Bitmask B + bmpData.setUint32(0x3E, swapRedBlue ? 0x000000FF : 0x00FF0000, Endian.little); + // Bitmask A + bmpData.setUint32(0x42, 0xFF000000, Endian.little); + + int destinationByte = headerSize; + final Uint32List combinedPixels = Uint32List.sublistView(pixels); + // BMP is scanlined from bottom to top. Rearrange here. + for (int rowCount = height - 1; rowCount >= 0; rowCount -= 1) { + int sourcePixel = rowCount * rowBytes; + for (int colCount = 0; colCount < width; colCount += 1) { + bmpData.setUint32(destinationByte, combinedPixels[sourcePixel], Endian.little); + destinationByte += 4; + sourcePixel += 1; } } @@ -585,15 +596,17 @@ void decodeImageFromPixels( bool allowUpscaling = true, }) { if (engine.useCanvasKit) { - engine.skiaInstantiateImageCodec( + engine.skiaDecodeImageFromPixels( pixels, width, height, - format.index, - rowBytes, - ).getNextFrame().then((FrameInfo info) { - callback(info.image); - }); + format, + callback, + rowBytes: rowBytes, + targetWidth: targetWidth, + targetHeight: targetHeight, + allowUpscaling: allowUpscaling, + ); return; } @@ -787,16 +800,18 @@ class ImageDescriptor { } } -class FragmentShader extends Shader { - FragmentShader({ - required ByteBuffer spirv, // ignore: avoid_unused_constructor_parameters - Float32List? floatUniforms, // ignore: avoid_unused_constructor_parameters - bool debugPrint = false, // ignore: avoid_unused_constructor_parameters - }) : super._() { - throw UnsupportedError('FragmentShader is not supported for the CanvasKit or HTML renderers.'); +class FragmentProgram { + static Future compile({ + required ByteBuffer spirv, + bool debugPrint = false, + }) { + throw UnsupportedError('FragmentProgram is not supported for the CanvasKit or HTML renderers.'); } - void update({Float32List? floatUniforms}) => - throw UnsupportedError('FragmentShader is not supported for the CanvasKit or HTML renderers.'); -} + FragmentProgram._(); + Shader shader({ + Float32List? floatUniforms, + List? samplerUniforms, + }) => throw UnsupportedError('FragmentProgram is not supported for the CanvasKit or HTML renderers.'); +} diff --git a/lib/web_ui/lib/src/ui/path.dart b/lib/web_ui/lib/src/ui/path.dart index 0d9b7cc6a9fb9..49cd141798048 100644 --- a/lib/web_ui/lib/src/ui/path.dart +++ b/lib/web_ui/lib/src/ui/path.dart @@ -4,7 +4,7 @@ part of ui; -// For documentation see https://github.com/flutter/engine/blob/master/lib/ui/painting.dart +// For documentation see https://github.com/flutter/engine/blob/main/lib/ui/painting.dart // ignore_for_file: public_member_api_docs abstract class Path { diff --git a/lib/web_ui/lib/src/ui/semantics.dart b/lib/web_ui/lib/src/ui/semantics.dart index 59adfda24e2af..ab18cea09e7e5 100644 --- a/lib/web_ui/lib/src/ui/semantics.dart +++ b/lib/web_ui/lib/src/ui/semantics.dart @@ -29,7 +29,9 @@ class SemanticsAction { static const int _kMoveCursorForwardByWordIndex = 1 << 19; static const int _kMoveCursorBackwardByWordIndex = 1 << 20; static const int _kSetText = 1 << 21; + final int index; + static const SemanticsAction tap = SemanticsAction._(_kTapIndex); static const SemanticsAction longPress = SemanticsAction._(_kLongPressIndex); static const SemanticsAction scrollLeft = SemanticsAction._(_kScrollLeftIndex); @@ -39,25 +41,20 @@ class SemanticsAction { static const SemanticsAction increase = SemanticsAction._(_kIncreaseIndex); static const SemanticsAction decrease = SemanticsAction._(_kDecreaseIndex); static const SemanticsAction showOnScreen = SemanticsAction._(_kShowOnScreenIndex); - static const SemanticsAction moveCursorForwardByCharacter = - SemanticsAction._(_kMoveCursorForwardByCharacterIndex); - static const SemanticsAction moveCursorBackwardByCharacter = - SemanticsAction._(_kMoveCursorBackwardByCharacterIndex); + static const SemanticsAction moveCursorForwardByCharacter = SemanticsAction._(_kMoveCursorForwardByCharacterIndex); + static const SemanticsAction moveCursorBackwardByCharacter = SemanticsAction._(_kMoveCursorBackwardByCharacterIndex); + static const SemanticsAction setText = SemanticsAction._(_kSetText); static const SemanticsAction setSelection = SemanticsAction._(_kSetSelectionIndex); static const SemanticsAction copy = SemanticsAction._(_kCopyIndex); static const SemanticsAction cut = SemanticsAction._(_kCutIndex); static const SemanticsAction paste = SemanticsAction._(_kPasteIndex); - static const SemanticsAction didGainAccessibilityFocus = - SemanticsAction._(_kDidGainAccessibilityFocusIndex); - static const SemanticsAction didLoseAccessibilityFocus = - SemanticsAction._(_kDidLoseAccessibilityFocusIndex); + static const SemanticsAction didGainAccessibilityFocus = SemanticsAction._(_kDidGainAccessibilityFocusIndex); + static const SemanticsAction didLoseAccessibilityFocus = SemanticsAction._(_kDidLoseAccessibilityFocusIndex); static const SemanticsAction customAction = SemanticsAction._(_kCustomAction); static const SemanticsAction dismiss = SemanticsAction._(_kDismissIndex); - static const SemanticsAction moveCursorForwardByWord = - SemanticsAction._(_kMoveCursorForwardByWordIndex); - static const SemanticsAction moveCursorBackwardByWord = - SemanticsAction._(_kMoveCursorBackwardByWordIndex); - static const SemanticsAction setText = SemanticsAction._(_kSetText); + static const SemanticsAction moveCursorForwardByWord = SemanticsAction._(_kMoveCursorForwardByWordIndex); + static const SemanticsAction moveCursorBackwardByWord = SemanticsAction._(_kMoveCursorBackwardByWordIndex); + static const Map values = { _kTapIndex: tap, _kLongPressIndex: longPress, @@ -131,12 +128,16 @@ class SemanticsAction { case _kSetText: return 'SemanticsAction.setText'; } - assert(false, 'Unhandled index: $index'); + assert(false, 'Unhandled index: $index (0x${index.toRadixString(8).padLeft(4, "0")})'); return ''; } } class SemanticsFlag { + const SemanticsFlag._(this.index) : assert(index != null); // ignore: unnecessary_null_comparison + + final int index; + static const int _kHasCheckedStateIndex = 1 << 0; static const int _kIsCheckedIndex = 1 << 1; static const int _kIsSelectedIndex = 1 << 2; @@ -163,15 +164,15 @@ class SemanticsFlag { static const int _kIsSliderIndex = 1 << 23; static const int _kIsKeyboardKeyIndex = 1 << 24; - const SemanticsFlag._(this.index) : assert(index != null); // ignore: unnecessary_null_comparison - final int index; static const SemanticsFlag hasCheckedState = SemanticsFlag._(_kHasCheckedStateIndex); static const SemanticsFlag isChecked = SemanticsFlag._(_kIsCheckedIndex); static const SemanticsFlag isSelected = SemanticsFlag._(_kIsSelectedIndex); static const SemanticsFlag isButton = SemanticsFlag._(_kIsButtonIndex); - static const SemanticsFlag isLink = SemanticsFlag._(_kIsLinkIndex); static const SemanticsFlag isTextField = SemanticsFlag._(_kIsTextFieldIndex); + static const SemanticsFlag isSlider = SemanticsFlag._(_kIsSliderIndex); + static const SemanticsFlag isKeyboardKey = SemanticsFlag._(_kIsKeyboardKeyIndex); static const SemanticsFlag isReadOnly = SemanticsFlag._(_kIsReadOnlyIndex); + static const SemanticsFlag isLink = SemanticsFlag._(_kIsLinkIndex); static const SemanticsFlag isFocusable = SemanticsFlag._(_kIsFocusableIndex); static const SemanticsFlag isFocused = SemanticsFlag._(_kIsFocusedIndex); static const SemanticsFlag hasEnabledState = SemanticsFlag._(_kHasEnabledStateIndex); @@ -179,6 +180,7 @@ class SemanticsFlag { static const SemanticsFlag isInMutuallyExclusiveGroup = SemanticsFlag._(_kIsInMutuallyExclusiveGroupIndex); static const SemanticsFlag isHeader = SemanticsFlag._(_kIsHeaderIndex); static const SemanticsFlag isObscured = SemanticsFlag._(_kIsObscuredIndex); + static const SemanticsFlag isMultiline = SemanticsFlag._(_kIsMultilineIndex); static const SemanticsFlag scopesRoute = SemanticsFlag._(_kScopesRouteIndex); static const SemanticsFlag namesRoute = SemanticsFlag._(_kNamesRouteIndex); static const SemanticsFlag isHidden = SemanticsFlag._(_kIsHiddenIndex); @@ -187,18 +189,13 @@ class SemanticsFlag { static const SemanticsFlag hasToggledState = SemanticsFlag._(_kHasToggledStateIndex); static const SemanticsFlag isToggled = SemanticsFlag._(_kIsToggledIndex); static const SemanticsFlag hasImplicitScrolling = SemanticsFlag._(_kHasImplicitScrollingIndex); - static const SemanticsFlag isMultiline = SemanticsFlag._(_kIsMultilineIndex); - static const SemanticsFlag isSlider = SemanticsFlag._(_kIsSliderIndex); - static const SemanticsFlag isKeyboardKey = SemanticsFlag._(_kIsKeyboardKeyIndex); + static const Map values = { _kHasCheckedStateIndex: hasCheckedState, _kIsCheckedIndex: isChecked, _kIsSelectedIndex: isSelected, _kIsButtonIndex: isButton, - _kIsLinkIndex: isLink, - _kIsSliderIndex: isSlider, _kIsTextFieldIndex: isTextField, - _kIsFocusableIndex: isFocusable, _kIsFocusedIndex: isFocused, _kHasEnabledStateIndex: hasEnabledState, _kIsEnabledIndex: isEnabled, @@ -215,6 +212,9 @@ class SemanticsFlag { _kHasImplicitScrollingIndex: hasImplicitScrolling, _kIsMultilineIndex: isMultiline, _kIsReadOnlyIndex: isReadOnly, + _kIsFocusableIndex: isFocusable, + _kIsLinkIndex: isLink, + _kIsSliderIndex: isSlider, _kIsKeyboardKeyIndex: isKeyboardKey, }; @@ -229,12 +229,8 @@ class SemanticsFlag { return 'SemanticsFlag.isSelected'; case _kIsButtonIndex: return 'SemanticsFlag.isButton'; - case _kIsLinkIndex: - return 'SemanticsFlag.isLink'; case _kIsTextFieldIndex: return 'SemanticsFlag.isTextField'; - case _kIsFocusableIndex: - return 'SemanticsFlag.isFocusable'; case _kIsFocusedIndex: return 'SemanticsFlag.isFocused'; case _kHasEnabledStateIndex: @@ -267,20 +263,27 @@ class SemanticsFlag { return 'SemanticsFlag.isMultiline'; case _kIsReadOnlyIndex: return 'SemanticsFlag.isReadOnly'; + case _kIsFocusableIndex: + return 'SemanticsFlag.isFocusable'; + case _kIsLinkIndex: + return 'SemanticsFlag.isLink'; + case _kIsSliderIndex: + return 'SemanticsFlag.isSlider'; case _kIsKeyboardKeyIndex: return 'SemanticsFlag.isKeyboardKey'; } - assert(false, 'Unhandled index: $index'); + assert(false, 'Unhandled index: $index (0x${index.toRadixString(8).padLeft(4, "0")})'); return ''; } } - // When adding a new StringAttributeType, the classes in these file must be // updated as well. // * engine/src/flutter/lib/ui/semantics.dart // * engine/src/flutter/lib/ui/semantics/string_attribute.h // * engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java +// * engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_api_test.dart +// * engine/src/flutter/testing/dart/semantics_test.dart abstract class StringAttribute { StringAttribute._({ @@ -304,7 +307,7 @@ class SpellOutStringAttribute extends StringAttribute { @override String toString() { - return 'SpellOutStringAttribute(range: $range)'; + return 'SpellOutStringAttribute($range)'; } } @@ -323,7 +326,7 @@ class LocaleStringAttribute extends StringAttribute { @override String toString() { - return 'LocaleStringAttribute(range: $range, locale: ${locale.toLanguageTag()})'; + return 'LocaleStringAttribute($range, ${locale.toLanguageTag()})'; } } diff --git a/lib/web_ui/pubspec.yaml b/lib/web_ui/pubspec.yaml index 64046a798235d..d399ce7bdfcfb 100644 --- a/lib/web_ui/pubspec.yaml +++ b/lib/web_ui/pubspec.yaml @@ -25,6 +25,6 @@ dev_dependencies: path: ../../web_sdk/web_engine_tester simulators: git: - url: git://github.com/flutter/web_installers.git + url: https://github.com/flutter/web_installers.git path: packages/simulators/ ref: 4a7b0a2c84b8993bf4d19030218de38c18838c26 diff --git a/lib/web_ui/test/canvaskit/canvas_golden_test.dart b/lib/web_ui/test/canvaskit/canvas_golden_test.dart index 414be1ebc793f..4688346254ab6 100644 --- a/lib/web_ui/test/canvaskit/canvas_golden_test.dart +++ b/lib/web_ui/test/canvaskit/canvas_golden_test.dart @@ -27,18 +27,6 @@ void main() { const ui.Rect kDefaultRegion = ui.Rect.fromLTRB(0, 0, 500, 250); -Future matchPictureGolden(String goldenFile, CkPicture picture, - {ui.Rect region = kDefaultRegion, bool write = false}) async { - final EnginePlatformDispatcher dispatcher = - ui.window.platformDispatcher as EnginePlatformDispatcher; - final LayerSceneBuilder sb = LayerSceneBuilder(); - sb.pushOffset(0, 0); - sb.addPicture(ui.Offset.zero, picture); - dispatcher.rasterizer!.draw(sb.build().layerTree); - await matchGoldenFile(goldenFile, - region: region, maxDiffRatePercent: 0.0, write: write); -} - void testMain() { group('CkCanvas', () { setUpCanvasKitTest(); @@ -63,7 +51,10 @@ void testMain() { expect(canvas.runtimeType, CkCanvas); drawTestPicture(canvas); await matchPictureGolden( - 'canvaskit_picture.png', recorder.endRecording()); + 'canvaskit_picture.png', + recorder.endRecording(), + region: kDefaultRegion, + ); // Safari does not support weak refs (FinalizationRegistry). // This test should be revisited when Safari ships weak refs. // TODO(yjbanov): skip Firefox due to a crash: https://github.com/flutter/flutter/issues/86632 @@ -78,7 +69,7 @@ void testMain() { drawTestPicture(canvas); final CkPicture originalPicture = recorder.endRecording(); - await matchPictureGolden('canvaskit_picture.png', originalPicture); + await matchPictureGolden('canvaskit_picture.png', originalPicture, region: kDefaultRegion); final ByteData originalPixels = (await (await originalPicture.toImage(50, 50)).toByteData())!; @@ -93,7 +84,7 @@ void testMain() { final ByteData restoredPixels = (await (await restoredPicture.toImage(50, 50)).toByteData())!; - await matchPictureGolden('canvaskit_picture.png', restoredPicture); + await matchPictureGolden('canvaskit_picture.png', restoredPicture, region: kDefaultRegion); expect(restoredPixels.buffer.asUint8List(), originalPixels.buffer.asUint8List()); }); @@ -140,11 +131,7 @@ void testMain() { ); canvas.drawRect(psl.paintBounds, shadowBoundsPaint); - final CkParagraphBuilder pb = CkParagraphBuilder( - CkParagraphStyle(), - ); - pb.addText('$elevation'); - final CkParagraph p = pb.build(); + final CkParagraph p = makeSimpleText('$elevation'); p.layout(const ui.ParagraphConstraints(width: 1000)); canvas.drawParagraph( p, ui.Offset(20 - p.maxIntrinsicWidth / 2, 20 - p.height / 2)); @@ -1137,19 +1124,7 @@ void drawTestPicture(CkCanvas canvas) { canvas.restore(); canvas.translate(60, 0); - final CkParagraphBuilder pb = CkParagraphBuilder(CkParagraphStyle( - fontFamily: 'Roboto', - fontStyle: ui.FontStyle.normal, - fontWeight: ui.FontWeight.normal, - fontSize: 18, - )); - pb.pushStyle(CkTextStyle( - color: const ui.Color(0xFF0000AA), - )); - pb.addText('Hello'); - pb.pop(); - final CkParagraph p = pb.build(); - p.layout(const ui.ParagraphConstraints(width: 1000)); + final CkParagraph p = makeSimpleText('Hello', fontSize: 18, color: const ui.Color(0xFF0000AA)); canvas.drawParagraph( p, const ui.Offset(10, 20), @@ -1192,7 +1167,7 @@ CkImage generateTestImage() { colorSpace: SkColorSpaceSRGB, ), imageData, - 4 * 20); + 4 * 20)!; return CkImage(skImage); } diff --git a/lib/web_ui/test/canvaskit/common.dart b/lib/web_ui/test/canvaskit/common.dart index 32f4f01d6a158..f062719bce2da 100644 --- a/lib/web_ui/test/canvaskit/common.dart +++ b/lib/web_ui/test/canvaskit/common.dart @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; +import 'package:web_engine_tester/golden_tester.dart'; export '../common.dart'; @@ -15,6 +18,8 @@ export '../common.dart'; /// See [TestCollector] for usage. late TestCollector testCollector; +const MethodCodec codec = StandardMethodCodec(); + /// Common test setup for all CanvasKit unit-tests. void setUpCanvasKitTest() { setUpAll(() async { @@ -170,3 +175,73 @@ class TestCollector implements Collector { _completedCollections.clear(); } } + +/// Checks that a [picture] matches the [goldenFile]. +/// +/// The picture is drawn onto the UI at [ui.Offset.zero] with no additional +/// layers. +Future matchPictureGolden(String goldenFile, CkPicture picture, + {required ui.Rect region, bool write = false}) async { + final EnginePlatformDispatcher dispatcher = + ui.window.platformDispatcher as EnginePlatformDispatcher; + final LayerSceneBuilder sb = LayerSceneBuilder(); + sb.pushOffset(0, 0); + sb.addPicture(ui.Offset.zero, picture); + dispatcher.rasterizer!.draw(sb.build().layerTree); + await matchGoldenFile(goldenFile, + region: region, maxDiffRatePercent: 0.0, write: write); +} + +/// Sends a platform message to create a Platform View with the given id and viewType. +Future createPlatformView(int id, String viewType) { + final Completer completer = Completer(); + window.sendPlatformMessage( + 'flutter/platform_views', + codec.encodeMethodCall(MethodCall( + 'create', + { + 'id': id, + 'viewType': viewType, + }, + )), + (dynamic _) => completer.complete(), + ); + return completer.future; +} + +/// Disposes of the platform view with the given [id]. +Future disposePlatformView(int id) { + final Completer completer = Completer(); + window.sendPlatformMessage( + 'flutter/platform_views', + codec.encodeMethodCall(MethodCall('dispose', id)), + (dynamic _) => completer.complete(), + ); + return completer.future; +} + +/// Creates a pre-laid out one-line paragraph of text. +/// +/// Useful in tests that need a simple label to annotate goldens. +CkParagraph makeSimpleText(String text, { + String? fontFamily, + double? fontSize, + ui.FontStyle? fontStyle, + ui.FontWeight? fontWeight, + ui.Color? color, +}) { + final CkParagraphBuilder builder = CkParagraphBuilder(CkParagraphStyle( + fontFamily: fontFamily ?? 'Roboto', + fontSize: fontSize ?? 14, + fontStyle: fontStyle ?? ui.FontStyle.normal, + fontWeight: fontWeight ?? ui.FontWeight.normal, + )); + builder.pushStyle(CkTextStyle( + color: color ?? const ui.Color(0xFF000000), + )); + builder.addText(text); + builder.pop(); + final CkParagraph paragraph = builder.build(); + paragraph.layout(const ui.ParagraphConstraints(width: 10000)); + return paragraph; +} diff --git a/lib/web_ui/test/canvaskit/embedded_views_test.dart b/lib/web_ui/test/canvaskit/embedded_views_test.dart index 7c944c152dcc2..b3f03f5a483dd 100644 --- a/lib/web_ui/test/canvaskit/embedded_views_test.dart +++ b/lib/web_ui/test/canvaskit/embedded_views_test.dart @@ -12,8 +12,6 @@ import 'package:ui/ui.dart' as ui; import 'common.dart'; -const MethodCodec codec = StandardMethodCodec(); - void main() { internalBootstrapBrowserTest(() => testMain); } @@ -31,7 +29,7 @@ void testMain() { 'test-platform-view', (int viewId) => html.DivElement()..id = 'view-0', ); - await _createPlatformView(0, 'test-platform-view'); + await createPlatformView(0, 'test-platform-view'); final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher; @@ -67,7 +65,7 @@ void testMain() { 'test-platform-view', (int viewId) => html.DivElement()..id = 'view-0', ); - await _createPlatformView(0, 'test-platform-view'); + await createPlatformView(0, 'test-platform-view'); final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher; @@ -105,7 +103,7 @@ void testMain() { 'test-platform-view', (int viewId) => html.DivElement()..id = 'view-0', ); - await _createPlatformView(0, 'test-platform-view'); + await createPlatformView(0, 'test-platform-view'); final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher; @@ -150,7 +148,7 @@ void testMain() { 'test-platform-view', (int viewId) => html.DivElement()..id = 'view-0', ); - await _createPlatformView(0, 'test-platform-view'); + await createPlatformView(0, 'test-platform-view'); final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher; @@ -177,7 +175,7 @@ void testMain() { 'test-platform-view', (int viewId) => html.DivElement()..id = 'view-0', ); - await _createPlatformView(0, 'test-platform-view'); + await createPlatformView(0, 'test-platform-view'); final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher; @@ -213,12 +211,12 @@ void testMain() { // Initialize all platform views to be used in the test. final List platformViewIds = []; - for (int i = 0; i < HtmlViewEmbedder.maximumSurfaces * 2; i++) { + for (int i = 0; i < configuration.canvasKitMaximumSurfaces * 2; i++) { ui.platformViewRegistry.registerViewFactory( 'test-platform-view', (int viewId) => html.DivElement()..id = 'view-$i', ); - await _createPlatformView(i, 'test-platform-view'); + await createPlatformView(i, 'test-platform-view'); platformViewIds.add(i); } @@ -242,8 +240,8 @@ void testMain() { // Frame 1: // Render: up to cache size platform views. // Expect: main canvas plus platform view overlays. - renderTestScene(viewCount: HtmlViewEmbedder.maximumSurfaces); - expect(countCanvases(), HtmlViewEmbedder.maximumSurfaces); + renderTestScene(viewCount: configuration.canvasKitMaximumSurfaces); + expect(countCanvases(), configuration.canvasKitMaximumSurfaces); // Frame 2: // Render: zero platform views. @@ -256,15 +254,15 @@ void testMain() { // Render: less than cache size platform views. // Expect: overlays reused. await Future.delayed(Duration.zero); - renderTestScene(viewCount: HtmlViewEmbedder.maximumSurfaces - 2); - expect(countCanvases(), HtmlViewEmbedder.maximumSurfaces - 1); + renderTestScene(viewCount: configuration.canvasKitMaximumSurfaces - 2); + expect(countCanvases(), configuration.canvasKitMaximumSurfaces - 1); // Frame 4: // Render: more platform views than max cache size. // Expect: main canvas, backup overlay, maximum overlays. await Future.delayed(Duration.zero); - renderTestScene(viewCount: HtmlViewEmbedder.maximumSurfaces * 2); - expect(countCanvases(), HtmlViewEmbedder.maximumSurfaces); + renderTestScene(viewCount: configuration.canvasKitMaximumSurfaces * 2); + expect(countCanvases(), configuration.canvasKitMaximumSurfaces); // Frame 5: // Render: zero platform views. @@ -303,7 +301,7 @@ void testMain() { // Frame 7: // Render: a platform view after error. // Expect: success. Just checking the system is not left in a corrupted state. - await _createPlatformView(0, 'test-platform-view'); + await createPlatformView(0, 'test-platform-view'); renderTestScene(viewCount: 0); // TODO(yjbanov): skipped due to https://github.com/flutter/flutter/issues/73867 }, skip: isSafari); @@ -321,7 +319,7 @@ void testMain() { 'test-platform-view', (int viewId) => html.DivElement()..id = 'view-$i', ); - await _createPlatformView(i, 'test-platform-view'); + await createPlatformView(i, 'test-platform-view'); platformViewIds.add(i); } @@ -346,21 +344,21 @@ void testMain() { // Render: Views 1-10 // Expect: main canvas plus platform view overlays; empty cache. renderTestScene([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - expect(countCanvases(), HtmlViewEmbedder.maximumSurfaces); + expect(countCanvases(), configuration.canvasKitMaximumSurfaces); // Frame 2: // Render: Views 2-11 // Expect: main canvas plus platform view overlays; empty cache. await Future.delayed(Duration.zero); renderTestScene([2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - expect(countCanvases(), HtmlViewEmbedder.maximumSurfaces); + expect(countCanvases(), configuration.canvasKitMaximumSurfaces); // Frame 3: // Render: Views 3-12 // Expect: main canvas plus platform view overlays; empty cache. await Future.delayed(Duration.zero); renderTestScene([3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - expect(countCanvases(), HtmlViewEmbedder.maximumSurfaces); + expect(countCanvases(), configuration.canvasKitMaximumSurfaces); // TODO(yjbanov): skipped due to https://github.com/flutter/flutter/issues/73867 }, skip: isSafari); @@ -370,7 +368,7 @@ void testMain() { 'test-platform-view', (int viewId) => html.DivElement()..id = 'view-0', ); - await _createPlatformView(0, 'test-platform-view'); + await createPlatformView(0, 'test-platform-view'); final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher; @@ -389,7 +387,7 @@ void testMain() { isNotNull, ); - await _disposePlatformView(0); + await disposePlatformView(0); sb = LayerSceneBuilder(); sb.pushOffset(0, 0); @@ -410,7 +408,7 @@ void testMain() { 'test-platform-view', (int viewId) => html.DivElement()..id = 'view-0', ); - await _createPlatformView(0, 'test-platform-view'); + await createPlatformView(0, 'test-platform-view'); final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher; @@ -430,7 +428,7 @@ void testMain() { ); // Render a frame with a different platform view. - await _createPlatformView(1, 'test-platform-view'); + await createPlatformView(1, 'test-platform-view'); sb = LayerSceneBuilder(); sb.pushOffset(0, 0); sb.addPlatformView(1, width: 10, height: 10); @@ -468,7 +466,7 @@ void testMain() { 'test-platform-view', (int viewId) => html.DivElement()..id = 'test-view', ); - await _createPlatformView(0, 'test-platform-view'); + await createPlatformView(0, 'test-platform-view'); final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher; @@ -561,7 +559,7 @@ void testMain() { 'test-platform-view', (int viewId) => html.DivElement()..id = 'view-0', ); - await _createPlatformView(0, 'test-platform-view'); + await createPlatformView(0, 'test-platform-view'); final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher; @@ -585,7 +583,7 @@ void testMain() { 'test-platform-view', (int viewId) => html.DivElement()..id = 'view-0', ); - await _createPlatformView(0, 'test-platform-view'); + await createPlatformView(0, 'test-platform-view'); final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher; @@ -611,8 +609,8 @@ void testMain() { 'test-platform-view', (int viewId) => html.DivElement()..id = 'view-0', ); - await _createPlatformView(0, 'test-platform-view'); - await _createPlatformView(1, 'test-platform-view'); + await createPlatformView(0, 'test-platform-view'); + await createPlatformView(1, 'test-platform-view'); final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher; @@ -646,30 +644,3 @@ void testMain() { // TODO(dit): https://github.com/flutter/flutter/issues/60040 }, skip: isIosSafari); } - -// Sends a platform message to create a Platform View with the given id and viewType. -Future _createPlatformView(int id, String viewType) { - final Completer completer = Completer(); - window.sendPlatformMessage( - 'flutter/platform_views', - codec.encodeMethodCall(MethodCall( - 'create', - { - 'id': id, - 'viewType': viewType, - }, - )), - (dynamic _) => completer.complete(), - ); - return completer.future; -} - -Future _disposePlatformView(int id) { - final Completer completer = Completer(); - window.sendPlatformMessage( - 'flutter/platform_views', - codec.encodeMethodCall(MethodCall('dispose', id)), - (dynamic _) => completer.complete(), - ); - return completer.future; -} diff --git a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart index 7cc2c57aa4fe2..6bba90a2ae152 100644 --- a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart +++ b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart @@ -12,8 +12,6 @@ import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import 'package:web_engine_tester/golden_tester.dart'; - import 'common.dart'; void main() { @@ -22,18 +20,6 @@ void main() { const ui.Rect kDefaultRegion = ui.Rect.fromLTRB(0, 0, 100, 50); -Future matchPictureGolden(String goldenFile, CkPicture picture, - {ui.Rect region = kDefaultRegion, bool write = false}) async { - final EnginePlatformDispatcher dispatcher = - ui.window.platformDispatcher as EnginePlatformDispatcher; - final LayerSceneBuilder sb = LayerSceneBuilder(); - sb.pushOffset(0, 0); - sb.addPicture(ui.Offset.zero, picture); - dispatcher.rasterizer!.draw(sb.build().layerTree); - await matchGoldenFile(goldenFile, - region: region, maxDiffRatePercent: 0.0, write: write); -} - void testMain() { group('Font fallbacks', () { setUpCanvasKitTest(); @@ -65,7 +51,7 @@ void testMain() { font-family: 'Noto Naskh Arabic UI'; font-style: normal; font-weight: 400; - src: url(packages/ui/assets/NotoNaskhArabic-Regular.ttf) format('ttf'); + src: url(/assets/fonts/NotoNaskhArabic-Regular.ttf) format('ttf'); unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC; } '''; @@ -101,7 +87,10 @@ void testMain() { canvas.drawParagraph(paragraph, const ui.Offset(0, 0)); await matchPictureGolden( - 'canvaskit_font_fallback_arabic.png', recorder.endRecording()); + 'canvaskit_font_fallback_arabic.png', + recorder.endRecording(), + region: kDefaultRegion, + ); // TODO(hterkelsen): https://github.com/flutter/flutter/issues/60040 // TODO(hterkelsen): https://github.com/flutter/flutter/issues/71520 }, skip: isIosSafari || isFirefox); @@ -113,7 +102,7 @@ void testMain() { ''' @font-face { font-family: 'Noto Color Emoji'; - src: url(packages/ui/assets/NotoColorEmoji.ttf) format('ttf'); + src: url(/assets/fonts/NotoColorEmoji.ttf) format('ttf'); } '''; @@ -125,7 +114,7 @@ void testMain() { font-family: 'Noto Naskh Arabic UI'; font-style: normal; font-weight: 400; - src: url(packages/ui/assets/NotoNaskhArabic-Regular.ttf) format('ttf'); + src: url(/assets/fonts/NotoNaskhArabic-Regular.ttf) format('ttf'); unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC; } '''; @@ -173,7 +162,7 @@ void testMain() { ''' @font-face { font-family: 'Noto Color Emoji'; - src: url(packages/ui/assets/NotoColorEmoji.ttf) format('ttf'); + src: url(/assets/fonts/NotoColorEmoji.ttf) format('ttf'); } '''; @@ -208,7 +197,10 @@ void testMain() { canvas.drawParagraph(paragraph, const ui.Offset(0, 0)); await matchPictureGolden( - 'canvaskit_font_fallback_emoji.png', recorder.endRecording()); + 'canvaskit_font_fallback_emoji.png', + recorder.endRecording(), + region: kDefaultRegion, + ); // TODO(hterkelsen): https://github.com/flutter/flutter/issues/60040 // TODO(hterkelsen): https://github.com/flutter/flutter/issues/71520 }, skip: isIosSafari || isFirefox); diff --git a/lib/web_ui/test/canvaskit/image_golden_test.dart b/lib/web_ui/test/canvaskit/image_golden_test.dart new file mode 100644 index 0000000000000..479e8262a9d8d --- /dev/null +++ b/lib/web_ui/test/canvaskit/image_golden_test.dart @@ -0,0 +1,648 @@ +// 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:async'; +import 'dart:html' as html; +import 'dart:typed_data'; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; +import 'package:web_engine_tester/golden_tester.dart'; + +import '../matchers.dart'; +import 'common.dart'; +import 'test_data.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('CanvasKit Images', () { + setUpCanvasKitTest(); + + tearDown(() { + debugRestoreHttpRequestFactory(); + }); + + _testForImageCodecs(useBrowserImageDecoder: false); + + if (browserSupportsImageDecoder) { + _testForImageCodecs(useBrowserImageDecoder: true); + } + + test('isAvif', () { + expect(isAvif(Uint8List.fromList([])), isFalse); + expect(isAvif(Uint8List.fromList([1, 2, 3])), isFalse); + expect( + isAvif(Uint8List.fromList([ + 0x00, 0x00, 0x00, 0x1c, 0x66, 0x74, 0x79, 0x70, + 0x61, 0x76, 0x69, 0x66, 0x00, 0x00, 0x00, 0x00, + ])), + isTrue, + ); + expect( + isAvif(Uint8List.fromList([ + 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, + 0x61, 0x76, 0x69, 0x66, 0x00, 0x00, 0x00, 0x00, + ])), + isTrue, + ); + }); + // TODO(hterkelsen): https://github.com/flutter/flutter/issues/60040 + }, skip: isIosSafari); +} + +void _testForImageCodecs({required bool useBrowserImageDecoder}) { + final String mode = useBrowserImageDecoder ? 'webcodecs' : 'wasm'; + + group('($mode})', () { + setUp(() { + browserSupportsImageDecoder = useBrowserImageDecoder; + }); + + tearDown(() { + debugResetBrowserSupportsImageDecoder(); + }); + + test('CkAnimatedImage can be explicitly disposed of', () { + final CkAnimatedImage image = CkAnimatedImage.decodeFromBytes(kTransparentImage, 'test'); + expect(image.debugDisposed, isFalse); + image.dispose(); + expect(image.debugDisposed, isTrue); + + // Disallow usage after disposal + expect(() => image.frameCount, throwsAssertionError); + expect(() => image.repetitionCount, throwsAssertionError); + expect(() => image.getNextFrame(), throwsAssertionError); + + // Disallow double-dispose. + expect(() => image.dispose(), throwsAssertionError); + testCollector.collectNow(); + }); + + test('CkAnimatedImage remembers last animation position after resurrection', () async { + browserSupportsFinalizationRegistry = false; + + Future expectFrameData(ui.FrameInfo frame, List data) async { + final ByteData frameData = (await frame.image.toByteData())!; + expect(frameData.buffer.asUint8List(), Uint8List.fromList(data)); + } + + final CkAnimatedImage image = CkAnimatedImage.decodeFromBytes(kAnimatedGif, 'test'); + expect(image.frameCount, 3); + expect(image.repetitionCount, -1); + + final ui.FrameInfo frame1 = await image.getNextFrame(); + expectFrameData(frame1, [0, 255, 0, 255]); + final ui.FrameInfo frame2 = await image.getNextFrame(); + expectFrameData(frame2, [0, 0, 255, 255]); + + // Pretend that the image is temporarily deleted. + image.delete(); + image.didDelete(); + + // Check that we got the 3rd frame after resurrection. + final ui.FrameInfo frame3 = await image.getNextFrame(); + expectFrameData(frame3, [255, 0, 0, 255]); + + testCollector.collectNow(); + }); + + test('CkImage toString', () { + final SkImage skImage = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)! + .makeImageAtCurrentFrame(); + final CkImage image = CkImage(skImage); + expect(image.toString(), '[1×1]'); + image.dispose(); + testCollector.collectNow(); + }); + + test('CkImage can be explicitly disposed of', () { + final SkImage skImage = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)! + .makeImageAtCurrentFrame(); + final CkImage image = CkImage(skImage); + expect(image.debugDisposed, isFalse); + expect(image.box.isDeletedPermanently, isFalse); + image.dispose(); + expect(image.debugDisposed, isTrue); + expect(image.box.isDeletedPermanently, isTrue); + + // Disallow double-dispose. + expect(() => image.dispose(), throwsAssertionError); + testCollector.collectNow(); + }); + + test('CkImage can be explicitly disposed of when cloned', () async { + final SkImage skImage = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)! + .makeImageAtCurrentFrame(); + final CkImage image = CkImage(skImage); + final SkiaObjectBox box = image.box; + expect(box.refCount, 1); + expect(box.debugGetStackTraces().length, 1); + + final CkImage clone = image.clone(); + expect(box.refCount, 2); + expect(box.debugGetStackTraces().length, 2); + + expect(image.isCloneOf(clone), isTrue); + expect(box.isDeletedPermanently, isFalse); + + testCollector.collectNow(); + expect(skImage.isDeleted(), isFalse); + image.dispose(); + expect(box.refCount, 1); + expect(box.isDeletedPermanently, isFalse); + + testCollector.collectNow(); + expect(skImage.isDeleted(), isFalse); + clone.dispose(); + expect(box.refCount, 0); + expect(box.isDeletedPermanently, isTrue); + + testCollector.collectNow(); + expect(skImage.isDeleted(), isTrue); + expect(box.debugGetStackTraces().length, 0); + testCollector.collectNow(); + }); + + test('CkImage toByteData', () async { + final SkImage skImage = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)! + .makeImageAtCurrentFrame(); + final CkImage image = CkImage(skImage); + expect((await image.toByteData()).lengthInBytes, greaterThan(0)); + expect((await image.toByteData(format: ui.ImageByteFormat.png)).lengthInBytes, greaterThan(0)); + testCollector.collectNow(); + }); + + // Regression test for https://github.com/flutter/flutter/issues/72469 + test('CkImage can be resurrected', () { + browserSupportsFinalizationRegistry = false; + final SkImage skImage = + canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)! + .makeImageAtCurrentFrame(); + final CkImage image = CkImage(skImage); + expect(image.box.rawSkiaObject, isNotNull); + + // Pretend that the image is temporarily deleted. + image.box.delete(); + image.box.didDelete(); + expect(image.box.rawSkiaObject, isNull); + + // Attempting to access the skia object here would previously throw + // "Stack Overflow" in Safari. + expect(image.box.skiaObject, isNotNull); + testCollector.collectNow(); + }); + + test('skiaInstantiateWebImageCodec loads an image from the network', + () async { + httpRequestFactory = () { + return TestHttpRequest() + ..status = 200 + ..onLoad = Stream.fromIterable([ + html.ProgressEvent('test progress event'), + ]) + ..response = kTransparentImage.buffer; + }; + final ui.Codec codec = await skiaInstantiateWebImageCodec('http://image-server.com/picture.jpg', null); + expect(codec.frameCount, 1); + final ui.Image image = (await codec.getNextFrame()).image; + expect(image.height, 1); + expect(image.width, 1); + testCollector.collectNow(); + }); + + test('instantiateImageCodec respects target image size', + () async { + const List> targetSizes = >[ + [1, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 4], + [10, 20], + ]; + + for (final List targetSize in targetSizes) { + final int targetWidth = targetSize[0]; + final int targetHeight = targetSize[1]; + + final ui.Codec codec = await ui.instantiateImageCodec( + k4x4PngImage, + targetWidth: targetWidth, + targetHeight: targetHeight, + ); + + final ui.Image image = (await codec.getNextFrame()).image; + // TODO(yjbanov): https://github.com/flutter/flutter/issues/34075 + // expect(image.width, targetWidth); + // expect(image.height, targetHeight); + image.dispose(); + codec.dispose(); + } + + testCollector.collectNow(); + }); + + test('skiaInstantiateWebImageCodec throws exception on request error', + () async { + httpRequestFactory = () { + return TestHttpRequest() + ..onError = Stream.fromIterable([ + html.ProgressEvent('test error'), + ]); + }; + try { + await skiaInstantiateWebImageCodec('url-does-not-matter', null); + fail('Expected to throw'); + } on ImageCodecException catch (exception) { + expect( + exception.toString(), + 'ImageCodecException: Failed to load network image.\n' + 'Image URL: url-does-not-matter\n' + 'Trying to load an image from another domain? Find answers at:\n' + 'https://flutter.dev/docs/development/platform-integration/web-images', + ); + } + testCollector.collectNow(); + }); + + test('skiaInstantiateWebImageCodec throws exception on HTTP error', + () async { + try { + await skiaInstantiateWebImageCodec('/does-not-exist.jpg', null); + fail('Expected to throw'); + } on ImageCodecException catch (exception) { + expect( + exception.toString(), + 'ImageCodecException: Failed to load network image.\n' + 'Image URL: /does-not-exist.jpg\n' + 'Server response code: 404', + ); + } + testCollector.collectNow(); + }); + + test('skiaInstantiateWebImageCodec includes URL in the error for malformed image', + () async { + httpRequestFactory = () { + return TestHttpRequest() + ..status = 200 + ..onLoad = Stream.fromIterable([ + html.ProgressEvent('test progress event'), + ]) + ..response = Uint8List(0).buffer; + }; + try { + await skiaInstantiateWebImageCodec('http://image-server.com/picture.jpg', null); + fail('Expected to throw'); + } on ImageCodecException catch (exception) { + if (!browserSupportsImageDecoder) { + expect( + exception.toString(), + 'ImageCodecException: Failed to decode image data.\n' + 'Image source: http://image-server.com/picture.jpg', + ); + } else { + expect( + exception.toString(), + 'ImageCodecException: Failed to detect image file format using the file header.\n' + 'File header was empty.\n' + 'Image source: http://image-server.com/picture.jpg', + ); + } + } + testCollector.collectNow(); + }); + + test('Reports error when failing to decode empty image data', () async { + try { + await ui.instantiateImageCodec(Uint8List(0)); + fail('Expected to throw'); + } on ImageCodecException catch (exception) { + if (!browserSupportsImageDecoder) { + expect( + exception.toString(), + 'ImageCodecException: Failed to decode image data.\n' + 'Image source: encoded image bytes', + ); + } else { + expect( + exception.toString(), + 'ImageCodecException: Failed to detect image file format using the file header.\n' + 'File header was empty.\n' + 'Image source: encoded image bytes', + ); + } + } + }); + + test('Reports error when failing to decode malformed image data', () async { + try { + await ui.instantiateImageCodec(Uint8List.fromList([ + 0xFF, 0xD8, 0xFF, 0xDB, 0x00, 0x00, 0x00, + ])); + fail('Expected to throw'); + } on ImageCodecException catch (exception) { + if (!browserSupportsImageDecoder) { + expect( + exception.toString(), + 'ImageCodecException: Failed to decode image data.\n' + 'Image source: encoded image bytes' + ); + } else { + expect( + exception.toString(), + // Browser error message is not checked as it can depend on the + // browser engine and version. + matches(RegExp( + r"ImageCodecException: Failed to decode image using the browser's ImageDecoder API.\n" + r'Image source: encoded image bytes\n' + r'Original browser error: .+' + )) + ); + } + } + }); + + test('Includes file header in the error message when fails to detect file type', () async { + try { + await ui.instantiateImageCodec(Uint8List.fromList([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, + ])); + fail('Expected to throw'); + } on ImageCodecException catch (exception) { + if (!browserSupportsImageDecoder) { + expect( + exception.toString(), + 'ImageCodecException: Failed to decode image data.\n' + 'Image source: encoded image bytes' + ); + } else { + expect( + exception.toString(), + 'ImageCodecException: Failed to detect image file format using the file header.\n' + 'File header was [0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x00].\n' + 'Image source: encoded image bytes' + ); + } + } + }); + + test('Provides readable error message when image type is unsupported', () async { + addTearDown(() { + debugContentTypeDetector = null; + }); + debugContentTypeDetector = (_) { + return 'unsupported/image-type'; + }; + try { + await ui.instantiateImageCodec(Uint8List.fromList([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, + ])); + fail('Expected to throw'); + } on ImageCodecException catch (exception) { + if (!browserSupportsImageDecoder) { + expect( + exception.toString(), + 'ImageCodecException: Failed to decode image data.\n' + 'Image source: encoded image bytes' + ); + } else { + expect( + exception.toString(), + 'ImageCodecException: Image file format (unsupported/image-type) is not supported by this browser\'s ImageDecoder API.\n' + 'Image source: encoded image bytes' + ); + } + } + }); + + test('decodeImageFromPixels', () async { + Future _testDecodeFromPixels(int width, int height) async { + final Completer completer = Completer(); + ui.decodeImageFromPixels( + Uint8List.fromList(List.filled(width * height * 4, 0, growable: false)), + width, + height, + ui.PixelFormat.rgba8888, + (ui.Image image) { + completer.complete(image); + }, + ); + return completer.future; + } + + final ui.Image image1 = await _testDecodeFromPixels(10, 20); + expect(image1, isNotNull); + expect(image1.width, 10); + expect(image1.height, 20); + + final ui.Image image2 = await _testDecodeFromPixels(40, 100); + expect(image2, isNotNull); + expect(image2.width, 40); + expect(image2.height, 100); + }); + + test('Decode test images', () async { + final html.Body listingResponse = await httpFetch('/test_images/'); + final List testFiles = (await listingResponse.json() as List).cast(); + + // Sanity-check the test file list. If suddenly test files are moved or + // deleted, and the test server returns an empty list, or is missing some + // important test files, we want to know. + expect(testFiles, isNotEmpty); + expect(testFiles, contains(matches(RegExp(r'.*\.jpg')))); + expect(testFiles, contains(matches(RegExp(r'.*\.png')))); + expect(testFiles, contains(matches(RegExp(r'.*\.gif')))); + expect(testFiles, contains(matches(RegExp(r'.*\.webp')))); + expect(testFiles, contains(matches(RegExp(r'.*\.bmp')))); + + for (final String testFile in testFiles) { + final html.Body imageResponse = await httpFetch('/test_images/$testFile'); + final Uint8List imageData = (await imageResponse.arrayBuffer() as ByteBuffer).asUint8List(); + final ui.Codec codec = await skiaInstantiateImageCodec(imageData); + expect(codec.frameCount, greaterThan(0)); + expect(codec.repetitionCount, isNotNull); + for (int i = 0; i < codec.frameCount; i++) { + final ui.FrameInfo frame = await codec.getNextFrame(); + expect(frame.duration, isNotNull); + expect(frame.image, isNotNull); + } + codec.dispose(); + } + }); + + // This is a regression test for the issues with transferring textures from + // one GL context to another, such as: + // + // * https://github.com/flutter/flutter/issues/86809 + // * https://github.com/flutter/flutter/issues/91881 + test('the same image can be rendered on difference surfaces', () async { + ui.platformViewRegistry.registerViewFactory( + 'test-platform-view', + (int viewId) => html.DivElement()..id = 'view-0', + ); + await createPlatformView(0, 'test-platform-view'); + + final EnginePlatformDispatcher dispatcher = + ui.window.platformDispatcher as EnginePlatformDispatcher; + + final ui.Codec codec = await ui.instantiateImageCodec(k4x4PngImage); + final CkImage image = (await codec.getNextFrame()).image as CkImage; + + final LayerSceneBuilder sb = LayerSceneBuilder(); + sb.pushOffset(4, 4); + { + final CkPictureRecorder recorder = CkPictureRecorder(); + final CkCanvas canvas = recorder.beginRecording(ui.Rect.largest); + canvas.save(); + canvas.scale(16, 16); + canvas.drawImage(image, ui.Offset.zero, CkPaint()); + canvas.restore(); + canvas.drawParagraph(makeSimpleText('1'), const ui.Offset(4, 4)); + sb.addPicture(ui.Offset.zero, recorder.endRecording()); + } + sb.addPlatformView(0, width: 100, height: 100); + sb.pushOffset(20, 20); + { + final CkPictureRecorder recorder = CkPictureRecorder(); + final CkCanvas canvas = recorder.beginRecording(ui.Rect.largest); + canvas.save(); + canvas.scale(16, 16); + canvas.drawImage(image, ui.Offset.zero, CkPaint()); + canvas.restore(); + canvas.drawParagraph(makeSimpleText('2'), const ui.Offset(2, 2)); + sb.addPicture(ui.Offset.zero, recorder.endRecording()); + } + dispatcher.rasterizer!.draw(sb.build().layerTree); + await matchGoldenFile( + 'canvaskit_cross_gl_context_image_$mode.png', + region: const ui.Rect.fromLTRB(0, 0, 100, 100), + maxDiffRatePercent: 0, + ); + + await disposePlatformView(0); + }); + }); +} + +class TestHttpRequest implements html.HttpRequest { + @override + String responseType = 'invalid'; + + @override + int? timeout = 10; + + @override + bool? withCredentials = false; + + @override + void abort() { + throw UnimplementedError(); + } + + @override + void addEventListener(String type, html.EventListener? listener, [bool? useCapture]) { + throw UnimplementedError(); + } + + @override + bool dispatchEvent(html.Event event) { + throw UnimplementedError(); + } + + @override + String getAllResponseHeaders() { + throw UnimplementedError(); + } + + @override + String getResponseHeader(String name) { + throw UnimplementedError(); + } + + @override + html.Events get on => throw UnimplementedError(); + + @override + Stream get onAbort => throw UnimplementedError(); + + @override + Stream onError = Stream.fromIterable([]); + + @override + Stream onLoad = Stream.fromIterable([]); + + @override + Stream get onLoadEnd => throw UnimplementedError(); + + @override + Stream get onLoadStart => throw UnimplementedError(); + + @override + Stream get onProgress => throw UnimplementedError(); + + @override + Stream get onReadyStateChange => throw UnimplementedError(); + + @override + Stream get onTimeout => throw UnimplementedError(); + + @override + void open(String method, String url, {bool? async, String? user, String? password}) {} + + @override + void overrideMimeType(String mime) { + throw UnimplementedError(); + } + + @override + int get readyState => throw UnimplementedError(); + + @override + void removeEventListener(String type, html.EventListener? listener, [bool? useCapture]) { + throw UnimplementedError(); + } + + @override + dynamic response; + + @override + Map get responseHeaders => throw UnimplementedError(); + + @override + String get responseText => throw UnimplementedError(); + + @override + String get responseUrl => throw UnimplementedError(); + + @override + html.Document get responseXml => throw UnimplementedError(); + + @override + void send([dynamic bodyOrData]) { + } + + @override + void setRequestHeader(String name, String value) { + throw UnimplementedError(); + } + + @override + int status = -1; + + @override + String get statusText => throw UnimplementedError(); + + @override + html.HttpRequestUpload get upload => throw UnimplementedError(); +} diff --git a/lib/web_ui/test/canvaskit/skia_font_collection_test.dart b/lib/web_ui/test/canvaskit/skia_font_collection_test.dart index b69a778d1ebb7..aacdafe7e10a6 100644 --- a/lib/web_ui/test/canvaskit/skia_font_collection_test.dart +++ b/lib/web_ui/test/canvaskit/skia_font_collection_test.dart @@ -51,7 +51,7 @@ void testMain() { [ { "family":"Roboto", - "fonts":[{"asset":"packages/ui/assets/Roboto-Regular.ttf"}] + "fonts":[{"asset":"/fonts/Roboto-Regular.ttf"}] }, { "family": "BrokenFont", diff --git a/lib/web_ui/test/canvaskit/test_data.dart b/lib/web_ui/test/canvaskit/test_data.dart index e13e62195bead..2e53c7366c16d 100644 --- a/lib/web_ui/test/canvaskit/test_data.dart +++ b/lib/web_ui/test/canvaskit/test_data.dart @@ -2,15 +2,24 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'dart:typed_data'; +/// A 1x1 fully transparent PNG image. final Uint8List kTransparentImage = Uint8List.fromList([ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, - 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, - 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, - 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, - 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x04, 0x00, 0x00, 0x00, 0xb5, 0x1c, 0x0c, + 0x02, 0x00, 0x00, 0x00, 0x0b, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x64, 0x60, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x02, 0x30, 0x81, 0xd0, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, + 0xae, 0x42, 0x60, 0x82, +]); + +/// A 4x4 PNG image sample. +final Uint8List k4x4PngImage = Uint8List.fromList([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x08, 0x06, 0x00, 0x00, 0x00, 0xa9, 0xf1, 0x9e, + 0x7e, 0x00, 0x00, 0x00, 0x13, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0xfc, 0xcf, 0xc0, 0x50, + 0xcf, 0x80, 0x04, 0x18, 0x49, 0x17, 0x00, 0x00, 0xf2, 0xae, 0x05, 0xfd, 0x52, 0x01, 0xc2, 0xde, + 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, ]); /// An animated GIF image with 3 1x1 pixel frames (a red, green, and blue diff --git a/lib/web_ui/test/engine/host_node_test.dart b/lib/web_ui/test/engine/host_node_test.dart index 2303650b683c6..a2364c86d01df 100644 --- a/lib/web_ui/test/engine/host_node_test.dart +++ b/lib/web_ui/test/engine/host_node_test.dart @@ -23,6 +23,15 @@ void testMain() { expect(hostNode.node, isA()); expect((hostNode.node as html.ShadowRoot).host, rootNode); expect(hostNode.node, rootNode.shadowRoot); + + // The shadow root should be initialized with correct parameters. + expect(rootNode.shadowRoot!.mode, 'open'); + if (browserEngine != BrowserEngine.firefox && + browserEngine != BrowserEngine.webkit) { + // Older versions of Safari and Firefox don't support this flag yet. + // See: https://caniuse.com/mdn-api_shadowroot_delegatesfocus + expect(rootNode.shadowRoot!.delegatesFocus, isFalse); + } }); test('Attaches a stylesheet to the shadow root', () { diff --git a/lib/web_ui/test/engine/platform_dispatcher_test.dart b/lib/web_ui/test/engine/platform_dispatcher_test.dart index eb2bcdd9ddb6f..7941a37bd2182 100644 --- a/lib/web_ui/test/engine/platform_dispatcher_test.dart +++ b/lib/web_ui/test/engine/platform_dispatcher_test.dart @@ -61,6 +61,7 @@ void testMain() { () async { // Patch browser so that clipboard api is not available. final dynamic originalClipboard = + // ignore: implicit_dynamic_function js_util.getProperty(html.window.navigator, 'clipboard'); js_util.setProperty(html.window.navigator, 'clipboard', null); const MethodCodec codec = JSONMethodCodec(); @@ -82,5 +83,34 @@ void testMain() { js_util.setProperty( html.window.navigator, 'clipboard', originalClipboard); }); + + test('can find text scale factor', () async { + const double deltaTolerance = 1e-5; + + final html.Element root = html.document.documentElement!; + final String oldFontSize = root.style.fontSize; + + addTearDown(() { + root.style.fontSize = oldFontSize; + }); + + root.style.fontSize = '16px'; + expect(findBrowserTextScaleFactor(), 1.0); + + root.style.fontSize = '20px'; + expect(findBrowserTextScaleFactor(), 1.25); + + root.style.fontSize = '24px'; + expect(findBrowserTextScaleFactor(), 1.5); + + root.style.fontSize = '14.4px'; + expect(findBrowserTextScaleFactor(), closeTo(0.9, deltaTolerance)); + + root.style.fontSize = '12.8px'; + expect(findBrowserTextScaleFactor(), closeTo(0.8, deltaTolerance)); + + root.style.fontSize = null; + expect(findBrowserTextScaleFactor(), 1.0); + }); }); } diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index 8840f0c5feb9b..c804355bf5acd 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -2325,6 +2325,7 @@ mixin _ButtonedEventMixin on _BasicEventContext { required double? deltaX, required double? deltaY, }) { + // ignore: implicit_dynamic_function final Function jsWheelEvent = js_util.getProperty(html.window, 'WheelEvent') as Function; final List eventArgs = [ 'wheel', @@ -2336,6 +2337,7 @@ mixin _ButtonedEventMixin on _BasicEventContext { 'deltaY': deltaY, } ]; + // ignore: implicit_dynamic_function return js_util.callConstructor( jsWheelEvent, js_util.jsify(eventArgs) as List, @@ -2553,6 +2555,7 @@ class _MouseEventContext extends _BasicEventContext double? clientY, }) { final Function jsMouseEvent = + // ignore: implicit_dynamic_function js_util.getProperty(html.window, 'MouseEvent') as Function; final List eventArgs = [ type, @@ -2563,6 +2566,7 @@ class _MouseEventContext extends _BasicEventContext 'clientY': clientY, } ]; + // ignore: implicit_dynamic_function return js_util.callConstructor( jsMouseEvent, js_util.jsify(eventArgs) as List, diff --git a/lib/web_ui/test/engine/semantics/semantics_api_test.dart b/lib/web_ui/test/engine/semantics/semantics_api_test.dart new file mode 100644 index 0000000000000..c83fb52956e14 --- /dev/null +++ b/lib/web_ui/test/engine/semantics/semantics_api_test.dart @@ -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. + +@TestOn('chrome || safari || firefox') + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/ui.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +// The body of this file is the same as ../../../../../testing/dart/semantics_test.dart +// Please keep them in sync. + +void testMain() { + // This must match the number of flags in lib/ui/semantics.dart + const int numSemanticsFlags = 25; + test('SemanticsFlag.values refers to all flags.', () async { + expect(SemanticsFlag.values.length, equals(numSemanticsFlags)); + for (int index = 0; index < numSemanticsFlags; ++index) { + final int flag = 1 << index; + expect(SemanticsFlag.values[flag], isNotNull); + expect(SemanticsFlag.values[flag].toString(), startsWith('SemanticsFlag.')); + } + }); + + // This must match the number of actions in lib/ui/semantics.dart + const int numSemanticsActions = 22; + test('SemanticsAction.values refers to all actions.', () async { + expect(SemanticsAction.values.length, equals(numSemanticsActions)); + for (int index = 0; index < numSemanticsActions; ++index) { + final int flag = 1 << index; + expect(SemanticsAction.values[flag], isNotNull); + expect(SemanticsAction.values[flag].toString(), startsWith('SemanticsAction.')); + } + }); + + test('SpellOutStringAttribute.toString', () async { + expect(SpellOutStringAttribute(range: const TextRange(start: 2, end: 5)).toString(), 'SpellOutStringAttribute(TextRange(start: 2, end: 5))'); + }); + + test('LocaleStringAttribute.toString', () async { + expect(LocaleStringAttribute(range: const TextRange(start: 2, end: 5), locale: const Locale('test')).toString(), 'LocaleStringAttribute(TextRange(start: 2, end: 5), test)'); + }); +} diff --git a/lib/web_ui/test/engine/surface/scene_builder_test.dart b/lib/web_ui/test/engine/surface/scene_builder_test.dart index e97999f05c679..94ab92e3c6787 100644 --- a/lib/web_ui/test/engine/surface/scene_builder_test.dart +++ b/lib/web_ui/test/engine/surface/scene_builder_test.dart @@ -921,6 +921,7 @@ HtmlImage createTestImage({int width = 100, int height = 50}) { ctx.fillRect(66, 0, 33, 50); ctx.fill(); final html.ImageElement imageElement = html.ImageElement(); + // ignore: implicit_dynamic_function imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; return HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/engine/util_test.dart b/lib/web_ui/test/engine/util_test.dart index ff710261251dc..ae943ec8f7d2c 100644 --- a/lib/web_ui/test/engine/util_test.dart +++ b/lib/web_ui/test/engine/util_test.dart @@ -39,4 +39,66 @@ void testMain() { expect(transformKindOf(rotation2d), TransformKind.transform2d); expect(isIdentityFloat32ListTransform(rotation2d), isFalse); }); + + test('canonicalizes font families correctly on iOS', () { + debugOperatingSystemOverride = OperatingSystem.iOs; + + expect( + canonicalizeFontFamily('sans-serif'), + 'sans-serif', + ); + expect( + canonicalizeFontFamily('foo'), + '"foo", -apple-system, BlinkMacSystemFont, sans-serif', + ); + expect( + canonicalizeFontFamily('.SF Pro Text'), + '-apple-system, BlinkMacSystemFont', + ); + + debugOperatingSystemOverride = null; + }); + + test('does not use -apple-system on iOS 15', () { + debugOperatingSystemOverride = OperatingSystem.iOs; + debugIsIOS15 = true; + + expect( + canonicalizeFontFamily('sans-serif'), + 'sans-serif', + ); + expect( + canonicalizeFontFamily('foo'), + '"foo", BlinkMacSystemFont, sans-serif', + ); + expect( + canonicalizeFontFamily('.SF Pro Text'), + 'BlinkMacSystemFont', + ); + + debugOperatingSystemOverride = null; + debugIsIOS15 = null; + }); + + test('parseFloat basic tests', () { + // Simple integers and doubles. + expect(parseFloat('108'), 108.0); + expect(parseFloat('.34'), 0.34); + expect(parseFloat('108.34'), 108.34); + + // Number followed by text. + expect(parseFloat('108.34px'), 108.34); + expect(parseFloat('108.34px29'), 108.34); + expect(parseFloat('108.34px 29'), 108.34); + + // Number followed by space and text. + expect(parseFloat('108.34 px29'), 108.34); + expect(parseFloat('108.34 px 29'), 108.34); + + // Invalid numbers. + expect(parseFloat('text'), isNull); + expect(parseFloat('text108'), isNull); + expect(parseFloat('text 108'), isNull); + expect(parseFloat('another text 108'), isNull); + }); } diff --git a/lib/web_ui/test/engine/web_experiments_test.dart b/lib/web_ui/test/engine/web_experiments_test.dart index fd31fc9b97ed1..0e0516ebd0880 100644 --- a/lib/web_ui/test/engine/web_experiments_test.dart +++ b/lib/web_ui/test/engine/web_experiments_test.dart @@ -31,6 +31,7 @@ void testMain() { } void jsUpdateExperiment(dynamic name, dynamic enabled) { + // ignore: implicit_dynamic_function js_util.callMethod( html.window, '_flutter_internal_update_experiment', diff --git a/lib/web_ui/test/html/canvas_clip_path_golden_test.dart b/lib/web_ui/test/html/canvas_clip_path_golden_test.dart index fc8ce74b0facd..0be4e70c1e559 100644 --- a/lib/web_ui/test/html/canvas_clip_path_golden_test.dart +++ b/lib/web_ui/test/html/canvas_clip_path_golden_test.dart @@ -140,6 +140,7 @@ engine.HtmlImage createTestImage({int width = 200, int height = 150}) { ctx.fillRect(2 * width / 3, 0, width / 3, height); ctx.fill(); final html.ImageElement imageElement = html.ImageElement(); + // ignore: implicit_dynamic_function imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; return engine.HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/html/compositing/canvas_blend_golden_test.dart b/lib/web_ui/test/html/compositing/canvas_blend_golden_test.dart index f873ff491a6af..e143b8f0949cb 100644 --- a/lib/web_ui/test/html/compositing/canvas_blend_golden_test.dart +++ b/lib/web_ui/test/html/compositing/canvas_blend_golden_test.dart @@ -120,6 +120,7 @@ HtmlImage createTestImage() { ctx.fillRect(66, 0, 33, 50); ctx.fill(); final html.ImageElement imageElement = html.ImageElement(); + // ignore: implicit_dynamic_function imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; return HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/html/compositing/color_filter_golden_test.dart b/lib/web_ui/test/html/compositing/color_filter_golden_test.dart index 6a3a5097432f6..f07e0ab22724b 100644 --- a/lib/web_ui/test/html/compositing/color_filter_golden_test.dart +++ b/lib/web_ui/test/html/compositing/color_filter_golden_test.dart @@ -189,6 +189,7 @@ HtmlImage createTestImage({int width = 200, int height = 150}) { ctx.fillRect(2 * width / 3, 0, width / 3, height); ctx.fill(); final html.ImageElement imageElement = html.ImageElement(); + // ignore: implicit_dynamic_function imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; return HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/html/drawing/canvas_draw_image_golden_test.dart b/lib/web_ui/test/html/drawing/canvas_draw_image_golden_test.dart index 8c5808cf283bc..a4c9461d52763 100644 --- a/lib/web_ui/test/html/drawing/canvas_draw_image_golden_test.dart +++ b/lib/web_ui/test/html/drawing/canvas_draw_image_golden_test.dart @@ -734,6 +734,7 @@ HtmlImage createTestImage({int width = 100, int height = 50}) { ctx.fillRect(66, 0, 33, 50); ctx.fill(); final html.ImageElement imageElement = html.ImageElement(); + // ignore: implicit_dynamic_function imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; return HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/html/drawing/draw_vertices_golden_test.dart b/lib/web_ui/test/html/drawing/draw_vertices_golden_test.dart index 6e7faae60c2cc..0d3c64fd99631 100644 --- a/lib/web_ui/test/html/drawing/draw_vertices_golden_test.dart +++ b/lib/web_ui/test/html/drawing/draw_vertices_golden_test.dart @@ -424,6 +424,7 @@ Future createTestImage({int width = 50, int height = 40}) { imageElement.onLoad.listen((html.Event event) { completer.complete(HtmlImage(imageElement, width, height)); }); + // ignore: implicit_dynamic_function imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; return completer.future; } diff --git a/lib/web_ui/test/html/image_test.dart b/lib/web_ui/test/html/image_test.dart new file mode 100644 index 0000000000000..6b6ec48bd312c --- /dev/null +++ b/lib/web_ui/test/html/image_test.dart @@ -0,0 +1,148 @@ +// 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'; +import 'dart:typed_data'; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' hide TextStyle; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +typedef _ListPredicate = bool Function(List); +_ListPredicate deepEqualList(List a) { + return (List b) { + if (a.length != b.length) + return false; + for (int i = 0; i < a.length; i += 1) { + if (a[i] != b[i]) + return false; + } + return true; + }; +} + +Matcher listEqual(List source, {int tolerance = 0}) { + return predicate( + (List target) { + if (source.length != target.length) + return false; + for (int i = 0; i < source.length; i += 1) { + if ((source[i] - target[i]).abs() > tolerance) + return false; + } + return true; + }, + source.toString(), + ); +} + +// Converts `rawPixels` into a list of bytes that represent raw pixels in rgba8888. +// +// Each element of `rawPixels` represents a bytes in order 0xRRGGBBAA, with +// pixel order Left to right, then top to bottom. +Uint8List _pixelsToBytes(List rawPixels) { + return Uint8List.fromList((() sync* { + for (final int pixel in rawPixels) { + yield (pixel >> 24) & 0xff; // r + yield (pixel >> 16) & 0xff; // g + yield (pixel >> 8) & 0xff; // b + yield (pixel >> 0) & 0xff; // a + } + })().toList()); +} + +Future _encodeToHtmlThenDecode( + Uint8List rawBytes, + int width, + int height, { + PixelFormat pixelFormat = PixelFormat.rgba8888, +}) async { + final ImageDescriptor descriptor = ImageDescriptor.raw( + await ImmutableBuffer.fromUint8List(rawBytes), + width: width, + height: height, + pixelFormat: pixelFormat, + ); + return (await (await descriptor.instantiateCodec()).getNextFrame()).image; +} + +Future testMain() async { + test('Correctly encodes an opaque image', () async { + // A 2x2 testing image without transparency. + final Image sourceImage = await _encodeToHtmlThenDecode( + _pixelsToBytes( + [0xFF0102FF, 0x04FE05FF, 0x0708FDFF, 0x0A0B0C00], + ), 2, 2, + ); + final Uint8List actualPixels = Uint8List.sublistView( + (await sourceImage.toByteData(format: ImageByteFormat.rawStraightRgba))!); + // The `benchmarkPixels` is identical to `sourceImage` except for the fully + // transparent last pixel, whose channels are turned 0. + final Uint8List benchmarkPixels = _pixelsToBytes( + [0xFF0102FF, 0x04FE05FF, 0x0708FDFF, 0x00000000], + ); + expect(actualPixels, listEqual(benchmarkPixels)); + }); + + test('Correctly encodes an opaque image in bgra8888', () async { + // A 2x2 testing image without transparency. + final Image sourceImage = await _encodeToHtmlThenDecode( + _pixelsToBytes( + [0xFF0102FF, 0x04FE05FF, 0x0708FDFF, 0x0A0B0C00], + ), 2, 2, pixelFormat: PixelFormat.bgra8888, + ); + final Uint8List actualPixels = Uint8List.sublistView( + (await sourceImage.toByteData(format: ImageByteFormat.rawStraightRgba))!); + // The `benchmarkPixels` is the same as `sourceImage` except that the R and + // G channels are swapped and the fully transparent last pixel is turned 0. + final Uint8List benchmarkPixels = _pixelsToBytes( + [0x0201FFFF, 0x05FE04FF, 0xFD0807FF, 0x00000000], + ); + expect(actualPixels, listEqual(benchmarkPixels)); + }); + + test('Correctly encodes a transparent image', () async { + // A 2x2 testing image with transparency. + final Image sourceImage = await _encodeToHtmlThenDecode( + _pixelsToBytes( + [0xFF800006, 0xFF800080, 0xFF8000C0, 0xFF8000FF], + ), 2, 2, + ); + final Image blueBackground = await _encodeToHtmlThenDecode( + _pixelsToBytes( + [0x0000FFFF, 0x0000FFFF, 0x0000FFFF, 0x0000FFFF], + ), 2, 2, + ); + // The standard way of testing the raw bytes of `sourceImage` is to draw + // the image onto a canvas and fetch its data (see HtmlImage.toByteData). + // But here, we draw an opaque background first before drawing the image, + // and test if the blended result is expected. + // + // This is because, if we only draw the `sourceImage`, the resulting pixels + // will be slightly off from the raw pixels. The reason is unknown, but + // very likely because the canvas.getImageData introduces rounding errors + // if any pixels are left semi-transparent, which might be caused by + // converting to and from pre-multiplied values. See + // https://github.com/flutter/flutter/issues/92958 . + final CanvasElement canvas = CanvasElement() + ..width = 2 + ..height = 2; + final CanvasRenderingContext2D ctx = canvas.context2D; + ctx.drawImage((blueBackground as HtmlImage).imgElement, 0, 0); + ctx.drawImage((sourceImage as HtmlImage).imgElement, 0, 0); + + final ImageData imageData = ctx.getImageData(0, 0, 2, 2); + final List actualPixels = imageData.data; + + final Uint8List benchmarkPixels = _pixelsToBytes( + [0x0603F9FF, 0x80407FFF, 0xC0603FFF, 0xFF8000FF], + ); + expect(actualPixels, listEqual(benchmarkPixels, tolerance: 1)); + }); +} diff --git a/lib/web_ui/test/html/paragraph/justify_golden_test.dart b/lib/web_ui/test/html/paragraph/justify_golden_test.dart new file mode 100644 index 0000000000000..b64470741541b --- /dev/null +++ b/lib/web_ui/test/html/paragraph/justify_golden_test.dart @@ -0,0 +1,164 @@ +// 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:async'; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' hide window; + +import 'helper.dart'; +import 'text_scuba.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +Future testMain() async { + setUpStableTestFonts(); + + test('TextAlign.justify with multiple spans', () { + const Rect bounds = Rect.fromLTWH(0, 0, 400, 400); + final BitmapCanvas canvas = BitmapCanvas(bounds, RenderStrategy()); + + void build(CanvasParagraphBuilder builder) { + builder.pushStyle(EngineTextStyle.only(color: black)); + builder.addText('Lorem ipsum dolor sit '); + builder.pushStyle(EngineTextStyle.only(color: blue)); + builder.addText('amet, consectetur '); + builder.pushStyle(EngineTextStyle.only(color: green)); + builder.addText('adipiscing elit, sed do eiusmod tempor incididunt ut '); + builder.pushStyle(EngineTextStyle.only(color: red)); + builder + .addText('labore et dolore magna aliqua. Ut enim ad minim veniam, '); + builder.pushStyle(EngineTextStyle.only(color: lightPurple)); + builder.addText('quis nostrud exercitation ullamco '); + builder.pushStyle(EngineTextStyle.only(color: blue)); + builder.addText('laboris nisi ut aliquip ex ea commodo consequat.'); + } + + final CanvasParagraph paragraph = rich( + EngineParagraphStyle( + fontFamily: 'Roboto', + fontSize: 20.0, + textAlign: TextAlign.justify, + ), + build, + ); + paragraph.layout(constrain(250.0)); + canvas.drawParagraph(paragraph, Offset.zero); + + return takeScreenshot(canvas, bounds, 'canvas_paragraph_justify'); + }); + + test('TextAlign.justify with single space and empty line', () { + const Rect bounds = Rect.fromLTWH(0, 0, 400, 400); + final BitmapCanvas canvas = BitmapCanvas(bounds, RenderStrategy()); + + void build(CanvasParagraphBuilder builder) { + builder.pushStyle(bg(yellow)); + builder.pushStyle(EngineTextStyle.only(color: black)); + builder.addText('Loremipsumdolorsit'); + builder.pushStyle(EngineTextStyle.only(color: blue)); + builder.addText('amet, consectetur\n\n'); + builder.pushStyle(EngineTextStyle.only(color: lightPurple)); + builder.addText('adipiscing elit, sed do eiusmod tempor incididunt ut '); + builder.pushStyle(EngineTextStyle.only(color: red)); + builder.addText('labore et dolore magna aliqua.'); + } + + final CanvasParagraph paragraph = rich( + EngineParagraphStyle( + fontFamily: 'Roboto', + fontSize: 20.0, + textAlign: TextAlign.justify, + ), + build, + ); + paragraph.layout(constrain(250.0)); + canvas.drawParagraph(paragraph, Offset.zero); + + return takeScreenshot( + canvas, bounds, 'canvas_paragraph_justify_empty_line'); + }); + + test('TextAlign.justify with ellipsis', () { + const Rect bounds = Rect.fromLTWH(0, 0, 400, 300); + final BitmapCanvas canvas = BitmapCanvas(bounds, RenderStrategy()); + + final EngineParagraphStyle paragraphStyle = EngineParagraphStyle( + fontFamily: 'Roboto', + fontSize: 20.0, + textAlign: TextAlign.justify, + maxLines: 4, + ellipsis: '...', + ); + final CanvasParagraph paragraph = rich( + paragraphStyle, + (CanvasParagraphBuilder builder) { + builder.pushStyle(EngineTextStyle.only(color: black)); + builder.addText('Lorem ipsum dolor sit '); + builder.pushStyle(EngineTextStyle.only(color: blue)); + builder.addText('amet, consectetur '); + builder.pushStyle(EngineTextStyle.only(color: green)); + builder + .addText('adipiscing elit, sed do eiusmod tempor incididunt ut '); + builder.pushStyle(EngineTextStyle.only(color: red)); + builder.addText( + 'labore et dolore magna aliqua. Ut enim ad minim veniam, '); + builder.pushStyle(EngineTextStyle.only(color: lightPurple)); + builder.addText('quis nostrud exercitation ullamco '); + builder.pushStyle(EngineTextStyle.only(color: blue)); + builder.addText('laboris nisi ut aliquip ex ea commodo consequat.'); + }, + ); + paragraph.layout(constrain(250)); + canvas.drawParagraph(paragraph, Offset.zero); + + return takeScreenshot(canvas, bounds, 'canvas_paragraph_justify_ellipsis'); + }); + + test('TextAlign.justify with background', () { + const Rect bounds = Rect.fromLTWH(0, 0, 400, 400); + final BitmapCanvas canvas = BitmapCanvas(bounds, RenderStrategy()); + + final EngineParagraphStyle paragraphStyle = EngineParagraphStyle( + fontFamily: 'Roboto', + fontSize: 20.0, + textAlign: TextAlign.justify, + ); + final CanvasParagraph paragraph = rich( + paragraphStyle, + (CanvasParagraphBuilder builder) { + builder.pushStyle(EngineTextStyle.only(color: black)); + builder.pushStyle(bg(blue)); + builder.addText('Lorem ipsum dolor sit '); + builder.pushStyle(bg(black)); + builder.pushStyle(EngineTextStyle.only(color: white)); + builder.addText('amet, consectetur '); + builder.pop(); + builder.pushStyle(bg(green)); + builder + .addText('adipiscing elit, sed do eiusmod tempor incididunt ut '); + builder.pushStyle(bg(yellow)); + builder.addText( + 'labore et dolore magna aliqua. Ut enim ad minim veniam, '); + builder.pushStyle(bg(red)); + builder.addText('quis nostrud exercitation ullamco '); + builder.pushStyle(bg(green)); + builder.addText('laboris nisi ut aliquip ex ea commodo consequat.'); + }, + ); + paragraph.layout(constrain(250)); + canvas.drawParagraph(paragraph, Offset.zero); + + return takeScreenshot( + canvas, bounds, 'canvas_paragraph_justify_background'); + }); +} + +EngineTextStyle bg(Color color) { + return EngineTextStyle.only(background: Paint()..color = color); +} diff --git a/lib/web_ui/test/html/shaders/image_shader_golden_test.dart b/lib/web_ui/test/html/shaders/image_shader_golden_test.dart index 082ce39f1aa3c..70156d3e28270 100644 --- a/lib/web_ui/test/html/shaders/image_shader_golden_test.dart +++ b/lib/web_ui/test/html/shaders/image_shader_golden_test.dart @@ -145,6 +145,7 @@ HtmlImage createTestImage() { ctx.fillRect(width2, width2, width2, width2); ctx.fill(); final html.ImageElement imageElement = html.ImageElement(); + // ignore: implicit_dynamic_function imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; return HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/keyboard_test.dart b/lib/web_ui/test/keyboard_test.dart index 8787196d3ed52..cf3004a78c1d8 100644 --- a/lib/web_ui/test/keyboard_test.dart +++ b/lib/web_ui/test/keyboard_test.dart @@ -658,6 +658,7 @@ html.KeyboardEvent dispatchKeyboardEvent( target ??= html.window; final Function jsKeyboardEvent = + // ignore: implicit_dynamic_function js_util.getProperty(html.window, 'KeyboardEvent') as Function; final List eventArgs = [ type, @@ -674,6 +675,7 @@ html.KeyboardEvent dispatchKeyboardEvent( 'cancelable': true, } ]; + // ignore: implicit_dynamic_function final html.KeyboardEvent event = js_util.callConstructor( jsKeyboardEvent, js_util.jsify(eventArgs) as List, diff --git a/lib/web_ui/test/text/canvas_paragraph_test.dart b/lib/web_ui/test/text/canvas_paragraph_test.dart index fff138c43ccbb..3649e5c5d01d3 100644 --- a/lib/web_ui/test/text/canvas_paragraph_test.dart +++ b/lib/web_ui/test/text/canvas_paragraph_test.dart @@ -401,26 +401,26 @@ Future testMain() async { // There should be no "B" in the first line's boxes. expect(firstLine.boxes, hasLength(2)); - expect((firstLine.boxes![0] as SpanBox).toText(), 'AAA'); - expect((firstLine.boxes![0] as SpanBox).left, 0.0); + expect((firstLine.boxes[0] as SpanBox).toText(), 'AAA'); + expect((firstLine.boxes[0] as SpanBox).left, 0.0); - expect((firstLine.boxes![1] as SpanBox).toText(), ' '); - expect((firstLine.boxes![1] as SpanBox).left, 30.0); + expect((firstLine.boxes[1] as SpanBox).toText(), ' '); + expect((firstLine.boxes[1] as SpanBox).left, 30.0); // Make sure the second line isn't missing any boxes. expect(secondLine.boxes, hasLength(4)); - expect((secondLine.boxes![0] as SpanBox).toText(), 'B'); - expect((secondLine.boxes![0] as SpanBox).left, 0.0); + expect((secondLine.boxes[0] as SpanBox).toText(), 'B'); + expect((secondLine.boxes[0] as SpanBox).left, 0.0); - expect((secondLine.boxes![1] as SpanBox).toText(), '_C'); - expect((secondLine.boxes![1] as SpanBox).left, 10.0); + expect((secondLine.boxes[1] as SpanBox).toText(), '_C'); + expect((secondLine.boxes[1] as SpanBox).left, 10.0); - expect((secondLine.boxes![2] as SpanBox).toText(), ' '); - expect((secondLine.boxes![2] as SpanBox).left, 30.0); + expect((secondLine.boxes[2] as SpanBox).toText(), ' '); + expect((secondLine.boxes[2] as SpanBox).left, 30.0); - expect((secondLine.boxes![3] as SpanBox).toText(), 'DD'); - expect((secondLine.boxes![3] as SpanBox).left, 40.0); + expect((secondLine.boxes[3] as SpanBox).toText(), 'DD'); + expect((secondLine.boxes[3] as SpanBox).left, 40.0); }); }); diff --git a/lib/web_ui/test/text/font_collection_test.dart b/lib/web_ui/test/text/font_collection_test.dart index c58e0add78d3f..e76d16cddc307 100644 --- a/lib/web_ui/test/text/font_collection_test.dart +++ b/lib/web_ui/test/text/font_collection_test.dart @@ -16,7 +16,7 @@ void main() { void testMain() { group('$FontManager', () { late FontManager fontManager; - const String _testFontUrl = 'packages/ui/assets/ahem.ttf'; + const String _testFontUrl = '/assets/fonts/ahem.ttf'; setUp(() { fontManager = FontManager(); diff --git a/lib/web_ui/test/text/font_loading_test.dart b/lib/web_ui/test/text/font_loading_test.dart index 22e8d65271174..26bfdafcb2726 100644 --- a/lib/web_ui/test/text/font_loading_test.dart +++ b/lib/web_ui/test/text/font_loading_test.dart @@ -19,7 +19,7 @@ void main() { Future testMain() async { await ui.webOnlyInitializeTestDomRenderer(); group('loadFontFromList', () { - const String _testFontUrl = 'packages/ui/assets/ahem.ttf'; + const String _testFontUrl = '/assets/fonts/ahem.ttf'; tearDown(() { html.document.fonts!.clear(); diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index e9f09b061bfa3..dc4b67fa6eb62 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -2147,6 +2147,7 @@ KeyboardEvent dispatchKeyboardEvent( String type, { required int keyCode, }) { + // ignore: implicit_dynamic_function final Function jsKeyboardEvent = js_util.getProperty(window, 'KeyboardEvent') as Function; final List eventArgs = [ type, @@ -2156,6 +2157,7 @@ KeyboardEvent dispatchKeyboardEvent( } ]; final KeyboardEvent event = + // ignore: implicit_dynamic_function js_util.callConstructor(jsKeyboardEvent, js_util.jsify(eventArgs) as List?) as KeyboardEvent; target.dispatchEvent(event); diff --git a/lib/web_ui/test/window_test.dart b/lib/web_ui/test/window_test.dart index 9069db61e0b2c..2aeeea42f5e9b 100644 --- a/lib/web_ui/test/window_test.dart +++ b/lib/web_ui/test/window_test.dart @@ -445,6 +445,7 @@ void testMain() { } void jsSetUrlStrategy(dynamic strategy) { + // ignore: implicit_dynamic_function js_util.callMethod( html.window, '_flutter_web_set_location_strategy', diff --git a/runtime/dart_isolate.cc b/runtime/dart_isolate.cc index 729cbf88bf8a0..ca2f384d120f0 100644 --- a/runtime/dart_isolate.cc +++ b/runtime/dart_isolate.cc @@ -74,6 +74,10 @@ void DartIsolate::Flags::SetNullSafetyEnabled(bool enabled) { flags_.null_safety = enabled; } +void DartIsolate::Flags::SetIsDontNeedSafe(bool value) { + flags_.snapshot_is_dontneed_safe = value; +} + Dart_IsolateFlags DartIsolate::Flags::Get() const { return flags_; } @@ -89,17 +93,20 @@ std::weak_ptr DartIsolate::SpawnIsolate( const fml::closure& isolate_shutdown_callback, std::optional dart_entrypoint, std::optional dart_entrypoint_library, - std::unique_ptr isolate_configration) const { + const std::vector& dart_entrypoint_args, + std::unique_ptr isolate_configuration) const { return CreateRunningRootIsolate( settings, // GetIsolateGroupData().GetIsolateSnapshot(), // std::move(platform_configuration), // flags, // + nullptr, // isolate_create_callback, // isolate_shutdown_callback, // dart_entrypoint, // dart_entrypoint_library, // - std::move(isolate_configration), // + dart_entrypoint_args, // + std::move(isolate_configuration), // UIDartState::Context{GetTaskRunners(), // snapshot_delegate, // GetIOManager(), // @@ -118,11 +125,13 @@ std::weak_ptr DartIsolate::CreateRunningRootIsolate( fml::RefPtr isolate_snapshot, std::unique_ptr platform_configuration, Flags isolate_flags, + fml::closure root_isolate_create_callback, const fml::closure& isolate_create_callback, const fml::closure& isolate_shutdown_callback, std::optional dart_entrypoint, std::optional dart_entrypoint_library, - std::unique_ptr isolate_configration, + const std::vector& dart_entrypoint_args, + std::unique_ptr isolate_configuration, const UIDartState::Context& context, const DartIsolate* spawning_isolate) { if (!isolate_snapshot) { @@ -130,13 +139,14 @@ std::weak_ptr DartIsolate::CreateRunningRootIsolate( return {}; } - if (!isolate_configration) { + if (!isolate_configuration) { FML_LOG(ERROR) << "Invalid isolate configuration."; return {}; } isolate_flags.SetNullSafetyEnabled( - isolate_configration->IsNullSafetyEnabled(*isolate_snapshot)); + isolate_configuration->IsNullSafetyEnabled(*isolate_snapshot)); + isolate_flags.SetIsDontNeedSafe(isolate_snapshot->IsDontNeedSafe()); auto isolate = CreateRootIsolate(settings, // isolate_snapshot, // @@ -166,7 +176,7 @@ std::weak_ptr DartIsolate::CreateRunningRootIsolate( return {}; } - if (!isolate_configration->PrepareIsolate(*isolate.get())) { + if (!isolate_configuration->PrepareIsolate(*isolate.get())) { FML_LOG(ERROR) << "Could not prepare isolate."; return {}; } @@ -184,10 +194,18 @@ std::weak_ptr DartIsolate::CreateRunningRootIsolate( settings.root_isolate_create_callback(*isolate.get()); } - if (!isolate->RunFromLibrary(dart_entrypoint_library, // - dart_entrypoint, // - settings.dart_entrypoint_args // - )) { + if (root_isolate_create_callback) { + root_isolate_create_callback(); + } + + FML_DCHECK(dart_entrypoint_args.empty() || + settings.dart_entrypoint_args.empty()); + const std::vector& args = !dart_entrypoint_args.empty() + ? dart_entrypoint_args + : settings.dart_entrypoint_args; + if (!isolate->RunFromLibrary(dart_entrypoint_library, // + dart_entrypoint, // + args)) { FML_LOG(ERROR) << "Could not run the run main Dart entrypoint."; return {}; } @@ -832,8 +850,7 @@ Dart_Isolate DartIsolate::DartCreateAndStartServiceIsolate( // TODO(68663): The service isolate in debug mode is always launched without // sound null safety. Fix after the isolate snapshot data is created with the // right flags. - flags->null_safety = - vm_data->GetIsolateSnapshot()->IsNullSafetyEnabled(nullptr); + flags->null_safety = vm_data->GetServiceIsolateSnapshotNullSafety(); #endif UIDartState::Context context( @@ -842,13 +859,13 @@ Dart_Isolate DartIsolate::DartCreateAndStartServiceIsolate( context.advisory_script_uri = DART_VM_SERVICE_ISOLATE_NAME; context.advisory_script_entrypoint = DART_VM_SERVICE_ISOLATE_NAME; std::weak_ptr weak_service_isolate = - DartIsolate::CreateRootIsolate(vm_data->GetSettings(), // - vm_data->GetIsolateSnapshot(), // - nullptr, // - DartIsolate::Flags{flags}, // - nullptr, // - nullptr, // - context); // + DartIsolate::CreateRootIsolate(vm_data->GetSettings(), // + vm_data->GetServiceIsolateSnapshot(), // + nullptr, // + DartIsolate::Flags{flags}, // + nullptr, // + nullptr, // + context); // std::shared_ptr service_isolate = weak_service_isolate.lock(); if (!service_isolate) { diff --git a/runtime/dart_isolate.h b/runtime/dart_isolate.h index 47f2ea88c2f3f..6c172d6b610d5 100644 --- a/runtime/dart_isolate.h +++ b/runtime/dart_isolate.h @@ -70,6 +70,7 @@ class DartIsolate : public UIDartState { ~Flags(); void SetNullSafetyEnabled(bool enabled); + void SetIsDontNeedSafe(bool value); Dart_IsolateFlags Get() const; @@ -169,9 +170,18 @@ class DartIsolate : public UIDartState { /// function to invoke. /// @param[in] dart_entrypoint_library The name of the dart library /// containing the entrypoint. + /// @param[in] dart_entrypoint_args Arguments passed as a List + /// to Dart's entrypoint function. /// @param[in] isolate_configuration The isolate configuration used to /// configure the isolate before /// invoking the entrypoint. + /// @param[in] root_isolate_create_callback A callback called after the root + /// isolate is created, _without_ + /// isolate scope. This gives the + /// caller a chance to finish any + /// setup before running the Dart + /// program, and after any embedder + /// callbacks in the settings object. /// @param[in] isolate_create_callback The isolate create callback. This /// will be called when the before the /// main Dart entrypoint is invoked in @@ -186,7 +196,7 @@ class DartIsolate : public UIDartState { /// isolate is still running at this /// point and an isolate scope is /// current. - /// @param[in] context Engine-owned state which is + /// @param[in] context Engine-owned state which is /// accessed by the root dart isolate. /// @param[in] spawning_isolate The isolate that is spawning the /// new isolate. See also @@ -202,11 +212,13 @@ class DartIsolate : public UIDartState { fml::RefPtr isolate_snapshot, std::unique_ptr platform_configuration, Flags flags, + fml::closure root_isolate_create_callback, const fml::closure& isolate_create_callback, const fml::closure& isolate_shutdown_callback, std::optional dart_entrypoint, std::optional dart_entrypoint_library, - std::unique_ptr isolate_configration, + const std::vector& dart_entrypoint_args, + std::unique_ptr isolate_configuration, const UIDartState::Context& context, const DartIsolate* spawning_isolate = nullptr); @@ -236,7 +248,8 @@ class DartIsolate : public UIDartState { const fml::closure& isolate_shutdown_callback, std::optional dart_entrypoint, std::optional dart_entrypoint_library, - std::unique_ptr isolate_configration) const; + const std::vector& dart_entrypoint_args, + std::unique_ptr isolate_configuration) const; // |UIDartState| ~DartIsolate() override; @@ -406,7 +419,7 @@ class DartIsolate : public UIDartState { friend class IsolateConfiguration; class AutoFireClosure { public: - AutoFireClosure(const fml::closure& closure); + explicit AutoFireClosure(const fml::closure& closure); ~AutoFireClosure(); diff --git a/runtime/dart_isolate_unittests.cc b/runtime/dart_isolate_unittests.cc index bd66b1d195fbf..38686d8846484 100644 --- a/runtime/dart_isolate_unittests.cc +++ b/runtime/dart_isolate_unittests.cc @@ -59,10 +59,12 @@ TEST_F(DartIsolateTest, RootIsolateCreationAndShutdown) { vm_data->GetIsolateSnapshot(), // isolate snapshot nullptr, // platform configuration DartIsolate::Flags{}, // flags + nullptr, // root_isolate_create_callback settings.isolate_create_callback, // isolate create callback settings.isolate_shutdown_callback, // isolate shutdown callback "main", // dart entrypoint std::nullopt, // dart entrypoint library + {}, // dart entrypoint arguments std::move(isolate_configuration), // isolate configuration std::move(context) // engine context ); @@ -97,10 +99,12 @@ TEST_F(DartIsolateTest, SpawnIsolate) { vm_data->GetIsolateSnapshot(), // isolate snapshot nullptr, // platform configuration DartIsolate::Flags{}, // flags + nullptr, // root_isolate_create_callback settings.isolate_create_callback, // isolate create callback settings.isolate_shutdown_callback, // isolate shutdown callback "main", // dart entrypoint std::nullopt, // dart entrypoint library + {}, // dart entrypoint arguments std::move(isolate_configuration), // isolate configuration std::move(context) // engine context ); @@ -121,7 +125,8 @@ TEST_F(DartIsolateTest, SpawnIsolate) { /*isolate_shutdown_callback=*/settings.isolate_shutdown_callback, /*dart_entrypoint=*/"main", /*dart_entrypoint_library=*/std::nullopt, - /*isolate_configration=*/std::move(spawn_configuration)); + /*dart_entrypoint_args=*/{}, + /*isolate_configuration=*/std::move(spawn_configuration)); auto spawn = weak_spawn.lock(); ASSERT_TRUE(spawn); ASSERT_EQ(spawn->GetPhase(), DartIsolate::Phase::Running); @@ -170,10 +175,12 @@ TEST_F(DartIsolateTest, IsolateShutdownCallbackIsInIsolateScope) { vm_data->GetIsolateSnapshot(), // isolate snapshot nullptr, // platform configuration DartIsolate::Flags{}, // flags + nullptr, // root_isolate_create_callback settings.isolate_create_callback, // isolate create callback settings.isolate_shutdown_callback, // isolate shutdown callback "main", // dart entrypoint std::nullopt, // dart entrypoint library + {}, // dart entrypoint arguments std::move(isolate_configuration), // isolate configuration std::move(context) // engine context ); @@ -392,10 +399,12 @@ TEST_F(DartIsolateTest, CanCreateServiceIsolate) { vm_data->GetIsolateSnapshot(), // isolate snapshot nullptr, // platform configuration DartIsolate::Flags{}, // flags + nullptr, // root_isolate_create_callback settings.isolate_create_callback, // isolate create callback settings.isolate_shutdown_callback, // isolate shutdown callback "main", // dart entrypoint std::nullopt, // dart entrypoint library + {}, // dart entrypoint arguments std::move(isolate_configuration), // isolate configuration std::move(context) // engine context ); @@ -490,10 +499,12 @@ TEST_F(DartIsolateTest, InvalidLoadingUnitFails) { vm_data->GetIsolateSnapshot(), // isolate snapshot nullptr, // platform configuration DartIsolate::Flags{}, // flags + nullptr, // root_isolate_create_callback settings.isolate_create_callback, // isolate create callback settings.isolate_shutdown_callback, // isolate shutdown callback "main", // dart entrypoint std::nullopt, // dart entrypoint library + {}, // dart entrypoint arguments std::move(isolate_configuration), // isolate configuration std::move(context) // engine context ); diff --git a/runtime/dart_lifecycle_unittests.cc b/runtime/dart_lifecycle_unittests.cc index b2beed0dd9753..6b3b4d2a3621d 100644 --- a/runtime/dart_lifecycle_unittests.cc +++ b/runtime/dart_lifecycle_unittests.cc @@ -64,10 +64,12 @@ static std::shared_ptr CreateAndRunRootIsolate( vm.GetIsolateSnapshot(), // isolate_snapshot {}, // platform configuration DartIsolate::Flags{}, // flags + nullptr, // root isolate create callback settings.isolate_create_callback, // isolate create callback settings.isolate_shutdown_callback, // isolate shutdown callback, entrypoint, // dart entrypoint std::nullopt, // dart entrypoint library + {}, // dart entrypoint arguments std::move(isolate_configuration), // isolate configuration std::move(context) // engine context ) diff --git a/runtime/dart_snapshot.cc b/runtime/dart_snapshot.cc index 0dbbab8b12fbe..e567dd58d41bc 100644 --- a/runtime/dart_snapshot.cc +++ b/runtime/dart_snapshot.cc @@ -97,7 +97,11 @@ static std::shared_ptr SearchMapping( static std::shared_ptr ResolveVMData( const Settings& settings) { #if DART_SNAPSHOT_STATIC_LINK - return std::make_unique(kDartVmSnapshotData, 0); + return std::make_unique(kDartVmSnapshotData, + 0, // size + nullptr, // release_func + true // dontneed_safe + ); #else // DART_SNAPSHOT_STATIC_LINK return SearchMapping( settings.vm_snapshot_data, // embedder_mapping_callback @@ -112,7 +116,11 @@ static std::shared_ptr ResolveVMData( static std::shared_ptr ResolveVMInstructions( const Settings& settings) { #if DART_SNAPSHOT_STATIC_LINK - return std::make_unique(kDartVmSnapshotInstructions, 0); + return std::make_unique(kDartVmSnapshotInstructions, + 0, // size + nullptr, // release_func + true // dontneed_safe + ); #else // DART_SNAPSHOT_STATIC_LINK return SearchMapping( settings.vm_snapshot_instr, // embedder_mapping_callback @@ -127,7 +135,11 @@ static std::shared_ptr ResolveVMInstructions( static std::shared_ptr ResolveIsolateData( const Settings& settings) { #if DART_SNAPSHOT_STATIC_LINK - return std::make_unique(kDartIsolateSnapshotData, 0); + return std::make_unique(kDartIsolateSnapshotData, + 0, // size + nullptr, // release_func + true // dontneed_safe + ); #else // DART_SNAPSHOT_STATIC_LINK return SearchMapping( settings.isolate_snapshot_data, // embedder_mapping_callback @@ -143,7 +155,11 @@ static std::shared_ptr ResolveIsolateInstructions( const Settings& settings) { #if DART_SNAPSHOT_STATIC_LINK return std::make_unique( - kDartIsolateSnapshotInstructions, 0); + kDartIsolateSnapshotInstructions, + 0, // size + nullptr, // release_func + true // dontneed_safe + ); #else // DART_SNAPSHOT_STATIC_LINK return SearchMapping( settings.isolate_snapshot_instr, // embedder_mapping_callback @@ -192,6 +208,25 @@ fml::RefPtr DartSnapshot::IsolateSnapshotFromMappings( return nullptr; } +fml::RefPtr DartSnapshot::VMServiceIsolateSnapshotFromSettings( + const Settings& settings) { +#if DART_SNAPSHOT_STATIC_LINK + return nullptr; +#else // DART_SNAPSHOT_STATIC_LINK + if (settings.vmservice_snapshot_library_path.empty()) { + return nullptr; + } + + std::shared_ptr snapshot_data = + SearchMapping(nullptr, "", settings.vmservice_snapshot_library_path, + DartSnapshot::kIsolateDataSymbol, false); + std::shared_ptr snapshot_instructions = + SearchMapping(nullptr, "", settings.vmservice_snapshot_library_path, + DartSnapshot::kIsolateInstructionsSymbol, true); + return IsolateSnapshotFromMappings(snapshot_data, snapshot_instructions); +#endif // DART_SNAPSHOT_STATIC_LINK +} + DartSnapshot::DartSnapshot(std::shared_ptr data, std::shared_ptr instructions) : data_(std::move(data)), instructions_(std::move(instructions)) {} @@ -214,6 +249,16 @@ const uint8_t* DartSnapshot::GetInstructionsMapping() const { return instructions_ ? instructions_->GetMapping() : nullptr; } +bool DartSnapshot::IsDontNeedSafe() const { + if (data_ && !data_->IsDontNeedSafe()) { + return false; + } + if (instructions_ && !instructions_->IsDontNeedSafe()) { + return false; + } + return true; +} + bool DartSnapshot::IsNullSafetyEnabled(const fml::Mapping* kernel) const { return ::Dart_DetectNullSafety( nullptr, // script_uri (unsupported by Flutter) diff --git a/runtime/dart_snapshot.h b/runtime/dart_snapshot.h index 97b5f40763ec9..47cef0d57ad32 100644 --- a/runtime/dart_snapshot.h +++ b/runtime/dart_snapshot.h @@ -114,6 +114,15 @@ class DartSnapshot : public fml::RefCountedThreadSafe { std::shared_ptr snapshot_data, std::shared_ptr snapshot_instructions); + //---------------------------------------------------------------------------- + /// @brief Create an isolate snapshot specialized for launching the + /// service isolate. Returns nullptr if no such snapshot is + /// available. + /// + /// @return A valid isolate snapshot or nullptr. + static fml::RefPtr VMServiceIsolateSnapshotFromSettings( + const Settings& settings); + //---------------------------------------------------------------------------- /// @brief Determines if this snapshot contains a heap component. Since /// the instructions component is optional, the method does not @@ -151,6 +160,11 @@ class DartSnapshot : public fml::RefCountedThreadSafe { /// const uint8_t* GetInstructionsMapping() const; + //---------------------------------------------------------------------------- + /// @brief Returns whether both the data and instructions mappings are + /// safe to use with madvise(DONTNEED). + bool IsDontNeedSafe() const; + bool IsNullSafetyEnabled( const fml::Mapping* application_kernel_mapping) const; diff --git a/runtime/dart_vm.cc b/runtime/dart_vm.cc index a2f400bb81ec4..de3b94a89c891 100644 --- a/runtime/dart_vm.cc +++ b/runtime/dart_vm.cc @@ -215,11 +215,25 @@ static std::vector ProfilingFlags(bool enable_profiling) { // the VM enables the same by default. In either case, we have some profiling // flags. if (enable_profiling) { - return {// This is the default. But just be explicit. - "--profiler", - // This instructs the profiler to walk C++ frames, and to include - // them in the profile. - "--profile-vm"}; + return { + // This is the default. But just be explicit. + "--profiler", + // This instructs the profiler to walk C++ frames, and to include + // them in the profile. + "--profile-vm", +#if OS_IOS && ARCH_CPU_ARM_FAMILY && ARCH_CPU_ARMEL + // Set the profiler interrupt period to 500Hz instead of the + // default 1000Hz on 32-bit iOS devices to reduce average and worst + // case frame build times. + // + // Note: profile_period is time in microseconds between sampling + // events, not frequency. Frequency is calculated 1/period (or + // 1,000,000 / 2,000 -> 500Hz in this case). + "--profile_period=2000", +#else + "--profile_period=1000", +#endif // OS_IOS && ARCH_CPU_ARM_FAMILY && ARCH_CPU_ARMEL + }; } else { return {"--no-profiler"}; } diff --git a/runtime/dart_vm_data.cc b/runtime/dart_vm_data.cc index 816d428f0899e..a47f5d1cc4887 100644 --- a/runtime/dart_vm_data.cc +++ b/runtime/dart_vm_data.cc @@ -32,19 +32,25 @@ std::shared_ptr DartVMData::Create( } } + fml::RefPtr service_isolate_snapshot = + DartSnapshot::VMServiceIsolateSnapshotFromSettings(settings); + return std::shared_ptr(new DartVMData( - std::move(settings), // - std::move(vm_snapshot), // - std::move(isolate_snapshot) // + std::move(settings), // + std::move(vm_snapshot), // + std::move(isolate_snapshot), // + std::move(service_isolate_snapshot) // )); } DartVMData::DartVMData(Settings settings, fml::RefPtr vm_snapshot, - fml::RefPtr isolate_snapshot) + fml::RefPtr isolate_snapshot, + fml::RefPtr service_isolate_snapshot) : settings_(settings), vm_snapshot_(vm_snapshot), - isolate_snapshot_(isolate_snapshot) {} + isolate_snapshot_(isolate_snapshot), + service_isolate_snapshot_(service_isolate_snapshot) {} DartVMData::~DartVMData() = default; @@ -60,4 +66,23 @@ fml::RefPtr DartVMData::GetIsolateSnapshot() const { return isolate_snapshot_; } +fml::RefPtr DartVMData::GetServiceIsolateSnapshot() const { + // Use the specialized snapshot for the service isolate if the embedder + // provides one. Otherwise, use the application snapshot. + return service_isolate_snapshot_ ? service_isolate_snapshot_ + : isolate_snapshot_; +} + +bool DartVMData::GetServiceIsolateSnapshotNullSafety() const { + if (service_isolate_snapshot_) { + // The specialized snapshot for the service isolate is always built + // using null safety. However, calling Dart_DetectNullSafety on + // the service isolate snapshot will not work as expected - it will + // instead return a cached value representing the app snapshot. + return true; + } else { + return isolate_snapshot_->IsNullSafetyEnabled(nullptr); + } +} + } // namespace flutter diff --git a/runtime/dart_vm_data.h b/runtime/dart_vm_data.h index f614a24ad0b7a..e0d914ea28a84 100644 --- a/runtime/dart_vm_data.h +++ b/runtime/dart_vm_data.h @@ -71,14 +71,32 @@ class DartVMData { /// fml::RefPtr GetIsolateSnapshot() const; + //---------------------------------------------------------------------------- + /// @brief Get the isolate snapshot used to launch the service isolate + /// in the Dart VM. + /// + /// @return The service isolate snapshot. + /// + fml::RefPtr GetServiceIsolateSnapshot() const; + + //---------------------------------------------------------------------------- + /// @brief Returns whether the service isolate snapshot requires null + /// safety in the Dart_IsolateFlags used to create the isolate. + /// + /// @return True if the snapshot requires null safety. + /// + bool GetServiceIsolateSnapshotNullSafety() const; + private: const Settings settings_; const fml::RefPtr vm_snapshot_; const fml::RefPtr isolate_snapshot_; + const fml::RefPtr service_isolate_snapshot_; DartVMData(Settings settings, fml::RefPtr vm_snapshot, - fml::RefPtr isolate_snapshot); + fml::RefPtr isolate_snapshot, + fml::RefPtr service_isolate_snapshot); FML_DISALLOW_COPY_AND_ASSIGN(DartVMData); }; diff --git a/runtime/dart_vm_lifecycle.cc b/runtime/dart_vm_lifecycle.cc index e621f00655d2d..c033cabe398ff 100644 --- a/runtime/dart_vm_lifecycle.cc +++ b/runtime/dart_vm_lifecycle.cc @@ -82,7 +82,7 @@ DartVMRef DartVMRef::Create(Settings settings, if (!vm) { FML_LOG(ERROR) << "Could not create Dart VM instance."; - return {nullptr}; + return DartVMRef{nullptr}; } gVMData = vm->GetVMData(); diff --git a/runtime/dart_vm_lifecycle.h b/runtime/dart_vm_lifecycle.h index 7c32882459f1b..2573fa50fcd5d 100644 --- a/runtime/dart_vm_lifecycle.h +++ b/runtime/dart_vm_lifecycle.h @@ -49,7 +49,7 @@ class DartVMRef { static std::shared_ptr GetIsolateNameServer(); - operator bool() const { return static_cast(vm_); } + explicit operator bool() const { return static_cast(vm_); } DartVM* get() { FML_DCHECK(vm_); @@ -81,7 +81,7 @@ class DartVMRef { std::shared_ptr vm_; - DartVMRef(std::shared_ptr vm); + explicit DartVMRef(std::shared_ptr vm); // Only used by Dart Isolate to register itself with the VM. static DartVM* GetRunningVM(); diff --git a/runtime/embedder_resources.h b/runtime/embedder_resources.h index 859268d9dccb9..e6dca91ac6c41 100644 --- a/runtime/embedder_resources.h +++ b/runtime/embedder_resources.h @@ -21,7 +21,7 @@ namespace flutter { class EmbedderResources { public: - EmbedderResources(runtime::ResourcesEntry* resources_table); + explicit EmbedderResources(runtime::ResourcesEntry* resources_table); static const int kNoSuchInstance; diff --git a/runtime/isolate_configuration.cc b/runtime/isolate_configuration.cc index dee5c781691bb..649d52e46f8c1 100644 --- a/runtime/isolate_configuration.cc +++ b/runtime/isolate_configuration.cc @@ -43,7 +43,8 @@ class AppSnapshotIsolateConfiguration final : public IsolateConfiguration { class KernelIsolateConfiguration : public IsolateConfiguration { public: - KernelIsolateConfiguration(std::unique_ptr kernel) + explicit KernelIsolateConfiguration( + std::unique_ptr kernel) : kernel_(std::move(kernel)) {} // |IsolateConfiguration| @@ -69,7 +70,7 @@ class KernelIsolateConfiguration : public IsolateConfiguration { class KernelListIsolateConfiguration final : public IsolateConfiguration { public: - KernelListIsolateConfiguration( + explicit KernelListIsolateConfiguration( std::vector>> kernel_pieces) : kernel_piece_futures_(std::move(kernel_pieces)) { diff --git a/runtime/runtime_controller.cc b/runtime/runtime_controller.cc index 35ce7e3e80ddb..f7dfa4fbf8a94 100644 --- a/runtime/runtime_controller.cc +++ b/runtime/runtime_controller.cc @@ -238,20 +238,6 @@ bool RuntimeController::DispatchPointerDataPacket( return false; } -bool RuntimeController::DispatchKeyDataPacket(const KeyDataPacket& packet, - KeyDataResponse callback) { - if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { - TRACE_EVENT1("flutter", "RuntimeController::DispatchKeyDataPacket", "mode", - "basic"); - uint64_t response_id = - platform_configuration->RegisterKeyDataResponse(std::move(callback)); - platform_configuration->get_window(0)->DispatchKeyDataPacket(packet, - response_id); - return true; - } - return false; -} - bool RuntimeController::DispatchSemanticsAction(int32_t id, SemanticsAction action, fml::MallocMapping args) { @@ -355,8 +341,10 @@ tonic::DartErrorHandleType RuntimeController::GetLastError() { bool RuntimeController::LaunchRootIsolate( const Settings& settings, + fml::closure root_isolate_create_callback, std::optional dart_entrypoint, std::optional dart_entrypoint_library, + const std::vector& dart_entrypoint_args, std::unique_ptr isolate_configuration) { if (root_isolate_.lock()) { FML_LOG(ERROR) << "Root isolate was already running."; @@ -369,10 +357,12 @@ bool RuntimeController::LaunchRootIsolate( isolate_snapshot_, // std::make_unique(this), // DartIsolate::Flags{}, // + root_isolate_create_callback, // isolate_create_callback_, // isolate_shutdown_callback_, // dart_entrypoint, // dart_entrypoint_library, // + dart_entrypoint_args, // std::move(isolate_configuration), // context_, // spawning_isolate_.lock().get()) // diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index e1fa9946e2cd1..7b46139a52815 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -120,12 +120,20 @@ class RuntimeController : public PlatformConfigurationClient { /// Launch an isolate in that runtime controller instead. /// /// @param[in] settings The per engine instance settings. + /// @param[in] root_isolate_create_callback A callback invoked before the + /// root isolate has launched the Dart + /// program, but after it has been + /// created. This is called without + /// isolate scope, and after any root + /// isolate callback in the settings. /// @param[in] dart_entrypoint The dart entrypoint. If /// `std::nullopt` or empty, `main` will /// be attempted. /// @param[in] dart_entrypoint_library The dart entrypoint library. If /// `std::nullopt` or empty, the core /// library will be attempted. + /// @param[in] dart_entrypoint_args Arguments passed as a List + /// to Dart's entrypoint function. /// @param[in] isolate_configuration The isolate configuration /// /// @return If the isolate could be launched and guided to the @@ -133,12 +141,14 @@ class RuntimeController : public PlatformConfigurationClient { /// [[nodiscard]] bool LaunchRootIsolate( const Settings& settings, + fml::closure root_isolate_create_callback, std::optional dart_entrypoint, std::optional dart_entrypoint_library, + const std::vector& dart_entrypoint_args, std::unique_ptr isolate_configuration); //---------------------------------------------------------------------------- - /// @brief Clone the the runtime controller. Launching an isolate with a + /// @brief Clone the runtime controller. Launching an isolate with a /// cloned runtime controller will use 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 @@ -304,7 +314,7 @@ class RuntimeController : public PlatformConfigurationClient { /// 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” + /// * If the total allocations exceeds 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 @@ -327,7 +337,7 @@ class RuntimeController : public PlatformConfigurationClient { /// 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 + /// were allocation and remained reachable such that 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 @@ -378,20 +388,6 @@ class RuntimeController : public PlatformConfigurationClient { /// bool DispatchPointerDataPacket(const PointerDataPacket& packet); - //---------------------------------------------------------------------------- - /// @brief Dispatch the specified pointer data message to the running - /// root isolate. - /// - /// @param[in] packet The key data message to dispatch to the isolate. - /// @param[in] callback Called when the framework has decided whether - /// to handle this key data. - /// - /// @return If the key data message was dispatched. This may fail is - /// an isolate is not running. - /// - bool DispatchKeyDataPacket(const KeyDataPacket& packet, - KeyDataResponse callback); - //---------------------------------------------------------------------------- /// @brief Dispatch the semantics action to the specified accessibility /// node. diff --git a/runtime/skia_concurrent_executor.h b/runtime/skia_concurrent_executor.h index 87dde23936325..0753206beb353 100644 --- a/runtime/skia_concurrent_executor.h +++ b/runtime/skia_concurrent_executor.h @@ -37,7 +37,7 @@ class SkiaConcurrentExecutor : public SkExecutor { /// /// @param[in] on_work The work callback. /// - SkiaConcurrentExecutor(const OnWorkCallback& on_work); + explicit SkiaConcurrentExecutor(const OnWorkCallback& on_work); // |SkExecutor| ~SkiaConcurrentExecutor() override; diff --git a/shell/common/BUILD.gn b/shell/common/BUILD.gn index b8d00101df091..62112b4b9d917 100644 --- a/shell/common/BUILD.gn +++ b/shell/common/BUILD.gn @@ -66,6 +66,7 @@ source_set("common") { "canvas_spy.h", "context_options.cc", "context_options.h", + "display.cc", "display.h", "display_manager.cc", "display_manager.h", @@ -73,6 +74,7 @@ source_set("common") { "engine.h", "pipeline.cc", "pipeline.h", + "platform_message_handler.h", "platform_view.cc", "platform_view.h", "pointer_data_dispatcher.cc", @@ -219,7 +221,7 @@ if (enable_unittests) { ":shell_unittests_gpu_configuration_config", ] - # SwiftShader only supports x86/x64_64 + # SwiftShader only supports x86/x86_64 if (target_cpu == "x86" || target_cpu == "x64") { if (test_enable_gl) { sources += [ diff --git a/shell/common/canvas_spy.h b/shell/common/canvas_spy.h index 3d88b9d91b34c..ed9b5e0728c98 100644 --- a/shell/common/canvas_spy.h +++ b/shell/common/canvas_spy.h @@ -24,7 +24,7 @@ class DidDrawCanvas; /// are specific to empty canvases. class CanvasSpy { public: - CanvasSpy(SkCanvas* target_canvas); + explicit CanvasSpy(SkCanvas* target_canvas); //---------------------------------------------------------------------------- /// @brief Returns true if any non transparent content has been drawn diff --git a/shell/common/display.cc b/shell/common/display.cc new file mode 100644 index 0000000000000..05feded91be5a --- /dev/null +++ b/shell/common/display.cc @@ -0,0 +1,11 @@ +// 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/common/display.h" + +namespace flutter { +double Display::GetRefreshRate() const { + return refresh_rate_; +} +} // namespace flutter diff --git a/shell/common/display.h b/shell/common/display.h index 2c7e0fa88ff73..37e964a217f34 100644 --- a/shell/common/display.h +++ b/shell/common/display.h @@ -7,6 +7,8 @@ #include +#include "flutter/fml/macros.h" + namespace flutter { /// Unique ID per display that is stable until the Flutter application restarts. @@ -36,11 +38,11 @@ class Display { explicit Display(double refresh_rate) : display_id_({}), refresh_rate_(refresh_rate) {} - ~Display() = default; + virtual ~Display() = default; // Get the display's maximum refresh rate in the unit of frame per second. // Return `kUnknownDisplayRefreshRate` if the refresh rate is unknown. - double GetRefreshRate() const { return refresh_rate_; } + virtual double GetRefreshRate() const; /// Returns the `DisplayId` of the display. std::optional GetDisplayId() const { return display_id_; } @@ -48,6 +50,8 @@ class Display { private: std::optional display_id_; double refresh_rate_; + + FML_DISALLOW_COPY_AND_ASSIGN(Display); }; } // namespace flutter diff --git a/shell/common/display_manager.cc b/shell/common/display_manager.cc index a30c80b1a31e8..75211308de727 100644 --- a/shell/common/display_manager.cc +++ b/shell/common/display_manager.cc @@ -18,18 +18,19 @@ double DisplayManager::GetMainDisplayRefreshRate() const { if (displays_.empty()) { return kUnknownDisplayRefreshRate; } else { - return displays_[0].GetRefreshRate(); + return displays_[0]->GetRefreshRate(); } } -void DisplayManager::HandleDisplayUpdates(DisplayUpdateType update_type, - std::vector displays) { +void DisplayManager::HandleDisplayUpdates( + DisplayUpdateType update_type, + std::vector> displays) { std::scoped_lock lock(displays_mutex_); CheckDisplayConfiguration(displays); switch (update_type) { case DisplayUpdateType::kStartup: FML_CHECK(displays_.empty()); - displays_ = displays; + displays_ = std::move(displays); return; default: FML_CHECK(false) << "Unknown DisplayUpdateType."; @@ -37,11 +38,11 @@ void DisplayManager::HandleDisplayUpdates(DisplayUpdateType update_type, } void DisplayManager::CheckDisplayConfiguration( - std::vector displays) const { + const std::vector>& displays) const { FML_CHECK(!displays.empty()); if (displays.size() > 1) { for (auto& display : displays) { - FML_CHECK(display.GetDisplayId().has_value()); + FML_CHECK(display->GetDisplayId().has_value()); } } } diff --git a/shell/common/display_manager.h b/shell/common/display_manager.h index aa4bbadbb8618..774373d54e70b 100644 --- a/shell/common/display_manager.h +++ b/shell/common/display_manager.h @@ -40,18 +40,19 @@ class DisplayManager { /// Handles the display updates. void HandleDisplayUpdates(DisplayUpdateType update_type, - std::vector displays); + std::vector> displays); private: /// Guards `displays_` vector. mutable std::mutex displays_mutex_; - std::vector displays_; + std::vector> displays_; /// Checks that the provided display configuration is valid. Currently this /// ensures that all the displays have an id in the case there are multiple /// displays. In case where there is a single display, it is valid for the /// display to not have an id. - void CheckDisplayConfiguration(std::vector displays) const; + void CheckDisplayConfiguration( + const std::vector>& displays) const; }; } // namespace flutter diff --git a/shell/common/engine.cc b/shell/common/engine.cc index 408567435613f..13056e7755c4c 100644 --- a/shell/common/engine.cc +++ b/shell/common/engine.cc @@ -199,24 +199,32 @@ Engine::RunStatus Engine::Run(RunConfiguration configuration) { last_entry_point_ = configuration.GetEntrypoint(); last_entry_point_library_ = configuration.GetEntrypointLibrary(); +#if (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG) + // This is only used to support restart. + last_entry_point_args_ = configuration.GetEntrypointArgs(); +#endif UpdateAssetManager(configuration.GetAssetManager()); - // If the embedding prefetched the default font manager, then set up the - // font manager later in the engine launch process. This makes it less - // likely that the setup will need to wait for the prefetch to complete. - if (settings_.prefetched_default_font_manager) { - SetupDefaultFontManager(); - } - if (runtime_controller_->IsRootIsolateRunning()) { return RunStatus::FailureAlreadyRunning; } + // If the embedding prefetched the default font manager, then set up the + // font manager later in the engine launch process. This makes it less + // likely that the setup will need to wait for the prefetch to complete. + auto root_isolate_create_callback = [&]() { + if (settings_.prefetched_default_font_manager) { + SetupDefaultFontManager(); + } + }; + if (!runtime_controller_->LaunchRootIsolate( settings_, // + root_isolate_create_callback, // configuration.GetEntrypoint(), // configuration.GetEntrypointLibrary(), // + configuration.GetEntrypointArgs(), // configuration.TakeIsolateConfiguration()) // ) { return RunStatus::Failure; @@ -431,14 +439,6 @@ void Engine::DispatchPointerDataPacket( pointer_data_dispatcher_->DispatchPacket(std::move(packet), trace_flow_id); } -void Engine::DispatchKeyDataPacket(std::unique_ptr packet, - KeyDataResponse callback) { - TRACE_EVENT0("flutter", "Engine::DispatchKeyDataPacket"); - if (runtime_controller_) { - runtime_controller_->DispatchKeyDataPacket(*packet, std::move(callback)); - } -} - void Engine::DispatchSemanticsAction(int id, SemanticsAction action, fml::MallocMapping args) { @@ -567,6 +567,10 @@ const std::string& Engine::GetLastEntrypointLibrary() const { return last_entry_point_library_; } +const std::vector& Engine::GetLastEntrypointArgs() const { + return last_entry_point_args_; +} + // |RuntimeDelegate| void Engine::RequestDartDeferredLibrary(intptr_t loading_unit_id) { return delegate_.RequestDartDeferredLibrary(loading_unit_id); diff --git a/shell/common/engine.h b/shell/common/engine.h index ca73c591432a6..b578bf07585c1 100644 --- a/shell/common/engine.h +++ b/shell/common/engine.h @@ -735,21 +735,6 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { void DispatchPointerDataPacket(std::unique_ptr packet, uint64_t trace_flow_id); - //---------------------------------------------------------------------------- - /// @brief Notifies the engine that the embedder has sent it a key data - /// packet. A key data packet contains one key event. This call - /// originates in the platform view and the shell has forwarded - /// the same to the engine on the UI task runner here. The engine - /// will decide whether to handle this event, and send the - /// result using `callback`, which will be called exactly once. - /// - /// @param[in] packet The key data packet. - /// @param[in] callback Called when the framework has decided whether - /// to handle this key data. - /// - void DispatchKeyDataPacket(std::unique_ptr packet, - KeyDataResponse callback); - //---------------------------------------------------------------------------- /// @brief Notifies the engine that the embedder encountered an /// accessibility related action on the specified node. This call @@ -834,6 +819,13 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// const std::string& GetLastEntrypointLibrary() const; + //---------------------------------------------------------------------------- + /// @brief Get the last Entrypoint Arguments that was used in the + /// RunConfiguration when |Engine::Run| was called.This is only + /// valid in debug mode. + /// + const std::vector& GetLastEntrypointArgs() const; + //---------------------------------------------------------------------------- /// @brief Getter for the initial route. This can be set with a platform /// message. @@ -910,29 +902,6 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { } private: - Engine::Delegate& delegate_; - const Settings settings_; - std::unique_ptr animator_; - std::unique_ptr runtime_controller_; - - // The pointer_data_dispatcher_ depends on animator_ and runtime_controller_. - // So it should be defined after them to ensure that pointer_data_dispatcher_ - // is destructed first. - std::unique_ptr pointer_data_dispatcher_; - - std::string last_entry_point_; - std::string last_entry_point_library_; - std::string initial_route_; - ViewportMetrics viewport_metrics_; - std::shared_ptr asset_manager_; - bool activity_running_; - bool have_surface_; - std::shared_ptr font_collection_; - ImageDecoder image_decoder_; - ImageGeneratorRegistry image_generator_registry_; - TaskRunners task_runners_; - fml::WeakPtrFactory weak_factory_; - // |RuntimeDelegate| std::string DefaultRouteName() override; @@ -981,6 +950,29 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { friend class testing::ShellTest; + Engine::Delegate& delegate_; + const Settings settings_; + std::unique_ptr animator_; + std::unique_ptr runtime_controller_; + + // The pointer_data_dispatcher_ depends on animator_ and runtime_controller_. + // So it should be defined after them to ensure that pointer_data_dispatcher_ + // is destructed first. + std::unique_ptr pointer_data_dispatcher_; + + std::string last_entry_point_; + std::string last_entry_point_library_; + std::vector last_entry_point_args_; + std::string initial_route_; + ViewportMetrics viewport_metrics_; + std::shared_ptr asset_manager_; + bool activity_running_; + bool have_surface_; + std::shared_ptr font_collection_; + ImageDecoder image_decoder_; + ImageGeneratorRegistry image_generator_registry_; + TaskRunners task_runners_; + fml::WeakPtrFactory weak_factory_; // Must be the last member. FML_DISALLOW_COPY_AND_ASSIGN(Engine); }; diff --git a/shell/common/fixtures/shell_test.dart b/shell/common/fixtures/shell_test.dart index f1610b5debf38..cd612828ef35a 100644 --- a/shell/common/fixtures/shell_test.dart +++ b/shell/common/fixtures/shell_test.dart @@ -13,6 +13,29 @@ void nativeReportTimingsCallback(List timings) native 'NativeReportTimingsC void nativeOnBeginFrame(int microseconds) native 'NativeOnBeginFrame'; void nativeOnPointerDataPacket(List sequences) native 'NativeOnPointerDataPacket'; + +@pragma('vm:entry-point') +void drawFrames() { + // Wait for native to tell us to start going. + notifyNative(); + + PlatformDispatcher.instance.onBeginFrame = (Duration beginTime) { + final SceneBuilder builder = SceneBuilder(); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawPaint(Paint()..color = const Color(0xFFABCDEF)); + final Picture picture = recorder.endRecording(); + builder.addPicture(Offset.zero, picture); + + final Scene scene = builder.build(); + window.render(scene); + + scene.dispose(); + picture.dispose(); + }; + PlatformDispatcher.instance.scheduleFrame(); +} + @pragma('vm:entry-point') void reportTimingsMain() { PlatformDispatcher.instance.onReportTimings = (List timings) { @@ -224,3 +247,17 @@ void canAccessResourceFromAssetDir() async { }, ); } + +void notifyNativeWhenEngineRun(bool success) native 'NotifyNativeWhenEngineRun'; + +void notifyNativeWhenEngineSpawn(bool success) native 'NotifyNativeWhenEngineSpawn'; + +@pragma('vm:entry-point') +void canRecieveArgumentsWhenEngineRun(List args) { + notifyNativeWhenEngineRun(args.length == 2 && args[0] == 'foo' && args[1] == 'bar'); +} + +@pragma('vm:entry-point') +void canRecieveArgumentsWhenEngineSpawn(List args) { + notifyNativeWhenEngineSpawn(args.length == 2 && args[0] == 'arg1' && args[1] == 'arg2'); +} diff --git a/shell/common/pipeline.h b/shell/common/pipeline.h index f51f41e942812..1bc43d0cf5071 100644 --- a/shell/common/pipeline.h +++ b/shell/common/pipeline.h @@ -71,7 +71,7 @@ class Pipeline { return result; } - operator bool() const { return continuation_ != nullptr; } + explicit operator bool() const { return continuation_ != nullptr; } private: friend class Pipeline; diff --git a/shell/common/platform_message_handler.h b/shell/common/platform_message_handler.h new file mode 100644 index 0000000000000..8e3f77e448236 --- /dev/null +++ b/shell/common/platform_message_handler.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 SHELL_COMMON_PLATFORM_MESSAGE_HANDLER_H_ +#define SHELL_COMMON_PLATFORM_MESSAGE_HANDLER_H_ + +#include + +#include "flutter/lib/ui/window/platform_message.h" + +namespace flutter { + +/// An interface over the ability to handle PlatformMessages that are being sent +/// from Flutter to the host platform. +class PlatformMessageHandler { + public: + virtual ~PlatformMessageHandler() = default; + + /// Ultimately sends the PlatformMessage to the host platform. + /// This method is invoked on the ui thread. + virtual void HandlePlatformMessage( + std::unique_ptr message) = 0; + + /// Performs the return procedure for an associated call to + /// HandlePlatformMessage. + /// This method should be thread-safe and able to be invoked on any thread. + virtual void InvokePlatformMessageResponseCallback( + int response_id, + std::unique_ptr mapping) = 0; + + /// Performs the return procedure for an associated call to + /// HandlePlatformMessage where there is no return value. + /// This method should be thread-safe and able to be invoked on any thread. + virtual void InvokePlatformMessageEmptyResponseCallback(int response_id) = 0; +}; +} // namespace flutter + +#endif // SHELL_COMMON_PLATFORM_MESSAGE_HANDLER_H_ diff --git a/shell/common/platform_view.cc b/shell/common/platform_view.cc index cd3fada30afb4..7862d63b9a2d7 100644 --- a/shell/common/platform_view.cc +++ b/shell/common/platform_view.cc @@ -39,12 +39,6 @@ void PlatformView::DispatchPointerDataPacket( pointer_data_packet_converter_.Convert(std::move(packet))); } -void PlatformView::DispatchKeyDataPacket(std::unique_ptr packet, - KeyDataResponse callback) { - delegate_.OnPlatformViewDispatchKeyDataPacket(std::move(packet), - std::move(callback)); -} - void PlatformView::DispatchSemanticsAction(int32_t id, SemanticsAction action, fml::MallocMapping args) { @@ -113,8 +107,9 @@ void PlatformView::UpdateSemantics(SemanticsNodeUpdates update, void PlatformView::HandlePlatformMessage( std::unique_ptr message) { - if (auto response = message->response()) + if (auto response = message->response()) { response->CompleteEmpty(); + } } void PlatformView::OnPreEngineRestart() const {} @@ -184,4 +179,9 @@ PlatformView::CreateSnapshotSurfaceProducer() { return nullptr; } +std::shared_ptr +PlatformView::GetPlatformMessageHandler() const { + return nullptr; +} + } // namespace flutter diff --git a/shell/common/platform_view.h b/shell/common/platform_view.h index 74baf6d2930ec..8921dfb777138 100644 --- a/shell/common/platform_view.h +++ b/shell/common/platform_view.h @@ -22,6 +22,7 @@ #include "flutter/lib/ui/window/pointer_data_packet.h" #include "flutter/lib/ui/window/pointer_data_packet_converter.h" #include "flutter/lib/ui/window/viewport_metrics.h" +#include "flutter/shell/common/platform_message_handler.h" #include "flutter/shell/common/pointer_data_dispatcher.h" #include "flutter/shell/common/vsync_waiter.h" #include "third_party/skia/include/core/SkSize.h" @@ -126,20 +127,6 @@ class PlatformView { virtual void OnPlatformViewDispatchPointerDataPacket( std::unique_ptr packet) = 0; - //-------------------------------------------------------------------------- - /// @brief Notifies the delegate that the platform view has encountered - /// a key event. This key event and the callback needs to be - /// forwarded to the running root isolate hosted by the engine - /// on the UI thread. - /// - /// @param[in] packet The key data packet containing one key event. - /// @param[in] callback Called when the framework has decided whether - /// to handle this key data. - /// - virtual void OnPlatformViewDispatchKeyDataPacket( - std::unique_ptr packet, - std::function callback) = 0; - //-------------------------------------------------------------------------- /// @brief Notifies the delegate that the platform view has encountered /// an accessibility related action on the specified node. This @@ -389,7 +376,7 @@ class PlatformView { /// may use the `DispatchPlatformMessage` method. This method is /// for messages that go the other way. /// - /// @see DisplatchPlatformMessage() + /// @see DispatchPlatformMessage() /// /// @param[in] message The message /// @@ -590,17 +577,6 @@ class PlatformView { /// void DispatchPointerDataPacket(std::unique_ptr packet); - //---------------------------------------------------------------------------- - /// @brief Dispatches key events from the embedder to the framework. Each - /// key data packet contains one physical event and multiple - /// logical key events. Each call to this method wakes up the UI - /// thread. - /// - /// @param[in] packet The key data packet to dispatch to the framework. - /// - void DispatchKeyDataPacket(std::unique_ptr packet, - Delegate::KeyDataResponse callback); - //-------------------------------------------------------------------------- /// @brief Used by the embedder to specify a texture that it wants the /// rasterizer to composite within the Flutter layer tree. All @@ -810,16 +786,26 @@ class PlatformView { virtual std::unique_ptr CreateSnapshotSurfaceProducer(); + //-------------------------------------------------------------------------- + /// @brief Specifies a delegate that will receive PlatformMessages from + /// Flutter to the host platform. + /// + /// @details If this returns `null` that means PlatformMessages should be sent + /// to the PlatformView. That is to protect legacy behavior, any embedder + /// that wants to support executing Platform Channel handlers on background + /// threads should be returing a thread-safe PlatformMessageHandler instead. + virtual std::shared_ptr GetPlatformMessageHandler() + const; + protected: + // This is the only method called on the raster task runner. + virtual std::unique_ptr CreateRenderingSurface(); + PlatformView::Delegate& delegate_; const TaskRunners task_runners_; - PointerDataPacketConverter pointer_data_packet_converter_; SkISize size_; - fml::WeakPtrFactory weak_factory_; - - // This is the only method called on the raster task runner. - virtual std::unique_ptr CreateRenderingSurface(); + fml::WeakPtrFactory weak_factory_; // Must be the last member. private: FML_DISALLOW_COPY_AND_ASSIGN(PlatformView); diff --git a/shell/common/pointer_data_dispatcher.h b/shell/common/pointer_data_dispatcher.h index 7791292611130..bfbf3a235c1d1 100644 --- a/shell/common/pointer_data_dispatcher.h +++ b/shell/common/pointer_data_dispatcher.h @@ -84,7 +84,8 @@ class PointerDataDispatcher { /// class DefaultPointerDataDispatcher : public PointerDataDispatcher { public: - DefaultPointerDataDispatcher(Delegate& delegate) : delegate_(delegate) {} + explicit DefaultPointerDataDispatcher(Delegate& delegate) + : delegate_(delegate) {} // |PointerDataDispatcer| void DispatchPacket(std::unique_ptr packet, @@ -138,7 +139,7 @@ class DefaultPointerDataDispatcher : public PointerDataDispatcher { /// See also input_events_unittests.cc where we test all our claims above. class SmoothPointerDataDispatcher : public DefaultPointerDataDispatcher { public: - SmoothPointerDataDispatcher(Delegate& delegate); + explicit SmoothPointerDataDispatcher(Delegate& delegate); // |PointerDataDispatcer| void DispatchPacket(std::unique_ptr packet, @@ -147,20 +148,18 @@ class SmoothPointerDataDispatcher : public DefaultPointerDataDispatcher { virtual ~SmoothPointerDataDispatcher(); private: + void DispatchPendingPacket(); + void ScheduleSecondaryVsyncCallback(); + // If non-null, this will be a pending pointer data packet for the next frame // to consume. This is used to smooth out the irregular drag events delivery. // See also `DispatchPointerDataPacket` and input_events_unittests.cc. std::unique_ptr pending_packet_; int pending_trace_flow_id_ = -1; - bool is_pointer_data_in_progress_ = false; + // WeakPtrFactory must be the last member. fml::WeakPtrFactory weak_factory_; - - void DispatchPendingPacket(); - - void ScheduleSecondaryVsyncCallback(); - FML_DISALLOW_COPY_AND_ASSIGN(SmoothPointerDataDispatcher); }; diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc index 9462fd6d78163..0cd00173abaa6 100644 --- a/shell/common/rasterizer.cc +++ b/shell/common/rasterizer.cc @@ -147,8 +147,6 @@ void Rasterizer::DrawLastLayerTree( if (!last_layer_tree_ || !surface_) { return; } - frame_timings_recorder->RecordRasterStart( - fml::TimePoint::Now(), &compositor_context_->raster_cache()); DrawToSurface(*frame_timings_recorder, *last_layer_tree_); } @@ -274,8 +272,9 @@ sk_sp Rasterizer::DoMakeRasterSnapshot( snapshot_surface = surface_.get(); } else if (snapshot_surface_producer_) { pbuffer_surface = snapshot_surface_producer_->CreateSnapshotSurface(); - if (pbuffer_surface && pbuffer_surface->GetContext()) + if (pbuffer_surface && pbuffer_surface->GetContext()) { snapshot_surface = pbuffer_surface.get(); + } } if (!snapshot_surface) { @@ -381,9 +380,6 @@ RasterStatus Rasterizer::DoDraw( return RasterStatus::kFailed; } - frame_timings_recorder->RecordRasterStart( - fml::TimePoint::Now(), &compositor_context_->raster_cache()); - PersistentCache* persistent_cache = PersistentCache::GetCacheForProcess(); persistent_cache->ResetStoredNewShaders(); @@ -537,15 +533,41 @@ RasterStatus Rasterizer::DrawToSurfaceUnsafe( external_view_embedder_.get(), // external view embedder root_surface_transformation, // root surface transformation true, // instrumentation enabled - frame->supports_readback(), // surface supports pixel reads - raster_thread_merger_ // thread merger + frame->framebuffer_info() + .supports_readback, // surface supports pixel reads + raster_thread_merger_ // thread merger ); if (compositor_frame) { - RasterStatus raster_status = compositor_frame->Raster(layer_tree, false); + compositor_context_->raster_cache().PrepareNewFrame(); + frame_timings_recorder.RecordRasterStart(fml::TimePoint::Now()); + + // Disable partial repaint if external_view_embedder_ SubmitFrame is + // involved - ExternalViewEmbedder unconditionally clears the entire + // surface and also partial repaint with platform view present is something + // that still need to be figured out. + bool disable_partial_repaint = + external_view_embedder_ && + (!raster_thread_merger_ || raster_thread_merger_->IsMerged()); + + FrameDamage damage; + if (!disable_partial_repaint && frame->framebuffer_info().existing_damage) { + damage.SetPreviousLayerTree(last_layer_tree_.get()); + damage.AddAdditonalDamage(*frame->framebuffer_info().existing_damage); + } + + RasterStatus raster_status = + compositor_frame->Raster(layer_tree, false, &damage); if (raster_status == RasterStatus::kFailed || raster_status == RasterStatus::kSkipAndRetry) { return raster_status; } + + SurfaceFrame::SubmitInfo submit_info; + submit_info.frame_damage = damage.GetFrameDamage(); + submit_info.buffer_damage = damage.GetBufferDamage(); + + frame->set_submit_info(submit_info); + if (external_view_embedder_ && (!raster_thread_merger_ || raster_thread_merger_->IsMerged())) { FML_DCHECK(!frame->IsSubmitted()); @@ -555,6 +577,7 @@ RasterStatus Rasterizer::DrawToSurfaceUnsafe( frame->Submit(); } + compositor_context_->raster_cache().CleanupAfterFrame(); frame_timings_recorder.RecordRasterEnd( &compositor_context_->raster_cache()); FireNextFrameCallbackIfPresent(); @@ -586,7 +609,7 @@ static sk_sp ScreenshotLayerTreeAsPicture( auto frame = compositor_context.AcquireFrame( nullptr, recorder.getRecordingCanvas(), nullptr, root_surface_transformation, false, true, nullptr); - frame->Raster(*tree, true); + frame->Raster(*tree, true, nullptr); #if defined(OS_FUCHSIA) SkSerialProcs procs = {0}; @@ -654,7 +677,7 @@ sk_sp Rasterizer::ScreenshotLayerTreeAsImage( root_surface_transformation, false, true, nullptr); canvas->clear(SK_ColorTRANSPARENT); - frame->Raster(*tree, true); + frame->Raster(*tree, true, nullptr); canvas->flush(); // Prepare an image from the surface, this image may potentially be on th GPU. diff --git a/shell/common/rasterizer.h b/shell/common/rasterizer.h index b05cabc5c11a8..6fe2ffb5f9edc 100644 --- a/shell/common/rasterizer.h +++ b/shell/common/rasterizer.h @@ -105,7 +105,7 @@ class Rasterizer final : public SnapshotDelegate { /// /// @param[in] delegate The rasterizer delegate. /// - Rasterizer(Delegate& delegate); + explicit Rasterizer(Delegate& delegate); //---------------------------------------------------------------------------- /// @brief Destroys the rasterizer. This must happen on the raster task @@ -448,22 +448,6 @@ class Rasterizer final : public SnapshotDelegate { void DisableThreadMergerIfNeeded(); private: - Delegate& delegate_; - std::unique_ptr surface_; - std::unique_ptr snapshot_surface_producer_; - std::unique_ptr compositor_context_; - // This is the last successfully rasterized layer tree. - std::unique_ptr last_layer_tree_; - // Set when we need attempt to rasterize the layer tree again. This layer_tree - // has not successfully rasterized. This can happen due to the change in the - // thread configuration. This will be inserted to the front of the pipeline. - std::unique_ptr resubmitted_layer_tree_; - fml::closure next_frame_callback_; - bool user_override_resource_cache_bytes_; - std::optional max_cache_bytes_; - fml::RefPtr raster_thread_merger_; - fml::TaskRunnerAffineWeakPtrFactory weak_factory_; - std::shared_ptr external_view_embedder_; // |SnapshotDelegate| sk_sp MakeRasterSnapshot( std::function draw_callback, @@ -500,6 +484,24 @@ class Rasterizer final : public SnapshotDelegate { static bool NoDiscard(const flutter::LayerTree& layer_tree) { return false; } + Delegate& delegate_; + std::unique_ptr surface_; + std::unique_ptr snapshot_surface_producer_; + std::unique_ptr compositor_context_; + // This is the last successfully rasterized layer tree. + std::unique_ptr last_layer_tree_; + // Set when we need attempt to rasterize the layer tree again. This layer_tree + // has not successfully rasterized. This can happen due to the change in the + // thread configuration. This will be inserted to the front of the pipeline. + std::unique_ptr resubmitted_layer_tree_; + fml::closure next_frame_callback_; + bool user_override_resource_cache_bytes_; + std::optional max_cache_bytes_; + fml::RefPtr raster_thread_merger_; + std::shared_ptr external_view_embedder_; + + // WeakPtrFactory must be the last member. + fml::TaskRunnerAffineWeakPtrFactory weak_factory_; FML_DISALLOW_COPY_AND_ASSIGN(Rasterizer); }; diff --git a/shell/common/rasterizer_unittests.cc b/shell/common/rasterizer_unittests.cc index 5d082a68eb493..78f96ed36a99b 100644 --- a/shell/common/rasterizer_unittests.cc +++ b/shell/common/rasterizer_unittests.cc @@ -138,8 +138,11 @@ TEST(RasterizerTest, std::make_shared(); rasterizer->SetExternalViewEmbedder(external_view_embedder); + SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; + auto surface_frame = std::make_unique( - /*surface=*/nullptr, /*supports_readback=*/true, + /*surface=*/nullptr, framebuffer_info, /*submit_callback=*/[](const SurfaceFrame&, SkCanvas*) { return true; }); EXPECT_CALL(*surface, AllowsDrawingWhenGpuDisabled()).WillOnce(Return(true)); EXPECT_CALL(*surface, AcquireFrame(SkISize())) @@ -199,8 +202,10 @@ TEST( rasterizer->SetExternalViewEmbedder(external_view_embedder); EXPECT_CALL(*external_view_embedder, SupportsDynamicThreadMerging) .WillRepeatedly(Return(true)); + SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; auto surface_frame = std::make_unique( - /*surface=*/nullptr, /*supports_readback=*/true, + /*surface=*/nullptr, framebuffer_info, /*submit_callback=*/[](const SurfaceFrame&, SkCanvas*) { return true; }); EXPECT_CALL(*surface, AllowsDrawingWhenGpuDisabled()).WillOnce(Return(true)); EXPECT_CALL(*surface, AcquireFrame(SkISize())) @@ -260,8 +265,11 @@ TEST( std::make_shared(); rasterizer->SetExternalViewEmbedder(external_view_embedder); + SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; + auto surface_frame = std::make_unique( - /*surface=*/nullptr, /*supports_readback=*/true, + /*surface=*/nullptr, framebuffer_info, /*submit_callback=*/[](const SurfaceFrame&, SkCanvas*) { return true; }); EXPECT_CALL(*surface, AllowsDrawingWhenGpuDisabled()).WillOnce(Return(true)); EXPECT_CALL(*surface, AcquireFrame(SkISize())) @@ -349,8 +357,10 @@ TEST(RasterizerTest, auto is_gpu_disabled_sync_switch = std::make_shared(false); + SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; auto surface_frame = std::make_unique( - /*surface=*/nullptr, /*supports_readback=*/true, + /*surface=*/nullptr, /*framebuffer_info=*/framebuffer_info, /*submit_callback=*/[](const SurfaceFrame&, SkCanvas*) { return true; }); EXPECT_CALL(*surface, AllowsDrawingWhenGpuDisabled()).WillOnce(Return(true)); ON_CALL(delegate, GetIsGpuDisabledSyncSwitch()) @@ -397,8 +407,11 @@ TEST( auto is_gpu_disabled_sync_switch = std::make_shared(true); + SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; + auto surface_frame = std::make_unique( - /*surface=*/nullptr, /*supports_readback=*/true, + /*surface=*/nullptr, /*framebuffer_info=*/framebuffer_info, /*submit_callback=*/[](const SurfaceFrame&, SkCanvas*) { return true; }); EXPECT_CALL(*surface, AllowsDrawingWhenGpuDisabled()).WillOnce(Return(true)); ON_CALL(delegate, GetIsGpuDisabledSyncSwitch()) @@ -447,8 +460,11 @@ TEST( auto is_gpu_disabled_sync_switch = std::make_shared(false); + SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; + auto surface_frame = std::make_unique( - /*surface=*/nullptr, /*supports_readback=*/true, + /*surface=*/nullptr, /*framebuffer_info=*/framebuffer_info, /*submit_callback=*/[](const SurfaceFrame&, SkCanvas*) { return true; }); EXPECT_CALL(*surface, AllowsDrawingWhenGpuDisabled()).WillOnce(Return(false)); EXPECT_CALL(delegate, GetIsGpuDisabledSyncSwitch()) @@ -496,8 +512,11 @@ TEST( auto is_gpu_disabled_sync_switch = std::make_shared(true); + SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; + auto surface_frame = std::make_unique( - /*surface=*/nullptr, /*supports_readback=*/true, + /*surface=*/nullptr, /*framebuffer_info=*/framebuffer_info, /*submit_callback=*/[](const SurfaceFrame&, SkCanvas*) { return true; }); EXPECT_CALL(*surface, AllowsDrawingWhenGpuDisabled()).WillOnce(Return(false)); EXPECT_CALL(delegate, GetIsGpuDisabledSyncSwitch()) diff --git a/shell/common/run_configuration.cc b/shell/common/run_configuration.cc index beace53a40e34..0d905e2791eae 100644 --- a/shell/common/run_configuration.cc +++ b/shell/common/run_configuration.cc @@ -77,6 +77,11 @@ void RunConfiguration::SetEntrypointAndLibrary(std::string entrypoint, entrypoint_library_ = std::move(library); } +void RunConfiguration::SetEntrypointArgs( + const std::vector& entrypoint_args) { + entrypoint_args_ = entrypoint_args; +} + std::shared_ptr RunConfiguration::GetAssetManager() const { return asset_manager_; } @@ -89,6 +94,10 @@ const std::string& RunConfiguration::GetEntrypointLibrary() const { return entrypoint_library_; } +const std::vector& RunConfiguration::GetEntrypointArgs() const { + return entrypoint_args_; +} + std::unique_ptr RunConfiguration::TakeIsolateConfiguration() { return std::move(isolate_configuration_); diff --git a/shell/common/run_configuration.h b/shell/common/run_configuration.h index cb5a5b531d248..ee680fd6aba75 100644 --- a/shell/common/run_configuration.h +++ b/shell/common/run_configuration.h @@ -66,7 +66,8 @@ class RunConfiguration { /// /// @param[in] configuration The configuration /// - RunConfiguration(std::unique_ptr configuration); + explicit RunConfiguration( + std::unique_ptr configuration); //---------------------------------------------------------------------------- /// @brief Creates a run configuration with the specified isolate @@ -150,6 +151,12 @@ class RunConfiguration { /// void SetEntrypointAndLibrary(std::string entrypoint, std::string library); + //---------------------------------------------------------------------------- + /// @brief Updates the main application entrypoint arguments. + /// + /// @param[in] entrypoint_args The entrypoint arguments to use. + void SetEntrypointArgs(const std::vector& entrypoint_args); + //---------------------------------------------------------------------------- /// @return The asset manager referencing all previously registered asset /// resolvers. @@ -167,6 +174,12 @@ class RunConfiguration { /// const std::string& GetEntrypointLibrary() const; + //---------------------------------------------------------------------------- + /// @return Arguments passed as a List to Dart's entrypoint + /// function. + /// + const std::vector& GetEntrypointArgs() const; + //---------------------------------------------------------------------------- /// @brief The engine uses this to take the isolate configuration from /// the run configuration. The run configuration is no longer @@ -183,6 +196,7 @@ class RunConfiguration { std::shared_ptr asset_manager_; std::string entrypoint_ = "main"; std::string entrypoint_library_ = ""; + std::vector entrypoint_args_; FML_DISALLOW_COPY_AND_ASSIGN(RunConfiguration); }; diff --git a/shell/common/shell.cc b/shell/common/shell.cc index 9143525be7874..f7c91472facab 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -484,35 +484,33 @@ std::unique_ptr Shell::Spawn( const CreateCallback& on_create_platform_view, const CreateCallback& on_create_rasterizer) const { FML_DCHECK(task_runners_.IsValid()); - auto shell_maker = [&](bool is_gpu_disabled) { - std::unique_ptr result(CreateWithSnapshot( - PlatformData{}, task_runners_, rasterizer_->GetRasterThreadMerger(), - GetSettings(), vm_, vm_->GetVMData()->GetIsolateSnapshot(), - on_create_platform_view, on_create_rasterizer, - [engine = this->engine_.get(), initial_route]( - Engine::Delegate& delegate, - const PointerDataDispatcherMaker& dispatcher_maker, DartVM& vm, - fml::RefPtr isolate_snapshot, - TaskRunners task_runners, const PlatformData& platform_data, - Settings settings, std::unique_ptr animator, - fml::WeakPtr io_manager, - fml::RefPtr unref_queue, - fml::WeakPtr snapshot_delegate, - std::shared_ptr volatile_path_tracker) { - return engine->Spawn(/*delegate=*/delegate, - /*dispatcher_maker=*/dispatcher_maker, - /*settings=*/settings, - /*animator=*/std::move(animator), - /*initial_route=*/initial_route); - }, - is_gpu_disabled)); - return result; - }; - std::unique_ptr result; + // It's safe to store this value since it is set on the platform thread. + bool is_gpu_disabled = false; GetIsGpuDisabledSyncSwitch()->Execute( fml::SyncSwitch::Handlers() - .SetIfFalse([&] { result = shell_maker(false); }) - .SetIfTrue([&] { result = shell_maker(true); })); + .SetIfFalse([&is_gpu_disabled] { is_gpu_disabled = false; }) + .SetIfTrue([&is_gpu_disabled] { is_gpu_disabled = true; })); + std::unique_ptr result = (CreateWithSnapshot( + PlatformData{}, task_runners_, rasterizer_->GetRasterThreadMerger(), + GetSettings(), vm_, vm_->GetVMData()->GetIsolateSnapshot(), + on_create_platform_view, on_create_rasterizer, + [engine = this->engine_.get(), initial_route]( + Engine::Delegate& delegate, + const PointerDataDispatcherMaker& dispatcher_maker, DartVM& vm, + fml::RefPtr isolate_snapshot, + TaskRunners task_runners, const PlatformData& platform_data, + Settings settings, std::unique_ptr animator, + fml::WeakPtr io_manager, + fml::RefPtr unref_queue, + fml::WeakPtr snapshot_delegate, + std::shared_ptr volatile_path_tracker) { + return engine->Spawn(/*delegate=*/delegate, + /*dispatcher_maker=*/dispatcher_maker, + /*settings=*/settings, + /*animator=*/std::move(animator), + /*initial_route=*/initial_route); + }, + is_gpu_disabled)); result->shared_resource_context_ = io_manager_->GetSharedResourceContext(); result->RunEngine(std::move(run_configuration)); return result; @@ -625,6 +623,7 @@ bool Shell::Setup(std::unique_ptr platform_view, } platform_view_ = std::move(platform_view); + platform_message_handler_ = platform_view_->GetPlatformMessageHandler(); engine_ = std::move(engine); rasterizer_ = std::move(rasterizer); io_manager_ = std::move(io_manager); @@ -733,16 +732,11 @@ void Shell::OnPlatformViewCreated(std::unique_ptr surface) { const bool should_post_raster_task = !task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread(); - // Note: - // This is a synchronous operation because certain platforms depend on - // setup/suspension of all activities that may be interacting with the GPU in - // a synchronous fashion. fml::AutoResetWaitableEvent latch; auto raster_task = fml::MakeCopyable([&waiting_for_first_frame = waiting_for_first_frame_, rasterizer = rasterizer_->GetWeakPtr(), // - surface = std::move(surface), // - &latch]() mutable { + surface = std::move(surface)]() mutable { if (rasterizer) { // Enables the thread merger which may be used by the external view // embedder. @@ -751,29 +745,15 @@ void Shell::OnPlatformViewCreated(std::unique_ptr surface) { } waiting_for_first_frame.store(true); - - // Step 3: All done. Signal the latch that the platform thread is - // waiting on. - latch.Signal(); }); - auto ui_task = [engine = engine_->GetWeakPtr(), // - raster_task_runner = task_runners_.GetRasterTaskRunner(), // - raster_task, should_post_raster_task, - &latch // - ] { + // TODO(91717): This probably isn't necessary. The engine should be able to + // handle things here via normal lifecycle messages. + // https://github.com/flutter/flutter/issues/91717 + auto ui_task = [engine = engine_->GetWeakPtr()] { if (engine) { engine->OnOutputSurfaceCreated(); } - // 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_raster_task, in this case we just unblock - // the platform thread. - latch.Signal(); - } }; // Threading: Capture platform view by raw pointer and not the weak pointer. @@ -786,7 +766,9 @@ void Shell::OnPlatformViewCreated(std::unique_ptr surface) { auto io_task = [io_manager = io_manager_->GetWeakPtr(), platform_view, ui_task_runner = task_runners_.GetUITaskRunner(), ui_task, - shared_resource_context = shared_resource_context_] { + shared_resource_context = shared_resource_context_, + raster_task_runner = task_runners_.GetRasterTaskRunner(), + raster_task, should_post_raster_task, &latch] { if (io_manager && !io_manager->GetResourceContext()) { sk_sp resource_context; if (shared_resource_context) { @@ -796,9 +778,16 @@ void Shell::OnPlatformViewCreated(std::unique_ptr surface) { } io_manager->NotifyResourceContextAvailable(resource_context); } - // Step 1: Next, post a task on the UI thread to tell the engine that it has + // Step 1: Post a task on the UI thread to tell the engine that it has // an output surface. fml::TaskRunner::RunNowOrPostTask(ui_task_runner, ui_task); + + // Step 2: 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); + } + latch.Signal(); }; fml::TaskRunner::RunNowOrPostTask(task_runners_.GetIOTaskRunner(), io_task); @@ -826,26 +815,15 @@ void Shell::OnPlatformViewDestroyed() { // configuration is changed by a task, and the assumption is no longer true. // // This incorrect assumption can lead to deadlock. - // See `should_post_raster_task` for more. rasterizer_->DisableThreadMergerIfNeeded(); - // 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 - // 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 platform/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. - const bool should_post_raster_task = - !task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread(); - // Note: // This is a synchronous operation because certain platforms depend on // setup/suspension of all activities that may be interacting with the GPU in // a synchronous fashion. + // The UI thread does not need to be serialized here - there is sufficient + // guardrailing in the rasterizer to allow the UI thread to post work to it + // even after the surface has been torn down. fml::AutoResetWaitableEvent latch; @@ -855,7 +833,7 @@ void Shell::OnPlatformViewDestroyed() { io_manager->GetIsGpuDisabledSyncSwitch()->Execute( fml::SyncSwitch::Handlers().SetIfFalse( [&] { io_manager->GetSkiaUnrefQueue()->Drain(); })); - // Step 3: All done. Signal the latch that the platform thread is waiting + // Step 4: All done. Signal the latch that the platform thread is waiting // on. latch.Signal(); }; @@ -870,37 +848,28 @@ void Shell::OnPlatformViewDestroyed() { rasterizer->EnableThreadMergerIfNeeded(); rasterizer->Teardown(); } - // Step 2: Next, tell the IO thread to complete its remaining work. + // Step 3: Tell the IO thread to complete its remaining work. fml::TaskRunner::RunNowOrPostTask(io_task_runner, io_task); }; - auto ui_task = [engine = engine_->GetWeakPtr(), - raster_task_runner = task_runners_.GetRasterTaskRunner(), - raster_task, should_post_raster_task, &latch]() { + // TODO(91717): This probably isn't necessary. The engine should be able to + // handle things here via normal lifecycle messages. + // https://github.com/flutter/flutter/issues/91717 + auto ui_task = [engine = engine_->GetWeakPtr()]() { if (engine) { engine->OnOutputSurfaceDestroyed(); } - if (should_post_raster_task) { - fml::TaskRunner::RunNowOrPostTask(raster_task_runner, raster_task); - } else { - // See comment on should_post_raster_task, in this case we just unblock - // the platform thread. - latch.Signal(); - } }; - // Step 0: Post a task onto the UI thread to tell the engine that its output + // Step 1: Post a task onto the UI thread to tell the engine that its output // surface is about to go away. fml::TaskRunner::RunNowOrPostTask(task_runners_.GetUITaskRunner(), ui_task); + // Step 2: Post a task to the Raster thread (possibly this thread) to tell the + // rasterizer the output surface is going away. + fml::TaskRunner::RunNowOrPostTask(task_runners_.GetRasterTaskRunner(), + raster_task); latch.Wait(); - 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 raster thread. - raster_task(); - latch.Wait(); - } } // |PlatformView::Delegate| @@ -973,23 +942,6 @@ void Shell::OnPlatformViewDispatchPointerDataPacket( next_pointer_flow_id_++; } -// |PlatformView::Delegate| -void Shell::OnPlatformViewDispatchKeyDataPacket( - std::unique_ptr packet, - std::function callback) { - TRACE_EVENT0("flutter", "Shell::OnPlatformViewDispatchKeyDataPacket"); - FML_DCHECK(is_setup_); - FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread()); - - task_runners_.GetUITaskRunner()->PostTask( - fml::MakeCopyable([engine = weak_engine_, packet = std::move(packet), - callback = std::move(callback)]() mutable { - if (engine) { - engine->DispatchKeyDataPacket(std::move(packet), std::move(callback)); - } - })); -} - // |PlatformView::Delegate| void Shell::OnPlatformViewDispatchSemanticsAction(int32_t id, SemanticsAction action, @@ -1222,13 +1174,17 @@ void Shell::OnEngineHandlePlatformMessage( return; } - task_runners_.GetPlatformTaskRunner()->PostTask( - fml::MakeCopyable([view = platform_view_->GetWeakPtr(), - message = std::move(message)]() mutable { - if (view) { - view->HandlePlatformMessage(std::move(message)); - } - })); + if (platform_message_handler_) { + platform_message_handler_->HandlePlatformMessage(std::move(message)); + } else { + task_runners_.GetPlatformTaskRunner()->PostTask( + fml::MakeCopyable([view = platform_view_->GetWeakPtr(), + message = std::move(message)]() mutable { + if (view) { + view->HandlePlatformMessage(std::move(message)); + } + })); + } } void Shell::HandleEngineSkiaMessage(std::unique_ptr message) { @@ -1237,15 +1193,18 @@ void Shell::HandleEngineSkiaMessage(std::unique_ptr message) { rapidjson::Document document; document.Parse(reinterpret_cast(data.GetMapping()), data.GetSize()); - if (document.HasParseError() || !document.IsObject()) + if (document.HasParseError() || !document.IsObject()) { return; + } auto root = document.GetObject(); auto method = root.FindMember("method"); - if (method->value != "Skia.setResourceCacheMaxBytes") + if (method->value != "Skia.setResourceCacheMaxBytes") { return; + } auto args = root.FindMember("args"); - if (args == root.MemberEnd() || !args->value.IsInt()) + if (args == root.MemberEnd() || !args->value.IsInt()) { return; + } task_runners_.GetRasterTaskRunner()->PostTask( [rasterizer = rasterizer_->GetWeakPtr(), max_bytes = args->value.GetInt(), @@ -1391,6 +1350,7 @@ void Shell::OnFrameRasterized(const FrameTiming& timing) { } size_t old_count = unreported_timings_.size(); + (void)old_count; for (auto phase : FrameTiming::kPhases) { unreported_timings_.push_back( timing.Get(phase).ToEpochDelta().ToMicroseconds()); @@ -1596,6 +1556,7 @@ bool Shell::OnServiceProtocolRunInView( configuration.SetEntrypointAndLibrary(engine_->GetLastEntrypoint(), engine_->GetLastEntrypointLibrary()); + configuration.SetEntrypointArgs(engine_->GetLastEntrypointArgs()); configuration.AddAssetResolver(std::make_unique( fml::OpenDirectory(asset_directory_path.c_str(), false, @@ -1865,6 +1826,7 @@ std::shared_ptr Shell::GetIsGpuDisabledSyncSwitch() } void Shell::SetGpuAvailability(GpuAvailability availability) { + FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread()); switch (availability) { case GpuAvailability::kAvailable: is_gpu_disabled_sync_switch_->SetSwitch(false); @@ -1889,12 +1851,17 @@ void Shell::SetGpuAvailability(GpuAvailability availability) { } void Shell::OnDisplayUpdates(DisplayUpdateType update_type, - std::vector displays) { - display_manager_->HandleDisplayUpdates(update_type, displays); + std::vector> displays) { + display_manager_->HandleDisplayUpdates(update_type, std::move(displays)); } fml::TimePoint Shell::GetCurrentTimePoint() { return fml::TimePoint::Now(); } +const std::shared_ptr& +Shell::GetPlatformMessageHandler() const { + return platform_message_handler_; +} + } // namespace flutter diff --git a/shell/common/shell.h b/shell/common/shell.h index 9fbdd5b1a423f..87ba86cb83ac5 100644 --- a/shell/common/shell.h +++ b/shell/common/shell.h @@ -373,7 +373,7 @@ class Shell final : public PlatformView::Delegate, /// @brief Notifies the display manager of the updates. /// void OnDisplayUpdates(DisplayUpdateType update_type, - std::vector displays); + std::vector> displays); //---------------------------------------------------------------------------- /// @brief Queries the `DisplayManager` for the main display refresh rate. @@ -395,6 +395,12 @@ class Shell final : public PlatformView::Delegate, /// @see `CreateCompatibleGenerator` void RegisterImageDecoder(ImageGeneratorFactory factory, int32_t priority); + //---------------------------------------------------------------------------- + /// @brief Returns the delegate object that handles PlatformMessage's from + /// Flutter to the host platform (and its responses). + const std::shared_ptr& GetPlatformMessageHandler() + const; + private: using ServiceProtocolHandler = std::function io_manager_; // on IO task runner std::shared_ptr is_gpu_disabled_sync_switch_; std::shared_ptr volatile_path_tracker_; + std::shared_ptr platform_message_handler_; fml::WeakPtr weak_engine_; // to be shared across threads fml::TaskRunnerAffineWeakPtr @@ -518,11 +525,6 @@ class Shell final : public PlatformView::Delegate, void OnPlatformViewDispatchPointerDataPacket( std::unique_ptr packet) override; - // |PlatformView::Delegate| - void OnPlatformViewDispatchKeyDataPacket( - std::unique_ptr packet, - std::function callback) override; - // |PlatformView::Delegate| void OnPlatformViewDispatchSemanticsAction(int32_t id, SemanticsAction action, diff --git a/shell/common/shell_test.cc b/shell/common/shell_test.cc index b5cc9c8e3ae52..b986d580c080f 100644 --- a/shell/common/shell_test.cc +++ b/shell/common/shell_test.cc @@ -325,7 +325,8 @@ std::unique_ptr ShellTest::CreateShell( std::shared_ptr shell_test_external_view_embedder, bool is_gpu_disabled, - ShellTestPlatformView::BackendType rendering_backend) { + ShellTestPlatformView::BackendType rendering_backend, + Shell::CreateCallback platform_view_create_callback) { const auto vsync_clock = std::make_shared(); CreateVsyncWaiter create_vsync_waiter = [&]() { @@ -338,21 +339,21 @@ std::unique_ptr ShellTest::CreateShell( } }; - Shell::CreateCallback platfrom_view_create_callback = - [vsync_clock, // - &create_vsync_waiter, // - shell_test_external_view_embedder, // - rendering_backend // - ](Shell& shell) { - return ShellTestPlatformView::Create( - shell, // - shell.GetTaskRunners(), // - vsync_clock, // - std::move(create_vsync_waiter), // - rendering_backend, // - shell_test_external_view_embedder // - ); - }; + if (!platform_view_create_callback) { + platform_view_create_callback = [vsync_clock, // + &create_vsync_waiter, // + shell_test_external_view_embedder, // + rendering_backend // + ](Shell& shell) { + return ShellTestPlatformView::Create(shell, // + shell.GetTaskRunners(), // + vsync_clock, // + std::move(create_vsync_waiter), // + rendering_backend, // + shell_test_external_view_embedder // + ); + }; + } Shell::CreateCallback rasterizer_create_callback = [](Shell& shell) { return std::make_unique(shell); }; @@ -360,7 +361,7 @@ std::unique_ptr ShellTest::CreateShell( return Shell::Create(flutter::PlatformData(), // task_runners, // settings, // - platfrom_view_create_callback, // + platform_view_create_callback, // rasterizer_create_callback, // is_gpu_disabled // ); diff --git a/shell/common/shell_test.h b/shell/common/shell_test.h index 1524c150e377a..4a41060c75ee9 100644 --- a/shell/common/shell_test.h +++ b/shell/common/shell_test.h @@ -43,7 +43,9 @@ class ShellTest : public FixtureTest { shell_test_external_view_embedder = nullptr, bool is_gpu_disabled = false, ShellTestPlatformView::BackendType rendering_backend = - ShellTestPlatformView::BackendType::kDefaultBackend); + ShellTestPlatformView::BackendType::kDefaultBackend, + Shell::CreateCallback platform_view_create_callback = + nullptr); void DestroyShell(std::unique_ptr shell); void DestroyShell(std::unique_ptr shell, TaskRunners task_runners); TaskRunners GetTaskRunnersForFixture(); diff --git a/shell/common/shell_test_platform_view_vulkan.cc b/shell/common/shell_test_platform_view_vulkan.cc index 754133c514f0e..df1baf6fa49d5 100644 --- a/shell/common/shell_test_platform_view_vulkan.cc +++ b/shell/common/shell_test_platform_view_vulkan.cc @@ -180,8 +180,11 @@ ShellTestPlatformViewVulkan::OffScreenSurface::AcquireFrame( return true; }; - return std::make_unique(std::move(surface), true, - std::move(callback)); + SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; + + return std::make_unique( + std::move(surface), std::move(framebuffer_info), std::move(callback)); } GrDirectContext* ShellTestPlatformViewVulkan::OffScreenSurface::GetContext() { diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index c8922a172dfe7..2aacb4b2f815e 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -45,6 +45,10 @@ namespace flutter { namespace testing { + +using ::testing::_; +using ::testing::Return; + namespace { class MockPlatformViewDelegate : public PlatformView::Delegate { MOCK_METHOD1(OnPlatformViewCreated, void(std::unique_ptr surface)); @@ -63,10 +67,6 @@ class MockPlatformViewDelegate : public PlatformView::Delegate { MOCK_METHOD1(OnPlatformViewDispatchPointerDataPacket, void(std::unique_ptr packet)); - MOCK_METHOD2(OnPlatformViewDispatchKeyDataPacket, - void(std::unique_ptr packet, - KeyDataResponse callback)); - MOCK_METHOD3(OnPlatformViewDispatchSemanticsAction, void(int32_t id, SemanticsAction action, @@ -119,6 +119,27 @@ class MockPlatformView : public PlatformView { MockPlatformView(MockPlatformViewDelegate& delegate, TaskRunners task_runners) : PlatformView(delegate, task_runners) {} MOCK_METHOD0(CreateRenderingSurface, std::unique_ptr()); + MOCK_CONST_METHOD0(GetPlatformMessageHandler, + std::shared_ptr()); +}; + +class MockPlatformMessageHandler : public PlatformMessageHandler { + public: + MOCK_METHOD1(HandlePlatformMessage, + void(std::unique_ptr message)); + MOCK_METHOD2(InvokePlatformMessageResponseCallback, + void(int response_id, std::unique_ptr mapping)); + MOCK_METHOD1(InvokePlatformMessageEmptyResponseCallback, + void(int response_id)); +}; + +class MockPlatformMessageResponse : public PlatformMessageResponse { + public: + static fml::RefPtr Create() { + return fml::AdoptRef(new MockPlatformMessageResponse()); + } + MOCK_METHOD1(Complete, void(std::unique_ptr data)); + MOCK_METHOD0(CompleteEmpty, void()); }; } // namespace @@ -439,6 +460,39 @@ TEST_F(ShellTest, LastEntrypoint) { ASSERT_FALSE(DartVMRef::IsInstanceRunning()); } +TEST_F(ShellTest, LastEntrypointArgs) { + ASSERT_FALSE(DartVMRef::IsInstanceRunning()); + auto settings = CreateSettingsForFixture(); + auto shell = CreateShell(settings); + ASSERT_TRUE(ValidateShell(shell.get())); + + auto configuration = RunConfiguration::InferFromSettings(settings); + ASSERT_TRUE(configuration.IsValid()); + std::string entry_point = "fixturesAreFunctionalMain"; + std::vector entry_point_args = {"arg1"}; + configuration.SetEntrypoint(entry_point); + configuration.SetEntrypointArgs(entry_point_args); + + fml::AutoResetWaitableEvent main_latch; + std::vector last_entry_point_args; + AddNativeCallback( + "SayHiFromFixturesAreFunctionalMain", CREATE_NATIVE_ENTRY([&](auto args) { + last_entry_point_args = shell->GetEngine()->GetLastEntrypointArgs(); + main_latch.Signal(); + })); + + RunEngine(shell.get(), std::move(configuration)); + main_latch.Wait(); +#if (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG) + EXPECT_EQ(last_entry_point_args, entry_point_args); +#else + ASSERT_TRUE(last_entry_point_args.empty()); +#endif + ASSERT_TRUE(DartVMRef::IsInstanceRunning()); + DestroyShell(std::move(shell)); + ASSERT_FALSE(DartVMRef::IsInstanceRunning()); +} + TEST_F(ShellTest, #if defined(WINUWP) // TODO(cbracken): https://github.com/flutter/flutter/issues/90481 @@ -1513,8 +1567,11 @@ TEST_F(ShellTest, SetResourceCacheSize) { PumpOneFrame(shell.get()); // The Vulkan and GL backends set different default values for the resource - // cache size. -#ifdef SHELL_ENABLE_VULKAN + // cache size. The default backend (specified by the default param of + // `CreateShell` in this test) will only resolve to Vulkan (in + // `ShellTestPlatformView::Create`) if GL is disabled. This situation arises + // when targeting the Fuchsia Emulator. +#if defined(SHELL_ENABLE_VULKAN) && !defined(SHELL_ENABLE_GL) EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell), vulkan::kGrCacheMaxByteSize); #else @@ -1852,7 +1909,7 @@ TEST_F(ShellTest, CanConvertToAndFromMappings) { const size_t buffer_size = 2 << 20; uint8_t* buffer = static_cast(::malloc(buffer_size)); - ASSERT_NE(buffer, nullptr); + ASSERT_TRUE(buffer != nullptr); ASSERT_TRUE(MemsetPatternSetOrCheck( buffer, buffer_size, MemsetPatternOp::kMemsetPatternOpSetBuffer)); @@ -2799,6 +2856,113 @@ TEST_F(ShellTest, Spawn) { ASSERT_FALSE(DartVMRef::IsInstanceRunning()); } +TEST_F(ShellTest, SpawnWithDartEntrypointArgs) { + auto settings = CreateSettingsForFixture(); + auto shell = CreateShell(settings); + ASSERT_TRUE(ValidateShell(shell.get())); + + auto configuration = RunConfiguration::InferFromSettings(settings); + ASSERT_TRUE(configuration.IsValid()); + configuration.SetEntrypoint("canRecieveArgumentsWhenEngineRun"); + const std::vector entrypoint_args{"foo", "bar"}; + configuration.SetEntrypointArgs(entrypoint_args); + + auto second_configuration = RunConfiguration::InferFromSettings(settings); + ASSERT_TRUE(second_configuration.IsValid()); + second_configuration.SetEntrypoint("canRecieveArgumentsWhenEngineSpawn"); + const std::vector second_entrypoint_args{"arg1", "arg2"}; + second_configuration.SetEntrypointArgs(second_entrypoint_args); + + const std::string initial_route("/foo"); + + fml::AutoResetWaitableEvent main_latch; + std::string last_entry_point; + // Fulfill native function for the first Shell's entrypoint. + AddNativeCallback("NotifyNativeWhenEngineRun", + CREATE_NATIVE_ENTRY(([&](Dart_NativeArguments args) { + ASSERT_TRUE(tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0))); + last_entry_point = + shell->GetEngine()->GetLastEntrypoint(); + main_latch.Signal(); + }))); + + fml::AutoResetWaitableEvent second_latch; + // Fulfill native function for the second Shell's entrypoint. + AddNativeCallback("NotifyNativeWhenEngineSpawn", + CREATE_NATIVE_ENTRY(([&](Dart_NativeArguments args) { + ASSERT_TRUE(tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0))); + last_entry_point = + shell->GetEngine()->GetLastEntrypoint(); + second_latch.Signal(); + }))); + + RunEngine(shell.get(), std::move(configuration)); + main_latch.Wait(); + ASSERT_TRUE(DartVMRef::IsInstanceRunning()); + // Check first Shell ran the first entrypoint. + ASSERT_EQ("canRecieveArgumentsWhenEngineRun", last_entry_point); + + PostSync( + shell->GetTaskRunners().GetPlatformTaskRunner(), + [this, &spawner = shell, &second_configuration, &second_latch, + initial_route]() { + MockPlatformViewDelegate platform_view_delegate; + auto spawn = spawner->Spawn( + std::move(second_configuration), initial_route, + [&platform_view_delegate](Shell& shell) { + auto result = std::make_unique( + platform_view_delegate, shell.GetTaskRunners()); + ON_CALL(*result, CreateRenderingSurface()) + .WillByDefault(::testing::Invoke( + [] { return std::make_unique(); })); + return result; + }, + [](Shell& shell) { return std::make_unique(shell); }); + ASSERT_NE(nullptr, spawn.get()); + ASSERT_TRUE(ValidateShell(spawn.get())); + + PostSync(spawner->GetTaskRunners().GetUITaskRunner(), + [&spawn, &spawner, initial_route] { + // Check second shell ran the second entrypoint. + ASSERT_EQ("canRecieveArgumentsWhenEngineSpawn", + spawn->GetEngine()->GetLastEntrypoint()); + ASSERT_EQ(initial_route, spawn->GetEngine()->InitialRoute()); + + // TODO(74520): Remove conditional once isolate groups are + // supported by JIT. + if (DartVM::IsRunningPrecompiledCode()) { + ASSERT_NE(spawner->GetEngine() + ->GetRuntimeController() + ->GetRootIsolateGroup(), + 0u); + ASSERT_EQ(spawner->GetEngine() + ->GetRuntimeController() + ->GetRootIsolateGroup(), + spawn->GetEngine() + ->GetRuntimeController() + ->GetRootIsolateGroup()); + } + }); + + PostSync( + spawner->GetTaskRunners().GetIOTaskRunner(), [&spawner, &spawn] { + ASSERT_EQ(spawner->GetIOManager()->GetResourceContext().get(), + spawn->GetIOManager()->GetResourceContext().get()); + }); + + // Before destroying the shell, wait for expectations of the spawned + // isolate to be met. + second_latch.Wait(); + + DestroyShell(std::move(spawn)); + }); + + DestroyShell(std::move(shell)); + ASSERT_FALSE(DartVMRef::IsInstanceRunning()); +} + TEST_F(ShellTest, UpdateAssetResolverByTypeReplaces) { ASSERT_FALSE(DartVMRef::IsInstanceRunning()); Settings settings = CreateSettingsForFixture(); @@ -3068,8 +3232,7 @@ TEST_F(ShellTest, UserTagSetOnStartup) { TEST_F(ShellTest, PrefetchDefaultFontManager) { auto settings = CreateSettingsForFixture(); settings.prefetched_default_font_manager = true; - - auto shell = CreateShell(std::move(settings)); + std::unique_ptr shell; auto get_font_manager_count = [&] { fml::AutoResetWaitableEvent latch; @@ -3084,8 +3247,17 @@ TEST_F(ShellTest, PrefetchDefaultFontManager) { latch.Wait(); return font_manager_count; }; + size_t initial_font_manager_count = 0; + settings.root_isolate_create_callback = [&](const auto& isolate) { + ASSERT_GT(initial_font_manager_count, 0ul); + // Should not have fetched the default font manager yet, since the root + // isolate was only just created. + ASSERT_EQ(get_font_manager_count(), initial_font_manager_count); + }; + + shell = CreateShell(std::move(settings)); - size_t initial_font_manager_count = get_font_manager_count(); + initial_font_manager_count = get_font_manager_count(); auto configuration = RunConfiguration::InferFromSettings(settings); configuration.SetEntrypoint("emptyMain"); @@ -3098,5 +3270,110 @@ TEST_F(ShellTest, PrefetchDefaultFontManager) { DestroyShell(std::move(shell)); } +TEST_F(ShellTest, OnPlatformViewCreatedWhenUIThreadIsBusy) { + // This test will deadlock if the threading logic in + // Shell::OnCreatePlatformView is wrong. + auto settings = CreateSettingsForFixture(); + auto shell = CreateShell(std::move(settings)); + + fml::AutoResetWaitableEvent latch; + fml::TaskRunner::RunNowOrPostTask(shell->GetTaskRunners().GetUITaskRunner(), + [&latch]() { latch.Wait(); }); + + ShellTest::PlatformViewNotifyCreated(shell.get()); + latch.Signal(); + + DestroyShell(std::move(shell)); +} + +TEST_F(ShellTest, UIWorkAfterOnPlatformViewDestroyed) { + auto settings = CreateSettingsForFixture(); + auto shell = CreateShell(std::move(settings)); + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("drawFrames"); + + fml::AutoResetWaitableEvent latch; + fml::AutoResetWaitableEvent notify_native_latch; + AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&](auto args) { + notify_native_latch.Signal(); + latch.Wait(); + })); + + RunEngine(shell.get(), std::move(configuration)); + // Wait to make sure we get called back from Dart and thus have latched + // the UI thread before we create/destroy the platform view. + notify_native_latch.Wait(); + + ShellTest::PlatformViewNotifyCreated(shell.get()); + + fml::AutoResetWaitableEvent destroy_latch; + fml::TaskRunner::RunNowOrPostTask( + shell->GetTaskRunners().GetPlatformTaskRunner(), + [&shell, &destroy_latch]() { + shell->GetPlatformView()->NotifyDestroyed(); + destroy_latch.Signal(); + }); + + destroy_latch.Wait(); + + // Unlatch the UI thread and let it send us a scene to render. + latch.Signal(); + + // Flush the UI task runner to make sure we process the render/scheduleFrame + // request. + fml::AutoResetWaitableEvent ui_flush_latch; + fml::TaskRunner::RunNowOrPostTask( + shell->GetTaskRunners().GetUITaskRunner(), + [&ui_flush_latch]() { ui_flush_latch.Signal(); }); + ui_flush_latch.Wait(); + DestroyShell(std::move(shell)); +} + +TEST_F(ShellTest, UsesPlatformMessageHandler) { + TaskRunners task_runners = GetTaskRunnersForFixture(); + auto settings = CreateSettingsForFixture(); + MockPlatformViewDelegate platform_view_delegate; + auto platform_message_handler = + std::make_shared(); + int message_id = 1; + EXPECT_CALL(*platform_message_handler, HandlePlatformMessage(_)); + EXPECT_CALL(*platform_message_handler, + InvokePlatformMessageEmptyResponseCallback(message_id)); + Shell::CreateCallback platform_view_create_callback = + [&platform_view_delegate, task_runners, + platform_message_handler](flutter::Shell& shell) { + auto result = std::make_unique(platform_view_delegate, + task_runners); + EXPECT_CALL(*result, GetPlatformMessageHandler()) + .WillOnce(Return(platform_message_handler)); + return result; + }; + auto shell = CreateShell( + /*settings=*/std::move(settings), + /*task_runners=*/task_runners, + /*simulate_vsync=*/false, + /*shell_test_external_view_embedder=*/nullptr, + /*is_gpu_disabled=*/false, + /*rendering_backend=*/ + ShellTestPlatformView::BackendType::kDefaultBackend, + /*platform_view_create_callback=*/platform_view_create_callback); + + EXPECT_EQ(platform_message_handler, shell->GetPlatformMessageHandler()); + PostSync(task_runners.GetUITaskRunner(), [&shell]() { + size_t data_size = 4; + fml::MallocMapping bytes = + fml::MallocMapping(static_cast(malloc(data_size)), data_size); + fml::RefPtr response = + MockPlatformMessageResponse::Create(); + auto message = std::make_unique( + /*channel=*/"foo", /*data=*/std::move(bytes), /*response=*/response); + (static_cast(shell.get())) + ->OnEngineHandlePlatformMessage(std::move(message)); + }); + shell->GetPlatformMessageHandler() + ->InvokePlatformMessageEmptyResponseCallback(message_id); + DestroyShell(std::move(shell)); +} + } // namespace testing } // namespace flutter diff --git a/shell/common/switches.cc b/shell/common/switches.cc index cb5bda824e981..38d4ece83a2fc 100644 --- a/shell/common/switches.cc +++ b/shell/common/switches.cc @@ -200,7 +200,7 @@ static bool GetSwitchValue(const fml::CommandLine& command_line, std::unique_ptr GetSymbolMapping(std::string symbol_prefix, std::string native_lib_path) { - const uint8_t* mapping; + const uint8_t* mapping = nullptr; intptr_t size; auto lookup_symbol = [&mapping, &size, symbol_prefix]( @@ -334,6 +334,13 @@ Settings SettingsFromCommandLine(const fml::CommandLine& command_line) { std::vector aot_shared_library_name = command_line.GetOptionValues(FlagForSwitch(Switch::AotSharedLibraryName)); + std::vector vmservice_shared_library_name = + command_line.GetOptionValues( + FlagForSwitch(Switch::AotVMServiceSharedLibraryName)); + for (auto path : vmservice_shared_library_name) { + settings.vmservice_snapshot_library_path.emplace_back(path); + } + std::string snapshot_asset_path; command_line.GetOptionValue(FlagForSwitch(Switch::SnapshotAssetPath), &snapshot_asset_path); diff --git a/shell/common/switches.h b/shell/common/switches.h index 6226e8d7e9f6f..0a44e6cc587c7 100644 --- a/shell/common/switches.h +++ b/shell/common/switches.h @@ -28,6 +28,10 @@ DEF_SWITCHES_START DEF_SWITCH(AotSharedLibraryName, "aot-shared-library-name", "Name of the *.so containing AOT compiled Dart assets.") +DEF_SWITCH(AotVMServiceSharedLibraryName, + "aot-vmservice-shared-library-name", + "Name of the *.so containing AOT compiled Dart assets for " + "launching the service isolate.") DEF_SWITCH(SnapshotAssetPath, "snapshot-asset-path", "Path to the directory containing the four files specified by " diff --git a/shell/common/vsync_waiter_fallback.cc b/shell/common/vsync_waiter_fallback.cc index 120e9a78551de..cb042c5e082e6 100644 --- a/shell/common/vsync_waiter_fallback.cc +++ b/shell/common/vsync_waiter_fallback.cc @@ -17,8 +17,9 @@ static fml::TimePoint SnapToNextTick(fml::TimePoint value, fml::TimePoint tick_phase, fml::TimeDelta tick_interval) { fml::TimeDelta offset = (tick_phase - value) % tick_interval; - if (offset != fml::TimeDelta::Zero()) + if (offset != fml::TimeDelta::Zero()) { offset = offset + tick_interval; + } return value + offset; } diff --git a/shell/common/vsync_waiters_test.h b/shell/common/vsync_waiters_test.h index f2d313621a66b..66f474b765b82 100644 --- a/shell/common/vsync_waiters_test.h +++ b/shell/common/vsync_waiters_test.h @@ -49,7 +49,7 @@ class ConstantFiringVsyncWaiter : public VsyncWaiter { static constexpr fml::TimePoint frame_target_time = fml::TimePoint::FromEpochDelta(fml::TimeDelta::FromSeconds(100)); - ConstantFiringVsyncWaiter(TaskRunners task_runners) + explicit ConstantFiringVsyncWaiter(TaskRunners task_runners) : VsyncWaiter(std::move(task_runners)) {} protected: diff --git a/shell/config.gni b/shell/config.gni index 781727443e305..85dd360cbc036 100644 --- a/shell/config.gni +++ b/shell/config.gni @@ -4,16 +4,19 @@ declare_args() { shell_enable_gl = !is_fuchsia - shell_enable_metal = false - # TODO(dworsham): Enable once Fuchsia supports Vulkan through the embedder. + # The logic for enabling Vulkan and Metal is in tools/gn. + shell_enable_metal = false shell_enable_vulkan = false + shell_enable_software = true } declare_args() { test_enable_gl = shell_enable_gl test_enable_metal = shell_enable_metal - test_enable_vulkan = is_fuchsia + + # The Vulkan unittests are combined with the GL unittests. + test_enable_vulkan = is_fuchsia || shell_enable_gl test_enable_software = shell_enable_software } diff --git a/shell/gpu/gpu_surface_gl.cc b/shell/gpu/gpu_surface_gl.cc index 9d06aa9d35715..6e81d0a0ae4f0 100644 --- a/shell/gpu/gpu_surface_gl.cc +++ b/shell/gpu/gpu_surface_gl.cc @@ -129,7 +129,7 @@ static SkColorType FirstSupportedColorType(GrDirectContext* context, static sk_sp WrapOnscreenSurface(GrDirectContext* context, const SkISize& size, intptr_t fbo) { - GrGLenum format; + GrGLenum format = kUnknown_SkColorType; const SkColorType color_type = FirstSupportedColorType(context, &format); GrGLFramebufferInfo framebuffer_info = {}; @@ -216,11 +216,15 @@ std::unique_ptr GPUSurfaceGL::AcquireFrame(const SkISize& size) { return nullptr; } + SurfaceFrame::FramebufferInfo framebuffer_info; + // TODO(38466): Refactor GPU surface APIs take into account the fact that an // external view embedder may want to render to the root surface. if (!render_to_surface_) { + framebuffer_info.supports_readback = true; return std::make_unique( - nullptr, true, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { + nullptr, std::move(framebuffer_info), + [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); } @@ -241,9 +245,10 @@ std::unique_ptr GPUSurfaceGL::AcquireFrame(const SkISize& size) { return weak ? weak->PresentSurface(canvas) : false; }; - return std::make_unique( - surface, delegate_->SurfaceSupportsReadback(), submit_callback, - std::move(context_switch)); + framebuffer_info = delegate_->GLContextFramebufferInfo(); + return std::make_unique(surface, std::move(framebuffer_info), + submit_callback, + std::move(context_switch)); } bool GPUSurfaceGL::PresentSurface(SkCanvas* canvas) { diff --git a/shell/gpu/gpu_surface_gl.h b/shell/gpu/gpu_surface_gl.h index e4b04d31f9ed5..fd641149decec 100644 --- a/shell/gpu/gpu_surface_gl.h +++ b/shell/gpu/gpu_surface_gl.h @@ -54,6 +54,14 @@ class GPUSurfaceGL : public Surface { bool AllowsDrawingWhenGpuDisabled() const override; private: + bool CreateOrUpdateSurfaces(const SkISize& size); + + sk_sp AcquireRenderSurface( + const SkISize& untransformed_size, + const SkMatrix& root_surface_transformation); + + bool PresentSurface(SkCanvas* canvas); + GPUSurfaceGLDelegate* delegate_; sk_sp context_; sk_sp onscreen_surface_; @@ -66,16 +74,9 @@ class GPUSurfaceGL : public Surface { // external view embedder is present. const bool render_to_surface_ = true; bool valid_ = false; - fml::TaskRunnerAffineWeakPtrFactory weak_factory_; - - bool CreateOrUpdateSurfaces(const SkISize& size); - - sk_sp AcquireRenderSurface( - const SkISize& untransformed_size, - const SkMatrix& root_surface_transformation); - - bool PresentSurface(SkCanvas* canvas); + // WeakPtrFactory must be the last member. + fml::TaskRunnerAffineWeakPtrFactory weak_factory_; FML_DISALLOW_COPY_AND_ASSIGN(GPUSurfaceGL); }; diff --git a/shell/gpu/gpu_surface_gl_delegate.cc b/shell/gpu/gpu_surface_gl_delegate.cc index 70bdfc03ba4bb..2edd3823bdadf 100644 --- a/shell/gpu/gpu_surface_gl_delegate.cc +++ b/shell/gpu/gpu_surface_gl_delegate.cc @@ -16,8 +16,11 @@ bool GPUSurfaceGLDelegate::GLContextFBOResetAfterPresent() const { return false; } -bool GPUSurfaceGLDelegate::SurfaceSupportsReadback() const { - return true; +SurfaceFrame::FramebufferInfo GPUSurfaceGLDelegate::GLContextFramebufferInfo() + const { + SurfaceFrame::FramebufferInfo res; + res.supports_readback = true; + return res; } SkMatrix GPUSurfaceGLDelegate::GLContextSurfaceTransformation() const { diff --git a/shell/gpu/gpu_surface_gl_delegate.h b/shell/gpu/gpu_surface_gl_delegate.h index fb13c8212390c..58a600347feb6 100644 --- a/shell/gpu/gpu_surface_gl_delegate.h +++ b/shell/gpu/gpu_surface_gl_delegate.h @@ -45,9 +45,8 @@ class GPUSurfaceGLDelegate { // rendering subsequent frames. virtual bool GLContextFBOResetAfterPresent() const; - // Indicates whether or not the surface supports pixel readback as used in - // circumstances such as a BackdropFilter. - virtual bool SurfaceSupportsReadback() const; + // Returns framebuffer info for current backbuffer + virtual SurfaceFrame::FramebufferInfo GLContextFramebufferInfo() const; // A transformation applied to the onscreen surface before the canvas is // flushed. diff --git a/shell/gpu/gpu_surface_metal.h b/shell/gpu/gpu_surface_metal.h index 8899089383df5..cc180ec5f4e6e 100644 --- a/shell/gpu/gpu_surface_metal.h +++ b/shell/gpu/gpu_surface_metal.h @@ -28,7 +28,6 @@ class SK_API_AVAILABLE_CA_METAL_LAYER GPUSurfaceMetal : public Surface { private: const GPUSurfaceMetalDelegate* delegate_; const MTLRenderTargetType render_target_type_; - GrMTLHandle next_drawable_ = nullptr; sk_sp context_; GrDirectContext* precompiled_sksl_context_ = nullptr; // TODO(38466): Refactor GPU surface APIs take into account the fact that an @@ -37,6 +36,10 @@ class SK_API_AVAILABLE_CA_METAL_LAYER GPUSurfaceMetal : public Surface { // external view embedder is present. bool render_to_surface_; + // Accumulated damage for each framebuffer; Key is address of underlying + // MTLTexture for each drawable + std::map damage_; + // |Surface| std::unique_ptr AcquireFrame(const SkISize& size) override; @@ -58,8 +61,6 @@ class SK_API_AVAILABLE_CA_METAL_LAYER GPUSurfaceMetal : public Surface { std::unique_ptr AcquireFrameFromMTLTexture( const SkISize& frame_info); - void ReleaseUnusedDrawableIfNecessary(); - void PrecompileKnownSkSLsIfNecessary(); FML_DISALLOW_COPY_AND_ASSIGN(GPUSurfaceMetal); diff --git a/shell/gpu/gpu_surface_metal.mm b/shell/gpu/gpu_surface_metal.mm index d3ec678d8bb5e..b9e92bb9b1db8 100644 --- a/shell/gpu/gpu_surface_metal.mm +++ b/shell/gpu/gpu_surface_metal.mm @@ -5,10 +5,12 @@ #include "flutter/shell/gpu/gpu_surface_metal.h" #import +#import #include "flutter/common/graphics/persistent_cache.h" #include "flutter/fml/make_copyable.h" #include "flutter/fml/platform/darwin/cf_utils.h" +#include "flutter/fml/platform/darwin/scoped_nsobject.h" #include "flutter/fml/trace_event.h" #include "flutter/shell/gpu/gpu_surface_metal_delegate.h" #include "third_party/skia/include/core/SkSurface.h" @@ -19,6 +21,22 @@ namespace flutter { +namespace { +sk_sp CreateSurfaceFromMetalTexture(GrDirectContext* context, + id texture, + GrSurfaceOrigin origin, + int sample_cnt, + SkColorType color_type, + sk_sp color_space, + const SkSurfaceProps* props) { + GrMtlTextureInfo info; + info.fTexture.reset([texture retain]); + GrBackendTexture backend_texture(texture.width, texture.height, GrMipmapped::kNo, info); + return SkSurface::MakeFromBackendTexture(context, backend_texture, origin, sample_cnt, color_type, + color_space, props); +} +} // namespace + GPUSurfaceMetal::GPUSurfaceMetal(GPUSurfaceMetalDelegate* delegate, sk_sp context, bool render_to_surface) @@ -27,9 +45,7 @@ context_(std::move(context)), render_to_surface_(render_to_surface) {} -GPUSurfaceMetal::~GPUSurfaceMetal() { - ReleaseUnusedDrawableIfNecessary(); -} +GPUSurfaceMetal::~GPUSurfaceMetal() = default; // |Surface| bool GPUSurfaceMetal::IsValid() { @@ -60,7 +76,8 @@ if (!render_to_surface_) { return std::make_unique( - nullptr, true, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); + nullptr, SurfaceFrame::FramebufferInfo(), + [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); } PrecompileKnownSkSLsIfNecessary(); @@ -85,24 +102,31 @@ return nullptr; } - ReleaseUnusedDrawableIfNecessary(); - sk_sp surface = - SkSurface::MakeFromCAMetalLayer(context_.get(), // context - layer, // layer - kTopLeft_GrSurfaceOrigin, // origin - 1, // sample count - kBGRA_8888_SkColorType, // color type - nullptr, // colorspace - nullptr, // surface properties - &next_drawable_ // drawable (transfer out) - ); + auto* mtl_layer = (CAMetalLayer*)layer; + // Get the drawable eagerly, we will need texture object to identify target framebuffer + fml::scoped_nsprotocol> drawable( + reinterpret_cast>([[mtl_layer nextDrawable] retain])); + + if (!drawable.get()) { + FML_LOG(ERROR) << "Could not obtain drawable from the metal layer."; + return nullptr; + } + + auto surface = CreateSurfaceFromMetalTexture(context_.get(), drawable.get().texture, + kTopLeft_GrSurfaceOrigin, // origin + 1, // sample count + kBGRA_8888_SkColorType, // color type + nullptr, // colorspace + nullptr // surface properties + ); if (!surface) { FML_LOG(ERROR) << "Could not create the SkSurface from the CAMetalLayer."; return nullptr; } - auto submit_callback = [this](const SurfaceFrame& surface_frame, SkCanvas* canvas) -> bool { + auto submit_callback = [this, drawable](const SurfaceFrame& surface_frame, + SkCanvas* canvas) -> bool { TRACE_EVENT0("flutter", "GPUSurfaceMetal::Submit"); if (canvas == nullptr) { FML_DLOG(ERROR) << "Canvas not available."; @@ -111,16 +135,33 @@ canvas->flush(); - GrMTLHandle drawable = next_drawable_; - if (!drawable) { - FML_DLOG(ERROR) << "Unable to obtain a metal drawable."; - return false; + uintptr_t texture = reinterpret_cast(drawable.get().texture); + for (auto& entry : damage_) { + if (entry.first != texture) { + // Accumulate damage for other framebuffers + if (surface_frame.submit_info().frame_damage) { + entry.second.join(*surface_frame.submit_info().frame_damage); + } + } } + // Reset accumulated damage for current framebuffer + damage_[texture] = SkIRect::MakeEmpty(); return delegate_->PresentDrawable(drawable); }; - return std::make_unique(std::move(surface), true, submit_callback); + SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; + + // Provide accumulated damage to rasterizer (area in current framebuffer that lags behind + // front buffer) + uintptr_t texture = reinterpret_cast(drawable.get().texture); + auto i = damage_.find(texture); + if (i != damage_.end()) { + framebuffer_info.existing_damage = i->second; + } + + return std::make_unique(std::move(surface), framebuffer_info, submit_callback); } std::unique_ptr GPUSurfaceMetal::AcquireFrameFromMTLTexture( @@ -133,13 +174,9 @@ return nullptr; } - GrMtlTextureInfo info; - info.fTexture.reset([mtl_texture retain]); - GrBackendTexture backend_texture(frame_info.width(), frame_info.height(), GrMipmapped::kNo, info); - sk_sp surface = - SkSurface::MakeFromBackendTexture(context_.get(), backend_texture, kTopLeft_GrSurfaceOrigin, - 1, kBGRA_8888_SkColorType, nullptr, nullptr); + CreateSurfaceFromMetalTexture(context_.get(), mtl_texture, kTopLeft_GrSurfaceOrigin, 1, + kBGRA_8888_SkColorType, nullptr, nullptr); if (!surface) { FML_LOG(ERROR) << "Could not create the SkSurface from the metal texture."; @@ -159,7 +196,11 @@ return delegate->PresentTexture(texture); }; - return std::make_unique(std::move(surface), true, submit_callback); + SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; + + return std::make_unique(std::move(surface), std::move(framebuffer_info), + submit_callback); } // |Surface| @@ -188,16 +229,4 @@ return delegate_->AllowsDrawingWhenGpuDisabled(); } -void GPUSurfaceMetal::ReleaseUnusedDrawableIfNecessary() { - // If the previous surface frame was not submitted before a new one is acquired, the old drawable - // needs to be released. An RAII wrapper may not be used because this needs to interoperate with - // Skia APIs. - if (next_drawable_ == nullptr) { - return; - } - - CFRelease(next_drawable_); - next_drawable_ = nullptr; -} - } // namespace flutter diff --git a/shell/gpu/gpu_surface_software.cc b/shell/gpu/gpu_surface_software.cc index 4d079e282d019..252f47fad16c2 100644 --- a/shell/gpu/gpu_surface_software.cc +++ b/shell/gpu/gpu_surface_software.cc @@ -25,11 +25,15 @@ bool GPUSurfaceSoftware::IsValid() { // |Surface| std::unique_ptr GPUSurfaceSoftware::AcquireFrame( const SkISize& logical_size) { + SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; + // TODO(38466): Refactor GPU surface APIs take into account the fact that an // external view embedder may want to render to the root surface. if (!render_to_surface_) { return std::make_unique( - nullptr, true, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { + nullptr, std::move(framebuffer_info), + [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); } @@ -69,7 +73,8 @@ std::unique_ptr GPUSurfaceSoftware::AcquireFrame( return self->delegate_->PresentBackingStore(surface_frame.SkiaSurface()); }; - return std::make_unique(backing_store, true, on_submit); + return std::make_unique(backing_store, + std::move(framebuffer_info), on_submit); } // |Surface| diff --git a/shell/gpu/gpu_surface_software.h b/shell/gpu/gpu_surface_software.h index 23d5cdedf8fed..18bc8d5b41446 100644 --- a/shell/gpu/gpu_surface_software.h +++ b/shell/gpu/gpu_surface_software.h @@ -39,7 +39,6 @@ class GPUSurfaceSoftware : public Surface { // external view embedder is present. const bool render_to_surface_; fml::TaskRunnerAffineWeakPtrFactory weak_factory_; - FML_DISALLOW_COPY_AND_ASSIGN(GPUSurfaceSoftware); }; diff --git a/shell/gpu/gpu_surface_vulkan.cc b/shell/gpu/gpu_surface_vulkan.cc index 37432503f7087..570783bffa13d 100644 --- a/shell/gpu/gpu_surface_vulkan.cc +++ b/shell/gpu/gpu_surface_vulkan.cc @@ -37,11 +37,15 @@ bool GPUSurfaceVulkan::IsValid() { std::unique_ptr GPUSurfaceVulkan::AcquireFrame( const SkISize& size) { + SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; + // TODO(38466): Refactor GPU surface APIs take into account the fact that an // external view embedder may want to render to the root surface. if (!render_to_surface_) { return std::make_unique( - nullptr, true, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { + nullptr, std::move(framebuffer_info), + [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); } @@ -63,8 +67,8 @@ std::unique_ptr GPUSurfaceVulkan::AcquireFrame( } return weak_this->window_.SwapBuffers(); }; - return std::make_unique(std::move(surface), true, - std::move(callback)); + return std::make_unique( + std::move(surface), std::move(framebuffer_info), std::move(callback)); } SkMatrix GPUSurfaceVulkan::GetRootTransformation() const { diff --git a/shell/gpu/gpu_surface_vulkan.h b/shell/gpu/gpu_surface_vulkan.h index 242ecef3d3d06..50c420e38790b 100644 --- a/shell/gpu/gpu_surface_vulkan.h +++ b/shell/gpu/gpu_surface_vulkan.h @@ -55,7 +55,6 @@ class GPUSurfaceVulkan : public Surface { const bool render_to_surface_; fml::WeakPtrFactory weak_factory_; - FML_DISALLOW_COPY_AND_ASSIGN(GPUSurfaceVulkan); }; diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 2780a734ef590..aaffebd273d89 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -64,6 +64,8 @@ source_set("flutter_shell_native_src") { "$root_build_dir/flutter_icu/icudtl.o", "android_context_gl.cc", "android_context_gl.h", + "android_display.cc", + "android_display.h", "android_environment_gl.cc", "android_environment_gl.h", "android_external_texture_gl.cc", @@ -79,6 +81,8 @@ source_set("flutter_shell_native_src") { "flutter_main.cc", "flutter_main.h", "library_loader.cc", + "platform_message_handler_android.cc", + "platform_message_handler_android.h", "platform_message_response_android.cc", "platform_message_response_android.h", "platform_view_android.cc", @@ -165,6 +169,7 @@ android_java_sources = [ "io/flutter/embedding/android/FlutterFragment.java", "io/flutter/embedding/android/FlutterFragmentActivity.java", "io/flutter/embedding/android/FlutterImageView.java", + "io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java", "io/flutter/embedding/android/FlutterSplashView.java", "io/flutter/embedding/android/FlutterSurfaceView.java", "io/flutter/embedding/android/FlutterTextureView.java", @@ -186,6 +191,7 @@ android_java_sources = [ "io/flutter/embedding/engine/dart/DartExecutor.java", "io/flutter/embedding/engine/dart/DartMessenger.java", "io/flutter/embedding/engine/dart/PlatformMessageHandler.java", + "io/flutter/embedding/engine/dart/PlatformTaskQueue.java", "io/flutter/embedding/engine/deferredcomponents/DeferredComponentManager.java", "io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java", "io/flutter/embedding/engine/loader/ApplicationInfoLoader.java", @@ -440,6 +446,17 @@ action("android_jar") { ":pom_embedding", ":pom_libflutter", ] + + if (flutter_runtime_mode == "profile") { + deps += [ "//flutter/shell/vmservice:vmservice_snapshot" ] + args += [ + "--native_lib", + rebase_path( + "$root_gen_dir/flutter/shell/vmservice/android/libs/$android_app_abi/libvmservice_snapshot.so", + root_build_dir, + root_build_dir), + ] + } } action("pom_libflutter") { @@ -529,7 +546,7 @@ zip_bundle("flutter_jar_zip") { } action("gen_android_javadoc") { - script = "//flutter/tools/gen_javadoc.py" + script = "//flutter/tools/javadoc/gen_javadoc.py" sources = android_java_sources + embedding_dependencies_jars outputs = [ "$target_gen_dir/javadocs" ] diff --git a/shell/platform/android/android_context_gl.cc b/shell/platform/android/android_context_gl.cc index 1d0f83cafa7e7..42ab9cd656dba 100644 --- a/shell/platform/android/android_context_gl.cc +++ b/shell/platform/android/android_context_gl.cc @@ -148,10 +148,12 @@ SkISize AndroidEGLSurface::GetSize() const { AndroidContextGL::AndroidContextGL( AndroidRenderingAPI rendering_api, - fml::RefPtr environment) + fml::RefPtr environment, + const TaskRunners& task_runners) : AndroidContext(AndroidRenderingAPI::kOpenGLES), environment_(environment), - config_(nullptr) { + config_(nullptr), + task_runners_(task_runners) { if (!environment_->IsValid()) { FML_LOG(ERROR) << "Could not create an Android GL environment."; return; @@ -189,6 +191,26 @@ AndroidContextGL::AndroidContextGL( } AndroidContextGL::~AndroidContextGL() { + FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread()); + sk_sp main_context = GetMainSkiaContext(); + SetMainSkiaContext(nullptr); + fml::AutoResetWaitableEvent latch; + // This context needs to be deallocated from the raster thread in order to + // keep a coherent usage of egl from a single thread. + fml::TaskRunner::RunNowOrPostTask(task_runners_.GetRasterTaskRunner(), [&] { + if (main_context) { + std::unique_ptr pbuffer_surface = + CreatePbufferSurface(); + if (pbuffer_surface->MakeCurrent()) { + main_context->releaseResourcesAndAbandonContext(); + main_context.reset(); + ClearCurrent(); + } + } + latch.Signal(); + }); + latch.Wait(); + if (!TeardownContext(environment_->Display(), context_)) { FML_LOG(ERROR) << "Could not tear down the EGL context. Possible resource leak."; diff --git a/shell/platform/android/android_context_gl.h b/shell/platform/android/android_context_gl.h index b9e34ecf6f854..08668e488ca4d 100644 --- a/shell/platform/android/android_context_gl.h +++ b/shell/platform/android/android_context_gl.h @@ -72,7 +72,8 @@ class AndroidEGLSurface { class AndroidContextGL : public AndroidContext { public: AndroidContextGL(AndroidRenderingAPI rendering_api, - fml::RefPtr environment); + fml::RefPtr environment, + const TaskRunners& taskRunners); ~AndroidContextGL(); @@ -131,6 +132,7 @@ class AndroidContextGL : public AndroidContext { EGLContext context_; EGLContext resource_context_; bool valid_ = false; + TaskRunners task_runners_; FML_DISALLOW_COPY_AND_ASSIGN(AndroidContextGL); }; diff --git a/shell/platform/android/android_context_gl_unittests.cc b/shell/platform/android/android_context_gl_unittests.cc index f31b023643c9c..2278acd6b37eb 100644 --- a/shell/platform/android/android_context_gl_unittests.cc +++ b/shell/platform/android/android_context_gl_unittests.cc @@ -1,11 +1,67 @@ +#define FML_USED_ON_EMBEDDER + #include +#include "flutter/shell/common/thread_host.h" #include "flutter/shell/platform/android/android_context_gl.h" #include "flutter/shell/platform/android/android_environment_gl.h" #include "gtest/gtest.h" +namespace flutter { +namespace testing { +namespace android { +namespace { +TaskRunners MakeTaskRunners(const std::string& thread_label, + const ThreadHost& thread_host) { + fml::MessageLoop::EnsureInitializedForCurrentThread(); + fml::RefPtr platform_runner = + fml::MessageLoop::GetCurrent().GetTaskRunner(); + + return TaskRunners(thread_label, platform_runner, + thread_host.raster_thread->GetTaskRunner(), + thread_host.ui_thread->GetTaskRunner(), + thread_host.io_thread->GetTaskRunner()); +} +} // namespace + TEST(AndroidContextGl, Create) { - auto environment = fml::MakeRefCounted(); - auto context = std::make_unique( - flutter::AndroidRenderingAPI::kOpenGLES, environment); + GrMockOptions main_context_options; + sk_sp main_context = + GrDirectContext::MakeMock(&main_context_options); + auto environment = fml::MakeRefCounted(); + std::string thread_label = + ::testing::UnitTest::GetInstance()->current_test_info()->name(); + ThreadHost thread_host(thread_label, ThreadHost::Type::UI | + ThreadHost::Type::RASTER | + ThreadHost::Type::IO); + TaskRunners task_runners = MakeTaskRunners(thread_label, thread_host); + auto context = std::make_unique( + AndroidRenderingAPI::kOpenGLES, environment, task_runners); + context->SetMainSkiaContext(main_context); + EXPECT_NE(context.get(), nullptr); + context.reset(); + EXPECT_TRUE(main_context->abandoned()); +} + +TEST(AndroidContextGl, CreateSingleThread) { + GrMockOptions main_context_options; + sk_sp main_context = + GrDirectContext::MakeMock(&main_context_options); + auto environment = fml::MakeRefCounted(); + std::string thread_label = + ::testing::UnitTest::GetInstance()->current_test_info()->name(); + fml::MessageLoop::EnsureInitializedForCurrentThread(); + fml::RefPtr platform_runner = + fml::MessageLoop::GetCurrent().GetTaskRunner(); + TaskRunners task_runners = + TaskRunners(thread_label, platform_runner, platform_runner, + platform_runner, platform_runner); + auto context = std::make_unique( + AndroidRenderingAPI::kOpenGLES, environment, task_runners); + context->SetMainSkiaContext(main_context); EXPECT_NE(context.get(), nullptr); + context.reset(); + EXPECT_TRUE(main_context->abandoned()); } +} // namespace android +} // namespace testing +} // namespace flutter diff --git a/shell/platform/android/android_display.cc b/shell/platform/android/android_display.cc new file mode 100644 index 0000000000000..edd378bff15d6 --- /dev/null +++ b/shell/platform/android/android_display.cc @@ -0,0 +1,19 @@ +// 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/android/android_display.h" +#include "android_display.h" + +namespace flutter { + +AndroidDisplay::AndroidDisplay( + std::shared_ptr jni_facade) + : Display(jni_facade->GetDisplayRefreshRate()), + jni_facade_(std::move(jni_facade)) {} + +double AndroidDisplay::GetRefreshRate() const { + return jni_facade_->GetDisplayRefreshRate(); +} + +} // namespace flutter diff --git a/shell/platform/android/android_display.h b/shell/platform/android/android_display.h new file mode 100644 index 0000000000000..524779af2f067 --- /dev/null +++ b/shell/platform/android/android_display.h @@ -0,0 +1,33 @@ +// 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_ANDROID_DISPLAY_H_ +#define FLUTTER_SHELL_PLATFORM_ANDROID_DISPLAY_H_ + +#include + +#include "flutter/fml/macros.h" +#include "flutter/shell/common/display.h" +#include "flutter/shell/platform/android/jni/platform_view_android_jni.h" + +namespace flutter { + +/// A |Display| that listens to refresh rate changes. +class AndroidDisplay : public Display { + public: + explicit AndroidDisplay(std::shared_ptr jni_facade); + ~AndroidDisplay() = default; + + // |Display| + double GetRefreshRate() const override; + + private: + std::shared_ptr jni_facade_; + + FML_DISALLOW_COPY_AND_ASSIGN(AndroidDisplay); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_ANDROID_DISPLAY_H_ diff --git a/shell/platform/android/android_shell_holder.cc b/shell/platform/android/android_shell_holder.cc index b85db32a6d1cc..1634f55b0de0f 100644 --- a/shell/platform/android/android_shell_holder.cc +++ b/shell/platform/android/android_shell_holder.cc @@ -25,6 +25,7 @@ #include "flutter/shell/common/rasterizer.h" #include "flutter/shell/common/run_configuration.h" #include "flutter/shell/common/thread_host.h" +#include "flutter/shell/platform/android/android_display.h" #include "flutter/shell/platform/android/android_image_generator.h" #include "flutter/shell/platform/android/context/android_context.h" #include "flutter/shell/platform/android/platform_view_android.h" @@ -61,8 +62,10 @@ AndroidShellHolder::AndroidShellHolder( .enable_software_rendering // use software rendering ); weak_platform_view = platform_view_android->GetWeakPtr(); - auto display = Display(jni_facade->GetDisplayRefreshRate()); - shell.OnDisplayUpdates(DisplayUpdateType::kStartup, {display}); + std::vector> displays; + displays.push_back(std::make_unique(jni_facade)); + shell.OnDisplayUpdates(DisplayUpdateType::kStartup, + std::move(displays)); return platform_view_android; }; @@ -175,7 +178,8 @@ std::unique_ptr AndroidShellHolder::Spawn( std::shared_ptr jni_facade, const std::string& entrypoint, const std::string& libraryUrl, - const std::string& initial_route) const { + const std::string& initial_route, + const std::vector& entrypoint_args) const { FML_DCHECK(shell_ && shell_->IsSetup()) << "A new Shell can only be spawned " "if the current Shell is properly constructed"; @@ -209,8 +213,10 @@ std::unique_ptr AndroidShellHolder::Spawn( android_context // Android context ); weak_platform_view = platform_view_android->GetWeakPtr(); - auto display = Display(jni_facade->GetDisplayRefreshRate()); - shell.OnDisplayUpdates(DisplayUpdateType::kStartup, {display}); + std::vector> displays; + displays.push_back(std::make_unique(jni_facade)); + shell.OnDisplayUpdates(DisplayUpdateType::kStartup, + std::move(displays)); return platform_view_android; }; @@ -220,7 +226,8 @@ std::unique_ptr AndroidShellHolder::Spawn( // TODO(xster): could be worth tracing this to investigate whether // the IsolateConfiguration could be cached somewhere. - auto config = BuildRunConfiguration(asset_manager_, entrypoint, libraryUrl); + auto config = BuildRunConfiguration(asset_manager_, entrypoint, libraryUrl, + entrypoint_args); if (!config) { // If the RunConfiguration was null, the kernel blob wasn't readable. // Fail the whole thing. @@ -236,15 +243,18 @@ std::unique_ptr AndroidShellHolder::Spawn( std::move(shell), weak_platform_view)); } -void AndroidShellHolder::Launch(std::shared_ptr asset_manager, - const std::string& entrypoint, - const std::string& libraryUrl) { +void AndroidShellHolder::Launch( + std::shared_ptr asset_manager, + const std::string& entrypoint, + const std::string& libraryUrl, + const std::vector& entrypoint_args) { if (!IsValid()) { return; } asset_manager_ = asset_manager; - auto config = BuildRunConfiguration(asset_manager, entrypoint, libraryUrl); + auto config = BuildRunConfiguration(asset_manager, entrypoint, libraryUrl, + entrypoint_args); if (!config) { return; } @@ -273,7 +283,8 @@ void AndroidShellHolder::NotifyLowMemoryWarning() { std::optional AndroidShellHolder::BuildRunConfiguration( std::shared_ptr asset_manager, const std::string& entrypoint, - const std::string& libraryUrl) const { + const std::string& libraryUrl, + const std::vector& entrypoint_args) const { std::unique_ptr isolate_configuration; if (flutter::DartVM::IsRunningPrecompiledCode()) { isolate_configuration = IsolateConfiguration::CreateForAppSnapshot(); @@ -299,6 +310,9 @@ std::optional AndroidShellHolder::BuildRunConfiguration( } else if (entrypoint.size() > 0) { config.SetEntrypoint(std::move(entrypoint)); } + if (entrypoint_args.size() > 0) { + config.SetEntrypointArgs(entrypoint_args); + } } return config; } diff --git a/shell/platform/android/android_shell_holder.h b/shell/platform/android/android_shell_holder.h index 9ea264f366722..c8c526026771c 100644 --- a/shell/platform/android/android_shell_holder.h +++ b/shell/platform/android/android_shell_holder.h @@ -16,6 +16,7 @@ #include "flutter/shell/common/shell.h" #include "flutter/shell/common/thread_host.h" #include "flutter/shell/platform/android/jni/platform_view_android_jni.h" +#include "flutter/shell/platform/android/platform_message_handler_android.h" #include "flutter/shell/platform/android/platform_view_android.h" namespace flutter { @@ -79,11 +80,13 @@ class AndroidShellHolder { std::shared_ptr jni_facade, const std::string& entrypoint, const std::string& libraryUrl, - const std::string& initial_route) const; + const std::string& initial_route, + const std::vector& entrypoint_args) const; void Launch(std::shared_ptr asset_manager, const std::string& entrypoint, - const std::string& libraryUrl); + const std::string& libraryUrl, + const std::vector& entrypoint_args); const flutter::Settings& GetSettings() const; @@ -96,6 +99,11 @@ class AndroidShellHolder { void NotifyLowMemoryWarning(); + const std::shared_ptr& GetPlatformMessageHandler() + const { + return shell_->GetPlatformMessageHandler(); + } + private: const flutter::Settings settings_; const std::shared_ptr jni_facade_; @@ -126,7 +134,8 @@ class AndroidShellHolder { std::optional BuildRunConfiguration( std::shared_ptr asset_manager, const std::string& entrypoint, - const std::string& libraryUrl) const; + const std::string& libraryUrl, + const std::vector& entrypoint_args) const; bool IsNDKImageDecoderAvailable(); diff --git a/shell/platform/android/android_shell_holder_unittests.cc b/shell/platform/android/android_shell_holder_unittests.cc index 2d3bc3ef311e1..85cb352e005f9 100644 --- a/shell/platform/android/android_shell_holder_unittests.cc +++ b/shell/platform/android/android_shell_holder_unittests.cc @@ -7,6 +7,7 @@ namespace flutter { namespace testing { namespace { class MockPlatformViewAndroidJNI : public PlatformViewAndroidJNI { + public: MOCK_METHOD2(FlutterViewHandlePlatformMessage, void(std::unique_ptr message, int responseId)); @@ -51,6 +52,15 @@ class MockPlatformViewAndroidJNI : public PlatformViewAndroidJNI { MOCK_METHOD0(GetDisplayRefreshRate, double()); MOCK_METHOD1(RequestDartDeferredLibrary, bool(int loading_unit_id)); }; + +class MockPlatformMessageResponse : public PlatformMessageResponse { + public: + static fml::RefPtr Create() { + return fml::AdoptRef(new MockPlatformMessageResponse()); + } + MOCK_METHOD1(Complete, void(std::unique_ptr data)); + MOCK_METHOD0(CompleteEmpty, void()); +}; } // namespace TEST(AndroidShellHolder, Create) { @@ -65,5 +75,34 @@ TEST(AndroidShellHolder, Create) { nullptr, /*is_fake_window=*/true); holder->GetPlatformView()->NotifyCreated(window); } + +TEST(AndroidShellHolder, HandlePlatformMessage) { + Settings settings; + settings.enable_software_rendering = false; + auto jni = std::make_shared(); + auto holder = std::make_unique(settings, jni); + EXPECT_NE(holder.get(), nullptr); + EXPECT_TRUE(holder->IsValid()); + EXPECT_NE(holder->GetPlatformView().get(), nullptr); + auto window = fml::MakeRefCounted( + nullptr, /*is_fake_window=*/true); + holder->GetPlatformView()->NotifyCreated(window); + EXPECT_TRUE(holder->GetPlatformMessageHandler()); + size_t data_size = 4; + fml::MallocMapping bytes = + fml::MallocMapping(static_cast(malloc(data_size)), data_size); + fml::RefPtr response = + MockPlatformMessageResponse::Create(); + auto message = std::make_unique( + /*channel=*/"foo", /*data=*/std::move(bytes), /*response=*/response); + int response_id = 1; + EXPECT_CALL(*jni, + FlutterViewHandlePlatformMessage(::testing::_, response_id)); + EXPECT_CALL(*response, CompleteEmpty()); + holder->GetPlatformMessageHandler()->HandlePlatformMessage( + std::move(message)); + holder->GetPlatformMessageHandler() + ->InvokePlatformMessageEmptyResponseCallback(response_id); +} } // namespace testing } // namespace flutter diff --git a/shell/platform/android/android_surface_gl.cc b/shell/platform/android/android_surface_gl.cc index ca3e24e8cdc0d..224b2280d22e2 100644 --- a/shell/platform/android/android_surface_gl.cc +++ b/shell/platform/android/android_surface_gl.cc @@ -52,7 +52,13 @@ std::unique_ptr AndroidSurfaceGL::CreateGPUSurface( if (gr_context) { return std::make_unique(sk_ref_sp(gr_context), this, true); } else { - return std::make_unique(this, true); + sk_sp main_skia_context = + GLContextPtr()->GetMainSkiaContext(); + if (!main_skia_context) { + main_skia_context = GPUSurfaceGL::MakeGLContext(this); + GLContextPtr()->SetMainSkiaContext(main_skia_context); + } + return std::make_unique(main_skia_context, this, true); } } @@ -171,7 +177,14 @@ AndroidContextGL* AndroidSurfaceGL::GLContextPtr() const { std::unique_ptr AndroidSurfaceGL::CreatePbufferSurface() { onscreen_surface_ = GLContextPtr()->CreatePbufferSurface(); - return std::make_unique(this, true); + sk_sp main_skia_context = + GLContextPtr()->GetMainSkiaContext(); + if (!main_skia_context) { + main_skia_context = GPUSurfaceGL::MakeGLContext(this); + GLContextPtr()->SetMainSkiaContext(main_skia_context); + } + + return std::make_unique(main_skia_context, this, true); } } // namespace flutter diff --git a/shell/platform/android/apk_asset_provider.cc b/shell/platform/android/apk_asset_provider.cc index 178227e9b156d..e5c266edab839 100644 --- a/shell/platform/android/apk_asset_provider.cc +++ b/shell/platform/android/apk_asset_provider.cc @@ -48,6 +48,8 @@ class APKAssetMapping : public fml::Mapping { return reinterpret_cast(AAsset_getBuffer(asset_)); } + bool IsDontNeedSafe() const override { return !AAsset_isAllocated(asset_); } + private: AAsset* const asset_; diff --git a/shell/platform/android/context/BUILD.gn b/shell/platform/android/context/BUILD.gn index e7c6c44725fe4..89918491187d3 100644 --- a/shell/platform/android/context/BUILD.gn +++ b/shell/platform/android/context/BUILD.gn @@ -3,6 +3,7 @@ # found in the LICENSE file. import("//flutter/common/config.gni") +import("//flutter/shell/config.gni") source_set("context") { sources = [ @@ -16,4 +17,8 @@ source_set("context") { "//flutter/fml", "//third_party/skia", ] + + if (shell_enable_vulkan) { + deps += [ "//flutter/vulkan" ] + } } diff --git a/shell/platform/android/context/android_context.cc b/shell/platform/android/context/android_context.cc index 4ff6550c15abb..1d4badf9543b7 100644 --- a/shell/platform/android/context/android_context.cc +++ b/shell/platform/android/context/android_context.cc @@ -9,7 +9,11 @@ namespace flutter { AndroidContext::AndroidContext(AndroidRenderingAPI rendering_api) : rendering_api_(rendering_api) {} -AndroidContext::~AndroidContext() = default; +AndroidContext::~AndroidContext() { + if (main_context_) { + main_context_->releaseResourcesAndAbandonContext(); + } +}; AndroidRenderingAPI AndroidContext::RenderingApi() const { return rendering_api_; @@ -19,4 +23,13 @@ bool AndroidContext::IsValid() const { return true; } +void AndroidContext::SetMainSkiaContext( + const sk_sp& main_context) { + main_context_ = main_context; +} + +sk_sp AndroidContext::GetMainSkiaContext() const { + return main_context_; +} + } // namespace flutter diff --git a/shell/platform/android/context/android_context.h b/shell/platform/android/context/android_context.h index 103340f7e53c0..0bed23e398674 100644 --- a/shell/platform/android/context/android_context.h +++ b/shell/platform/android/context/android_context.h @@ -6,6 +6,7 @@ #define FLUTTER_SHELL_PLATFORM_ANDROID_ANDROID_CONTEXT_H_ #include "flutter/fml/macros.h" +#include "flutter/fml/task_runner.h" #include "third_party/skia/include/gpu/GrDirectContext.h" namespace flutter { @@ -28,9 +29,37 @@ class AndroidContext { virtual bool IsValid() const; + //---------------------------------------------------------------------------- + /// @brief Setter for the Skia context to be used by subsequent + /// AndroidSurfaces. + /// @details This is useful to reduce memory consumption when creating + /// multiple AndroidSurfaces for the same AndroidContext. + /// + /// The first AndroidSurface should set this for the + /// AndroidContext if the AndroidContext does not yet have a + /// Skia context to share via GetMainSkiaContext. + /// + void SetMainSkiaContext(const sk_sp& main_context); + + //---------------------------------------------------------------------------- + /// @brief Accessor for the Skia context associated with AndroidSurfaces + /// and the raster thread. + /// @details This context is created lazily by the AndroidSurface based + /// on their respective rendering backend and set on this + /// AndroidContext to share via SetMainSkiaContext. + /// @returns `nullptr` when no Skia context has been set yet by its + /// AndroidSurface via SetMainSkiaContext. + /// @attention The software context doesn't have a Skia context, so this + /// value will be nullptr. + /// + sk_sp GetMainSkiaContext() const; + private: const AndroidRenderingAPI rendering_api_; + // This is the Skia context used for on-screen rendering. + sk_sp main_context_; + FML_DISALLOW_COPY_AND_ASSIGN(AndroidContext); }; diff --git a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc index aa72b6b13f73f..c5a3c19701a3f 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc @@ -112,7 +112,7 @@ TEST(AndroidExternalViewEmbedder, GetCurrentCanvases) { ASSERT_EQ(SkISize::Make(10, 20), canvases[1]->getBaseLayerSize()); } -TEST(AndroidExternalViewEmbedder, GetCurrentCanvases__CompositeOrder) { +TEST(AndroidExternalViewEmbedder, GetCurrentCanvasesCompositeOrder) { auto jni_mock = std::make_shared(); auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); @@ -248,7 +248,7 @@ TEST(AndroidExternalViewEmbedder, PlatformViewRect) { ASSERT_EQ(SkRect::MakeXYWH(15, 30, 45, 60), embedder->GetViewRect(view_id)); } -TEST(AndroidExternalViewEmbedder, PlatformViewRect__ChangedParams) { +TEST(AndroidExternalViewEmbedder, PlatformViewRectChangedParams) { auto jni_mock = std::make_shared(); auto android_context = AndroidContext(AndroidRenderingAPI::kSoftware); @@ -296,15 +296,16 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame) { auto window = fml::MakeRefCounted(nullptr); auto gr_context = GrDirectContext::MakeMock(nullptr); auto frame_size = SkISize::Make(1000, 1000); + SurfaceFrame::FramebufferInfo framebuffer_info; auto surface_factory = std::make_shared( - [&android_context, gr_context, window, frame_size]() { + [&android_context, gr_context, window, frame_size, framebuffer_info]() { auto surface_frame_1 = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); auto surface_frame_2 = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); @@ -335,7 +336,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame) { { auto did_submit_frame = false; auto surface_frame = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [&did_submit_frame](const SurfaceFrame& surface_frame, SkCanvas* canvas) mutable { if (canvas != nullptr) { @@ -402,7 +403,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame) { auto did_submit_frame = false; auto surface_frame = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [&did_submit_frame](const SurfaceFrame& surface_frame, SkCanvas* canvas) mutable { if (canvas != nullptr) { @@ -467,7 +468,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame) { auto did_submit_frame = false; auto surface_frame = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [&did_submit_frame](const SurfaceFrame& surface_frame, SkCanvas* canvas) mutable { if (canvas != nullptr) { @@ -487,7 +488,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame) { } } -TEST(AndroidExternalViewEmbedder, SubmitFrame__overlayComposition) { +TEST(AndroidExternalViewEmbedder, SubmitFrameOverlayComposition) { auto jni_mock = std::make_shared(); auto android_context = std::make_shared(AndroidRenderingAPI::kSoftware); @@ -495,10 +496,11 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame__overlayComposition) { auto window = fml::MakeRefCounted(nullptr); auto gr_context = GrDirectContext::MakeMock(nullptr); auto frame_size = SkISize::Make(1000, 1000); + SurfaceFrame::FramebufferInfo framebuffer_info; auto surface_factory = std::make_shared( - [&android_context, gr_context, window, frame_size]() { + [&android_context, gr_context, window, frame_size, framebuffer_info]() { auto surface_frame_1 = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); @@ -577,7 +579,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame__overlayComposition) { .Times(2); auto surface_frame = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) mutable { return true; }); @@ -588,7 +590,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame__overlayComposition) { embedder->EndFrame(/*should_resubmit_frame=*/false, raster_thread_merger); } -TEST(AndroidExternalViewEmbedder, SubmitFrame__platformViewWithoutAnyOverlay) { +TEST(AndroidExternalViewEmbedder, SubmitFramePlatformViewWithoutAnyOverlay) { auto jni_mock = std::make_shared(); auto android_context = std::make_shared(AndroidRenderingAPI::kSoftware); @@ -596,10 +598,11 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame__platformViewWithoutAnyOverlay) { auto window = fml::MakeRefCounted(nullptr); auto gr_context = GrDirectContext::MakeMock(nullptr); auto frame_size = SkISize::Make(1000, 1000); + SurfaceFrame::FramebufferInfo framebuffer_info; auto surface_factory = std::make_shared( - [&android_context, gr_context, window, frame_size]() { + [&android_context, gr_context, window, frame_size, framebuffer_info]() { auto surface_frame_1 = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); @@ -643,7 +646,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame__platformViewWithoutAnyOverlay) { EXPECT_CALL(*jni_mock, FlutterViewCreateOverlaySurface()).Times(0); auto surface_frame = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) mutable { return true; }); @@ -682,10 +685,11 @@ TEST(AndroidExternalViewEmbedder, DestroyOverlayLayersOnSizeChange) { auto window = fml::MakeRefCounted(nullptr); auto gr_context = GrDirectContext::MakeMock(nullptr); auto frame_size = SkISize::Make(1000, 1000); + SurfaceFrame::FramebufferInfo framebuffer_info; auto surface_factory = std::make_shared( - [&android_context, gr_context, window, frame_size]() { + [&android_context, gr_context, window, frame_size, framebuffer_info]() { auto surface_frame_1 = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); @@ -741,10 +745,12 @@ TEST(AndroidExternalViewEmbedder, DestroyOverlayLayersOnSizeChange) { EXPECT_CALL(*jni_mock, FlutterViewDisplayOverlaySurface(0, 50, 50, 200, 200)); - auto surface_frame = - std::make_unique(SkSurface::MakeNull(1000, 1000), false, - [](const SurfaceFrame& surface_frame, - SkCanvas* canvas) { return true; }); + SurfaceFrame::FramebufferInfo framebuffer_info; + auto surface_frame = std::make_unique( + SkSurface::MakeNull(1000, 1000), framebuffer_info, + [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { + return true; + }); embedder->SubmitFrame(gr_context.get(), std::move(surface_frame)); EXPECT_CALL(*jni_mock, FlutterViewEndFrame()); @@ -766,10 +772,11 @@ TEST(AndroidExternalViewEmbedder, DoesNotDestroyOverlayLayersOnSizeChange) { auto window = fml::MakeRefCounted(nullptr); auto gr_context = GrDirectContext::MakeMock(nullptr); auto frame_size = SkISize::Make(1000, 1000); + SurfaceFrame::FramebufferInfo framebuffer_info; auto surface_factory = std::make_shared( - [&android_context, gr_context, window, frame_size]() { + [&android_context, gr_context, window, frame_size, framebuffer_info]() { auto surface_frame_1 = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); @@ -825,10 +832,11 @@ TEST(AndroidExternalViewEmbedder, DoesNotDestroyOverlayLayersOnSizeChange) { EXPECT_CALL(*jni_mock, FlutterViewDisplayOverlaySurface(0, 50, 50, 200, 200)); - auto surface_frame = - std::make_unique(SkSurface::MakeNull(1000, 1000), false, - [](const SurfaceFrame& surface_frame, - SkCanvas* canvas) { return true; }); + auto surface_frame = std::make_unique( + SkSurface::MakeNull(1000, 1000), framebuffer_info, + [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { + return true; + }); embedder->SubmitFrame(gr_context.get(), std::move(surface_frame)); EXPECT_CALL(*jni_mock, FlutterViewEndFrame()); @@ -892,8 +900,9 @@ TEST(AndroidExternalViewEmbedder, Teardown) { auto frame_size = SkISize::Make(1000, 1000); auto surface_factory = std::make_shared( [&android_context, gr_context, window, frame_size]() { + SurfaceFrame::FramebufferInfo framebuffer_info; auto surface_frame_1 = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); @@ -936,8 +945,9 @@ TEST(AndroidExternalViewEmbedder, Teardown) { ByMove(std::make_unique( 0, window)))); + SurfaceFrame::FramebufferInfo framebuffer_info; auto surface_frame = std::make_unique( - SkSurface::MakeNull(1000, 1000), false, + SkSurface::MakeNull(1000, 1000), framebuffer_info, [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); embedder->SubmitFrame(gr_context.get(), std::move(surface_frame)); diff --git a/shell/platform/android/external_view_embedder/surface_pool_unittests.cc b/shell/platform/android/external_view_embedder/surface_pool_unittests.cc index ce37e8c81a8e7..caebb97f4fd03 100644 --- a/shell/platform/android/external_view_embedder/surface_pool_unittests.cc +++ b/shell/platform/android/external_view_embedder/surface_pool_unittests.cc @@ -36,7 +36,7 @@ class TestAndroidSurfaceFactory : public AndroidSurfaceFactory { TestSurfaceProducer surface_producer_; }; -TEST(SurfacePool, GetLayer__AllocateOneLayer) { +TEST(SurfacePool, GetLayerAllocateOneLayer) { auto pool = std::make_unique(); auto gr_context = GrDirectContext::MakeMock(nullptr); @@ -100,7 +100,7 @@ TEST(SurfacePool, GetUnusedLayers) { ASSERT_EQ(layer, pool->GetUnusedLayers()[0]); } -TEST(SurfacePool, GetLayer__Recycle) { +TEST(SurfacePool, GetLayerRecycle) { auto pool = std::make_unique(); auto gr_context_1 = GrDirectContext::MakeMock(nullptr); @@ -145,7 +145,7 @@ TEST(SurfacePool, GetLayer__Recycle) { layer_2->gr_context_key); } -TEST(SurfacePool, GetLayer__AllocateTwoLayers) { +TEST(SurfacePool, GetLayerAllocateTwoLayers) { auto pool = std::make_unique(); auto gr_context = GrDirectContext::MakeMock(nullptr); @@ -219,7 +219,7 @@ TEST(SurfacePool, DestroyLayers) { ASSERT_TRUE(pool->GetUnusedLayers().empty()); } -TEST(SurfacePool, DestroyLayers__frameSizeChanged) { +TEST(SurfacePool, DestroyLayersFrameSizeChanged) { auto pool = std::make_unique(); auto jni_mock = std::make_shared(); diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java b/shell/platform/android/io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java new file mode 100644 index 0000000000000..b10dcbf9f04c4 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java @@ -0,0 +1,56 @@ +// 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. + +package io.flutter.embedding.android; + +import androidx.annotation.CallSuper; +import com.google.android.play.core.splitcompat.SplitCompatApplication; +import io.flutter.FlutterInjector; +import io.flutter.embedding.engine.deferredcomponents.PlayStoreDeferredComponentManager; + +// TODO(garyq): Add a note about deferred components automatically adding this to manifest via +// manifest variable injection once it is implemented. +/** + * Flutter's extension of {@link SplitCompatApplication} that injects a {@link + * PlayStoreDeferredComponentManager} with {@link FlutterInjector} to enable Split AOT Flutter apps. + * + *

To use this class, either have your custom application class extend + * FlutterPlayStoreSplitApplication or use it directly in the app's AndroidManifest.xml by adding + * the following line: + * + *

{@code
+ * 
+ *    
+ *  
+ * }
+ * + * This class is meant to be used with the Google Play store. Custom non-play store applications do + * not need to extend {@link com.google.android.play.core.splitcompat.SplitCompatApplication} and + * should inject a custom {@link + * io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager} implementation like so: + * + *
{@code
+ * FlutterInjector.setInstance(
+ *      new FlutterInjector.Builder().setDeferredComponentManager(yourCustomManager).build());
+ * }
+ */ +public class FlutterPlayStoreSplitApplication extends SplitCompatApplication { + @Override + @CallSuper + public void onCreate() { + super.onCreate(); + // Create and inject a PlayStoreDeferredComponentManager, which is the default manager for + // interacting with the Google Play Store. + PlayStoreDeferredComponentManager deferredComponentManager = + new PlayStoreDeferredComponentManager(this, null); + FlutterInjector.setInstance( + new FlutterInjector.Builder() + .setDeferredComponentManager(deferredComponentManager) + .build()); + } +} diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index 8327324d90053..b2a10184f6610 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -60,9 +60,9 @@ public class KeyboardManager { * Constructor for {@link KeyboardManager} that takes a list of {@link * KeyboardManager.Responder}s. * - *

The view is used as the destination to send the synthesized key to. This means that the the - * next thing in the focus chain will get the event when the {@link KeyboardManager.Responder}s - * return false from onKeyDown/onKeyUp. + *

The view is used as the destination to send the synthesized key to. This means that the next + * thing in the focus chain will get the event when the {@link KeyboardManager.Responder}s return + * false from onKeyDown/onKeyUp. * *

It is possible that that in the middle of the async round trip, the focus chain could * change, and instead of the native widget that was "next" when the event was fired getting the diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 828bb94586832..ff712d4edcc7e 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -38,6 +38,7 @@ import io.flutter.plugin.localization.LocalizationPlugin; import io.flutter.plugin.platform.PlatformViewsController; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -380,13 +381,17 @@ private boolean isAttachedToJni() { * @param dartEntrypoint specifies the {@link DartEntrypoint} the new engine should run. It * doesn't need to be the same entrypoint as the current engine but must be built in the same * AOT or snapshot. + * @param initialRoute The name of the initial Flutter `Navigator` `Route` to load. If this is + * null, it will default to the "/" route. + * @param dartEntrypointArgs Arguments passed as a list of string to Dart's entrypoint function. * @return a new {@link io.flutter.embedding.engine.FlutterEngine}. */ @NonNull /*package*/ FlutterEngine spawn( @NonNull Context context, @NonNull DartEntrypoint dartEntrypoint, - @Nullable String initialRoute) { + @Nullable String initialRoute, + @Nullable List dartEntrypointArgs) { if (!isAttachedToJni()) { throw new IllegalStateException( "Spawn can only be called on a fully constructed FlutterEngine"); @@ -396,7 +401,8 @@ private boolean isAttachedToJni() { flutterJNI.spawn( dartEntrypoint.dartEntrypointFunctionName, dartEntrypoint.dartEntrypointLibrary, - initialRoute); + initialRoute, + dartEntrypointArgs); return new FlutterEngine( context, // Context. null, // FlutterLoader. A null value passed here causes the constructor to get it from the diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java index 98d0047162004..cae465a363da0 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java @@ -14,6 +14,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.Lifecycle; +import androidx.tracing.Trace; import io.flutter.Log; import io.flutter.embedding.android.ExclusiveAppComponent; import io.flutter.embedding.engine.loader.FlutterLoader; @@ -122,70 +123,76 @@ public void destroy() { @Override public void add(@NonNull FlutterPlugin plugin) { - if (has(plugin.getClass())) { - Log.w( - TAG, - "Attempted to register plugin (" - + plugin - + ") but it was " - + "already registered with this FlutterEngine (" - + flutterEngine - + ")."); - return; - } + Trace.beginSection("FlutterEngineConnectionRegistry#add " + plugin.getClass().getSimpleName()); + + try { + if (has(plugin.getClass())) { + Log.w( + TAG, + "Attempted to register plugin (" + + plugin + + ") but it was " + + "already registered with this FlutterEngine (" + + flutterEngine + + ")."); + return; + } - Log.v(TAG, "Adding plugin: " + plugin); - // Add the plugin to our generic set of plugins and notify the plugin - // that is has been attached to an engine. - plugins.put(plugin.getClass(), plugin); - plugin.onAttachedToEngine(pluginBinding); + Log.v(TAG, "Adding plugin: " + plugin); + // Add the plugin to our generic set of plugins and notify the plugin + // that is has been attached to an engine. + plugins.put(plugin.getClass(), plugin); + plugin.onAttachedToEngine(pluginBinding); - // For ActivityAware plugins, add the plugin to our set of ActivityAware - // plugins, and if this engine is currently attached to an Activity, - // notify the ActivityAware plugin that it is now attached to an Activity. - if (plugin instanceof ActivityAware) { - ActivityAware activityAware = (ActivityAware) plugin; - activityAwarePlugins.put(plugin.getClass(), activityAware); + // For ActivityAware plugins, add the plugin to our set of ActivityAware + // plugins, and if this engine is currently attached to an Activity, + // notify the ActivityAware plugin that it is now attached to an Activity. + if (plugin instanceof ActivityAware) { + ActivityAware activityAware = (ActivityAware) plugin; + activityAwarePlugins.put(plugin.getClass(), activityAware); - if (isAttachedToActivity()) { - activityAware.onAttachedToActivity(activityPluginBinding); + if (isAttachedToActivity()) { + activityAware.onAttachedToActivity(activityPluginBinding); + } } - } - // For ServiceAware plugins, add the plugin to our set of ServiceAware - // plugins, and if this engine is currently attached to a Service, - // notify the ServiceAware plugin that it is now attached to a Service. - if (plugin instanceof ServiceAware) { - ServiceAware serviceAware = (ServiceAware) plugin; - serviceAwarePlugins.put(plugin.getClass(), serviceAware); + // For ServiceAware plugins, add the plugin to our set of ServiceAware + // plugins, and if this engine is currently attached to a Service, + // notify the ServiceAware plugin that it is now attached to a Service. + if (plugin instanceof ServiceAware) { + ServiceAware serviceAware = (ServiceAware) plugin; + serviceAwarePlugins.put(plugin.getClass(), serviceAware); - if (isAttachedToService()) { - serviceAware.onAttachedToService(servicePluginBinding); + if (isAttachedToService()) { + serviceAware.onAttachedToService(servicePluginBinding); + } } - } - // For BroadcastReceiverAware plugins, add the plugin to our set of BroadcastReceiverAware - // plugins, and if this engine is currently attached to a BroadcastReceiver, - // notify the BroadcastReceiverAware plugin that it is now attached to a BroadcastReceiver. - if (plugin instanceof BroadcastReceiverAware) { - BroadcastReceiverAware broadcastReceiverAware = (BroadcastReceiverAware) plugin; - broadcastReceiverAwarePlugins.put(plugin.getClass(), broadcastReceiverAware); + // For BroadcastReceiverAware plugins, add the plugin to our set of BroadcastReceiverAware + // plugins, and if this engine is currently attached to a BroadcastReceiver, + // notify the BroadcastReceiverAware plugin that it is now attached to a BroadcastReceiver. + if (plugin instanceof BroadcastReceiverAware) { + BroadcastReceiverAware broadcastReceiverAware = (BroadcastReceiverAware) plugin; + broadcastReceiverAwarePlugins.put(plugin.getClass(), broadcastReceiverAware); - if (isAttachedToBroadcastReceiver()) { - broadcastReceiverAware.onAttachedToBroadcastReceiver(broadcastReceiverPluginBinding); + if (isAttachedToBroadcastReceiver()) { + broadcastReceiverAware.onAttachedToBroadcastReceiver(broadcastReceiverPluginBinding); + } } - } - // For ContentProviderAware plugins, add the plugin to our set of ContentProviderAware - // plugins, and if this engine is currently attached to a ContentProvider, - // notify the ContentProviderAware plugin that it is now attached to a ContentProvider. - if (plugin instanceof ContentProviderAware) { - ContentProviderAware contentProviderAware = (ContentProviderAware) plugin; - contentProviderAwarePlugins.put(plugin.getClass(), contentProviderAware); + // For ContentProviderAware plugins, add the plugin to our set of ContentProviderAware + // plugins, and if this engine is currently attached to a ContentProvider, + // notify the ContentProviderAware plugin that it is now attached to a ContentProvider. + if (plugin instanceof ContentProviderAware) { + ContentProviderAware contentProviderAware = (ContentProviderAware) plugin; + contentProviderAwarePlugins.put(plugin.getClass(), contentProviderAware); - if (isAttachedToContentProvider()) { - contentProviderAware.onAttachedToContentProvider(contentProviderPluginBinding); + if (isAttachedToContentProvider()) { + contentProviderAware.onAttachedToContentProvider(contentProviderPluginBinding); + } } + } finally { + Trace.endSection(); } } @@ -209,7 +216,13 @@ public FlutterPlugin get(@NonNull Class pluginClass) { @Override public void remove(@NonNull Class pluginClass) { FlutterPlugin plugin = plugins.get(pluginClass); - if (plugin != null) { + if (plugin == null) { + return; + } + + Trace.beginSection("FlutterEngineConnectionRegistry#remove " + pluginClass.getSimpleName()); + + try { Log.v(TAG, "Removing plugin: " + plugin); // For ActivityAware plugins, notify the plugin that it is detached from // an Activity if an Activity is currently attached to this engine. Then @@ -259,6 +272,8 @@ public void remove(@NonNull Class pluginClass) { // it from our set of generic plugins. plugin.onDetachedFromEngine(pluginBinding); plugins.remove(pluginClass); + } finally { + Trace.endSection(); } } @@ -301,20 +316,26 @@ private Activity attachedActivity() { @Override public void attachToActivity( @NonNull ExclusiveAppComponent exclusiveActivity, @NonNull Lifecycle lifecycle) { - Log.v( - TAG, - "Attaching to an exclusive Activity: " - + exclusiveActivity.getAppComponent() - + (isAttachedToActivity() ? " evicting previous activity " + attachedActivity() : "") - + "." - + (isWaitingForActivityReattachment ? " This is after a config change." : "")); - if (this.exclusiveActivity != null) { - this.exclusiveActivity.detachFromFlutterEngine(); - } - // If we were already attached to an app component, detach from it. - detachFromAppComponent(); - this.exclusiveActivity = exclusiveActivity; - attachToActivityInternal(exclusiveActivity.getAppComponent(), lifecycle); + Trace.beginSection("FlutterEngineConnectionRegistry#attachToActivity"); + + try { + Log.v( + TAG, + "Attaching to an exclusive Activity: " + + exclusiveActivity.getAppComponent() + + (isAttachedToActivity() ? " evicting previous activity " + attachedActivity() : "") + + "." + + (isWaitingForActivityReattachment ? " This is after a config change." : "")); + if (this.exclusiveActivity != null) { + this.exclusiveActivity.detachFromFlutterEngine(); + } + // If we were already attached to an app component, detach from it. + detachFromAppComponent(); + this.exclusiveActivity = exclusiveActivity; + attachToActivityInternal(exclusiveActivity.getAppComponent(), lifecycle); + } finally { + Trace.endSection(); + } } private void attachToActivityInternal(@NonNull Activity activity, @NonNull Lifecycle lifecycle) { @@ -341,14 +362,20 @@ private void attachToActivityInternal(@NonNull Activity activity, @NonNull Lifec @Override public void detachFromActivityForConfigChanges() { if (isAttachedToActivity()) { + Trace.beginSection("FlutterEngineConnectionRegistry#detachFromActivityForConfigChanges"); Log.v(TAG, "Detaching from an Activity for config changes: " + attachedActivity()); - isWaitingForActivityReattachment = true; - for (ActivityAware activityAware : activityAwarePlugins.values()) { - activityAware.onDetachedFromActivityForConfigChanges(); - } + try { + isWaitingForActivityReattachment = true; - detachFromActivityInternal(); + for (ActivityAware activityAware : activityAwarePlugins.values()) { + activityAware.onDetachedFromActivityForConfigChanges(); + } + + detachFromActivityInternal(); + } finally { + Trace.endSection(); + } } else { Log.e(TAG, "Attempted to detach plugins from an Activity when no Activity was attached."); } @@ -357,12 +384,18 @@ public void detachFromActivityForConfigChanges() { @Override public void detachFromActivity() { if (isAttachedToActivity()) { - Log.v(TAG, "Detaching from an Activity: " + attachedActivity()); - for (ActivityAware activityAware : activityAwarePlugins.values()) { - activityAware.onDetachedFromActivity(); - } + Trace.beginSection("FlutterEngineConnectionRegistry#detachFromActivity"); + + try { + Log.v(TAG, "Detaching from an Activity: " + attachedActivity()); + for (ActivityAware activityAware : activityAwarePlugins.values()) { + activityAware.onDetachedFromActivity(); + } - detachFromActivityInternal(); + detachFromActivityInternal(); + } finally { + Trace.endSection(); + } } else { Log.e(TAG, "Attempted to detach plugins from an Activity when no Activity was attached."); } @@ -381,12 +414,19 @@ public boolean onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResult) { Log.v(TAG, "Forwarding onRequestPermissionsResult() to plugins."); if (isAttachedToActivity()) { - return activityPluginBinding.onRequestPermissionsResult( - requestCode, permissions, grantResult); + Trace.beginSection("FlutterEngineConnectionRegistry#onRequestPermissionsResult"); + + try { + return activityPluginBinding.onRequestPermissionsResult( + requestCode, permissions, grantResult); + } finally { + Trace.endSection(); + } } else { Log.e( TAG, - "Attempted to notify ActivityAware plugins of onRequestPermissionsResult, but no Activity was attached."); + "Attempted to notify ActivityAware plugins of onRequestPermissionsResult, but no Activity" + + " was attached."); return false; } } @@ -395,11 +435,18 @@ public boolean onRequestPermissionsResult( public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { Log.v(TAG, "Forwarding onActivityResult() to plugins."); if (isAttachedToActivity()) { - return activityPluginBinding.onActivityResult(requestCode, resultCode, data); + Trace.beginSection("FlutterEngineConnectionRegistry#onActivityResult"); + + try { + return activityPluginBinding.onActivityResult(requestCode, resultCode, data); + } finally { + Trace.endSection(); + } } else { Log.e( TAG, - "Attempted to notify ActivityAware plugins of onActivityResult, but no Activity was attached."); + "Attempted to notify ActivityAware plugins of onActivityResult, but no Activity was" + + " attached."); return false; } } @@ -408,11 +455,18 @@ public boolean onActivityResult(int requestCode, int resultCode, @Nullable Inten public void onNewIntent(@NonNull Intent intent) { Log.v(TAG, "Forwarding onNewIntent() to plugins."); if (isAttachedToActivity()) { - activityPluginBinding.onNewIntent(intent); + Trace.beginSection("FlutterEngineConnectionRegistry#onNewIntent"); + + try { + activityPluginBinding.onNewIntent(intent); + } finally { + Trace.endSection(); + } } else { Log.e( TAG, - "Attempted to notify ActivityAware plugins of onNewIntent, but no Activity was attached."); + "Attempted to notify ActivityAware plugins of onNewIntent, but no Activity was" + + " attached."); } } @@ -420,11 +474,18 @@ public void onNewIntent(@NonNull Intent intent) { public void onUserLeaveHint() { Log.v(TAG, "Forwarding onUserLeaveHint() to plugins."); if (isAttachedToActivity()) { - activityPluginBinding.onUserLeaveHint(); + Trace.beginSection("FlutterEngineConnectionRegistry#onUserLeaveHint"); + + try { + activityPluginBinding.onUserLeaveHint(); + } finally { + Trace.endSection(); + } } else { Log.e( TAG, - "Attempted to notify ActivityAware plugins of onUserLeaveHint, but no Activity was attached."); + "Attempted to notify ActivityAware plugins of onUserLeaveHint, but no Activity was" + + " attached."); } } @@ -432,11 +493,18 @@ public void onUserLeaveHint() { public void onSaveInstanceState(@NonNull Bundle bundle) { Log.v(TAG, "Forwarding onSaveInstanceState() to plugins."); if (isAttachedToActivity()) { - activityPluginBinding.onSaveInstanceState(bundle); + Trace.beginSection("FlutterEngineConnectionRegistry#onSaveInstanceState"); + + try { + activityPluginBinding.onSaveInstanceState(bundle); + } finally { + Trace.endSection(); + } } else { Log.e( TAG, - "Attempted to notify ActivityAware plugins of onSaveInstanceState, but no Activity was attached."); + "Attempted to notify ActivityAware plugins of onSaveInstanceState, but no Activity was" + + " attached."); } } @@ -444,11 +512,18 @@ public void onSaveInstanceState(@NonNull Bundle bundle) { public void onRestoreInstanceState(@Nullable Bundle bundle) { Log.v(TAG, "Forwarding onRestoreInstanceState() to plugins."); if (isAttachedToActivity()) { - activityPluginBinding.onRestoreInstanceState(bundle); + Trace.beginSection("FlutterEngineConnectionRegistry#onRestoreInstanceState"); + + try { + activityPluginBinding.onRestoreInstanceState(bundle); + } finally { + Trace.endSection(); + } } else { Log.e( TAG, - "Attempted to notify ActivityAware plugins of onRestoreInstanceState, but no Activity was attached."); + "Attempted to notify ActivityAware plugins of onRestoreInstanceState, but no Activity was" + + " attached."); } } // ------- End ActivityControlSurface ----- @@ -461,30 +536,42 @@ private boolean isAttachedToService() { @Override public void attachToService( @NonNull Service service, @Nullable Lifecycle lifecycle, boolean isForeground) { + Trace.beginSection("FlutterEngineConnectionRegistry#attachToService"); Log.v(TAG, "Attaching to a Service: " + service); - // If we were already attached to an Android component, detach from it. - detachFromAppComponent(); - this.service = service; - this.servicePluginBinding = new FlutterEngineServicePluginBinding(service, lifecycle); + try { + // If we were already attached to an Android component, detach from it. + detachFromAppComponent(); + + this.service = service; + this.servicePluginBinding = new FlutterEngineServicePluginBinding(service, lifecycle); - // Notify all ServiceAware plugins that they are now attached to a new Service. - for (ServiceAware serviceAware : serviceAwarePlugins.values()) { - serviceAware.onAttachedToService(servicePluginBinding); + // Notify all ServiceAware plugins that they are now attached to a new Service. + for (ServiceAware serviceAware : serviceAwarePlugins.values()) { + serviceAware.onAttachedToService(servicePluginBinding); + } + } finally { + Trace.endSection(); } } @Override public void detachFromService() { if (isAttachedToService()) { + Trace.beginSection("FlutterEngineConnectionRegistry#detachFromService"); Log.v(TAG, "Detaching from a Service: " + service); - // Notify all ServiceAware plugins that they are no longer attached to a Service. - for (ServiceAware serviceAware : serviceAwarePlugins.values()) { - serviceAware.onDetachedFromService(); - } - service = null; - servicePluginBinding = null; + try { + // Notify all ServiceAware plugins that they are no longer attached to a Service. + for (ServiceAware serviceAware : serviceAwarePlugins.values()) { + serviceAware.onDetachedFromService(); + } + + service = null; + servicePluginBinding = null; + } finally { + Trace.endSection(); + } } else { Log.e(TAG, "Attempted to detach plugins from a Service when no Service was attached."); } @@ -493,16 +580,28 @@ public void detachFromService() { @Override public void onMoveToForeground() { if (isAttachedToService()) { - Log.v(TAG, "Attached Service moved to foreground."); - servicePluginBinding.onMoveToForeground(); + Trace.beginSection("FlutterEngineConnectionRegistry#onMoveToForeground"); + + try { + Log.v(TAG, "Attached Service moved to foreground."); + servicePluginBinding.onMoveToForeground(); + } finally { + Trace.endSection(); + } } } @Override public void onMoveToBackground() { if (isAttachedToService()) { + Trace.beginSection("FlutterEngineConnectionRegistry#onMoveToBackground"); Log.v(TAG, "Attached Service moved to background."); - servicePluginBinding.onMoveToBackground(); + + try { + servicePluginBinding.onMoveToBackground(); + } finally { + Trace.endSection(); + } } } // ----- End ServiceControlSurface --- @@ -515,36 +614,50 @@ private boolean isAttachedToBroadcastReceiver() { @Override public void attachToBroadcastReceiver( @NonNull BroadcastReceiver broadcastReceiver, @NonNull Lifecycle lifecycle) { + Trace.beginSection("FlutterEngineConnectionRegistry#attachToBroadcastReceiver"); Log.v(TAG, "Attaching to BroadcastReceiver: " + broadcastReceiver); - // If we were already attached to an Android component, detach from it. - detachFromAppComponent(); - this.broadcastReceiver = broadcastReceiver; - this.broadcastReceiverPluginBinding = - new FlutterEngineBroadcastReceiverPluginBinding(broadcastReceiver); - // TODO(mattcarroll): resolve possibility of different lifecycles between this and engine - // attachment + try { + // If we were already attached to an Android component, detach from it. + detachFromAppComponent(); - // Notify all BroadcastReceiverAware plugins that they are now attached to a new - // BroadcastReceiver. - for (BroadcastReceiverAware broadcastReceiverAware : broadcastReceiverAwarePlugins.values()) { - broadcastReceiverAware.onAttachedToBroadcastReceiver(broadcastReceiverPluginBinding); + this.broadcastReceiver = broadcastReceiver; + this.broadcastReceiverPluginBinding = + new FlutterEngineBroadcastReceiverPluginBinding(broadcastReceiver); + // TODO(mattcarroll): resolve possibility of different lifecycles between this and engine + // attachment + + // Notify all BroadcastReceiverAware plugins that they are now attached to a new + // BroadcastReceiver. + for (BroadcastReceiverAware broadcastReceiverAware : broadcastReceiverAwarePlugins.values()) { + broadcastReceiverAware.onAttachedToBroadcastReceiver(broadcastReceiverPluginBinding); + } + } finally { + Trace.endSection(); } } @Override public void detachFromBroadcastReceiver() { if (isAttachedToBroadcastReceiver()) { + Trace.beginSection("FlutterEngineConnectionRegistry#detachFromBroadcastReceiver"); Log.v(TAG, "Detaching from BroadcastReceiver: " + broadcastReceiver); - // Notify all BroadcastReceiverAware plugins that they are no longer attached to a - // BroadcastReceiver. - for (BroadcastReceiverAware broadcastReceiverAware : broadcastReceiverAwarePlugins.values()) { - broadcastReceiverAware.onDetachedFromBroadcastReceiver(); + + try { + // Notify all BroadcastReceiverAware plugins that they are no longer attached to a + // BroadcastReceiver. + for (BroadcastReceiverAware broadcastReceiverAware : + broadcastReceiverAwarePlugins.values()) { + broadcastReceiverAware.onDetachedFromBroadcastReceiver(); + } + } finally { + Trace.endSection(); } } else { Log.e( TAG, - "Attempted to detach plugins from a BroadcastReceiver when no BroadcastReceiver was attached."); + "Attempted to detach plugins from a BroadcastReceiver when no BroadcastReceiver was" + + " attached."); } } // ----- End BroadcastReceiverControlSurface ---- @@ -557,35 +670,49 @@ private boolean isAttachedToContentProvider() { @Override public void attachToContentProvider( @NonNull ContentProvider contentProvider, @NonNull Lifecycle lifecycle) { + Trace.beginSection("FlutterEngineConnectionRegistry#attachToContentProvider"); Log.v(TAG, "Attaching to ContentProvider: " + contentProvider); - // If we were already attached to an Android component, detach from it. - detachFromAppComponent(); - this.contentProvider = contentProvider; - this.contentProviderPluginBinding = - new FlutterEngineContentProviderPluginBinding(contentProvider); - // TODO(mattcarroll): resolve possibility of different lifecycles between this and engine - // attachment + try { + // If we were already attached to an Android component, detach from it. + detachFromAppComponent(); - // Notify all ContentProviderAware plugins that they are now attached to a new ContentProvider. - for (ContentProviderAware contentProviderAware : contentProviderAwarePlugins.values()) { - contentProviderAware.onAttachedToContentProvider(contentProviderPluginBinding); + this.contentProvider = contentProvider; + this.contentProviderPluginBinding = + new FlutterEngineContentProviderPluginBinding(contentProvider); + // TODO(mattcarroll): resolve possibility of different lifecycles between this and engine + // attachment + + // Notify all ContentProviderAware plugins that they are now attached to a new + // ContentProvider. + for (ContentProviderAware contentProviderAware : contentProviderAwarePlugins.values()) { + contentProviderAware.onAttachedToContentProvider(contentProviderPluginBinding); + } + } finally { + Trace.endSection(); } } @Override public void detachFromContentProvider() { if (isAttachedToContentProvider()) { + Trace.beginSection("FlutterEngineConnectionRegistry#detachFromContentProvider"); Log.v(TAG, "Detaching from ContentProvider: " + contentProvider); - // Notify all ContentProviderAware plugins that they are no longer attached to a - // ContentProvider. - for (ContentProviderAware contentProviderAware : contentProviderAwarePlugins.values()) { - contentProviderAware.onDetachedFromContentProvider(); + + try { + // Notify all ContentProviderAware plugins that they are no longer attached to a + // ContentProvider. + for (ContentProviderAware contentProviderAware : contentProviderAwarePlugins.values()) { + contentProviderAware.onDetachedFromContentProvider(); + } + } finally { + Trace.endSection(); } } else { Log.e( TAG, - "Attempted to detach plugins from a ContentProvider when no ContentProvider was attached."); + "Attempted to detach plugins from a ContentProvider when no ContentProvider was" + + " attached."); } } // ----- End ContentProviderControlSurface ----- diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java index 4dd8f43922fbe..bb1a9ba2995cf 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java @@ -117,8 +117,32 @@ public FlutterEngine createAndRunEngine( @NonNull Context context, @Nullable DartEntrypoint dartEntrypoint, @Nullable String initialRoute) { + return createAndRunEngine( + new Options(context).setDartEntrypoint(dartEntrypoint).setInitialRoute(initialRoute)); + } + + /** + * Creates a {@link io.flutter.embedding.engine.FlutterEngine} in this group and run its {@link + * io.flutter.embedding.engine.dart.DartExecutor} with the specified {@link DartEntrypoint}, the + * specified {@code initialRoute} and the {@code dartEntrypointArgs}. + * + *

If no prior {@link io.flutter.embedding.engine.FlutterEngine} were created in this group, + * the initialization cost will be slightly higher than subsequent engines. The very first {@link + * io.flutter.embedding.engine.FlutterEngine} created per program, regardless of + * FlutterEngineGroup, also incurs the Dart VM creation time. + * + *

Subsequent engine creations will share resources with existing engines. However, if all + * existing engines were {@link io.flutter.embedding.engine.FlutterEngine#destroy()}ed, the next + * engine created will recreate its dependencies. + */ + public FlutterEngine createAndRunEngine(@NonNull Options options) { FlutterEngine engine = null; + Context context = options.getContext(); + DartEntrypoint dartEntrypoint = options.getDartEntrypoint(); + String initialRoute = options.getInitialRoute(); + List dartEntrypointArgs = options.getDartEntrypointArgs(); + if (dartEntrypoint == null) { dartEntrypoint = DartEntrypoint.createDefault(); } @@ -128,9 +152,10 @@ public FlutterEngine createAndRunEngine( if (initialRoute != null) { engine.getNavigationChannel().setInitialRoute(initialRoute); } - engine.getDartExecutor().executeDartEntrypoint(dartEntrypoint); + engine.getDartExecutor().executeDartEntrypoint(dartEntrypoint, dartEntrypointArgs); } else { - engine = activeEngines.get(0).spawn(context, dartEntrypoint, initialRoute); + engine = + activeEngines.get(0).spawn(context, dartEntrypoint, initialRoute, dartEntrypointArgs); } activeEngines.add(engine); @@ -156,4 +181,75 @@ public void onEngineWillDestroy() { /* package */ FlutterEngine createEngine(Context context) { return new FlutterEngine(context); } + + /** Options that control how a FlutterEngine should be created. */ + public static class Options { + @NonNull private Context context; + @Nullable private DartEntrypoint dartEntrypoint; + @Nullable private String initialRoute; + @Nullable private List dartEntrypointArgs; + + public Options(@NonNull Context context) { + this.context = context; + } + + public Context getContext() { + return context; + } + + /** + * dartEntrypoint specifies the {@link DartEntrypoint} the new engine should run. It doesn't + * need to be the same entrypoint as the current engine but must be built in the same AOT or + * snapshot. + */ + public DartEntrypoint getDartEntrypoint() { + return dartEntrypoint; + } + + /** + * The name of the initial Flutter `Navigator` `Route` to load. If this is null, it will default + * to the "/" route. + */ + public String getInitialRoute() { + return initialRoute; + } + + /** Arguments passed as a list of string to Dart's entrypoint function. */ + public List getDartEntrypointArgs() { + return dartEntrypointArgs; + } + + /** + * Setter for `dartEntrypoint` property. + * + * @param dartEntrypoint specifies the {@link DartEntrypoint} the new engine should run. It + * doesn't need to be the same entrypoint as the current engine but must be built in the + * same AOT or snapshot. + */ + public Options setDartEntrypoint(DartEntrypoint dartEntrypoint) { + this.dartEntrypoint = dartEntrypoint; + return this; + } + + /** + * Setter for `initialRoute` property. + * + * @param initialRoute The name of the initial Flutter `Navigator` `Route` to load. If this is + * null, it will default to the "/" route. + */ + public Options setInitialRoute(String initialRoute) { + this.initialRoute = initialRoute; + return this; + } + + /** + * Setter for `dartEntrypointArgs` property. + * + * @param dartEntrypointArgs Arguments passed as a list of string to Dart's entrypoint function. + */ + public Options setDartEntrypointArgs(List dartEntrypointArgs) { + this.dartEntrypointArgs = dartEntrypointArgs; + return this; + } + } } diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 4ac8740641541..5b6c6579de6df 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -41,6 +41,7 @@ import java.util.Locale; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Interface between Flutter embedding's Java code and Flutter engine's C/C++ code. @@ -98,6 +99,12 @@ @Keep public class FlutterJNI { private static final String TAG = "FlutterJNI"; + // This serializes the invocation of platform message responses and the + // attachment and detachment of the shell holder. This ensures that we don't + // detach FlutterJNI on the platform thread while a background thread invokes + // a message response. Typically accessing the shell holder happens on the + // platform thread and doesn't require locking. + private ReentrantReadWriteLock shellHolderLock = new ReentrantReadWriteLock(); // BEGIN Methods related to loading for FlutterLoader. /** @@ -179,9 +186,15 @@ private static native void nativeInit( // END methods related to FlutterLoader @Nullable private static AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate; - // This should also be updated by FlutterView when it is attached to a Display. - // The initial value of 0.0 indicates unknown refresh rate. - private static float refreshRateFPS = 0.0f; + + /** + * This value is updated by the VsyncWaiter when it is initialized. + * + *

On API 17+, it is updated whenever the default display refresh rate changes. + * + *

It is defaulted to 60. + */ + private static float refreshRateFPS = 60.0f; // This is set from native code via JNI. @Nullable private static String observatoryUri; @@ -209,19 +222,34 @@ public static String getObservatoryUri() { return observatoryUri; } - public static void setRefreshRateFPS(float refreshRateFPS) { - if (FlutterJNI.setRefreshRateFPSCalled) { - Log.w(TAG, "FlutterJNI.setRefreshRateFPS called more than once"); - } - + /** + * Notifies the engine about the refresh rate of the display when the API level is below 30. + * + *

For API 30 and above, this value is ignored. + * + *

Calling this method multiple times will update the refresh rate for the next vsync period. + * However, callers should avoid calling {@link android.view.Display#getRefreshRate} frequently, + * since it is expensive on some vendor implementations. + * + * @param refreshRateFPS The refresh rate in nanoseconds. + */ + public void setRefreshRateFPS(float refreshRateFPS) { + // This is ok because it only ever tracks the refresh rate of the main + // display. If we ever need to support the refresh rate of other displays + // on Android we will need to refactor this. Static lookup makes things a + // bit easier on the C++ side. FlutterJNI.refreshRateFPS = refreshRateFPS; - FlutterJNI.setRefreshRateFPSCalled = true; } - private static boolean setRefreshRateFPSCalled = false; - - // TODO(mattcarroll): add javadocs - public static void setAsyncWaitForVsyncDelegate(@Nullable AsyncWaitForVsyncDelegate delegate) { + /** + * The Android vsync waiter implementation in C++ needs to know when a vsync signal arrives, which + * is obtained via Java API. The delegate set here is called on the C++ side when the engine is + * ready to wait for the next vsync signal. The delegate is expected to add a postFrameCallback to + * the {@link android.view.Choreographer}, and call {@link nativeOnVsync} to notify the engine. + * + * @param delegate The delegate that will call the engine back on the next vsync signal. + */ + public void setAsyncWaitForVsyncDelegate(@Nullable AsyncWaitForVsyncDelegate delegate) { asyncWaitForVsyncDelegate = delegate; } @@ -236,9 +264,15 @@ private static void asyncWaitForVsync(final long cookie) { } } - // TODO(mattcarroll): add javadocs - public static native void nativeOnVsync( - long frameDelayNanos, long refreshPeriodNanos, long cookie); + /** + * Notifies the engine that the Choreographer has signaled a vsync. + * + * @param frameDelayNanos The time in nanoseconds when the frame started being rendered, + * subtracted from the {@link System#nanoTime} timebase. + * @param refreshPeriodNanos The display refresh period in nanoseconds. + * @param cookie An opaque handle to the C++ VSyncWaiter object. + */ + public native void nativeOnVsync(long frameDelayNanos, long refreshPeriodNanos, long cookie); // TODO(mattcarroll): add javadocs @NonNull @@ -304,7 +338,12 @@ public boolean isAttached() { public void attachToNative() { ensureRunningOnMainThread(); ensureNotAttachedToNative(); - nativeShellHolderId = performNativeAttach(this); + shellHolderLock.writeLock().lock(); + try { + nativeShellHolderId = performNativeAttach(this); + } finally { + shellHolderLock.writeLock().unlock(); + } } @VisibleForTesting @@ -325,20 +364,24 @@ public long performNativeAttach(@NonNull FlutterJNI flutterJNI) { * #attachToNative()}. * *

Static methods that should be only called once such as {@link #init(Context, String[], - * String, String, String, long)} or {@link #setRefreshRateFPS(float)} shouldn't be called again - * on the spawned FlutterJNI instance. + * String, String, String, long)} shouldn't be called again on the spawned FlutterJNI instance. */ @UiThread @NonNull public FlutterJNI spawn( @Nullable String entrypointFunctionName, @Nullable String pathToEntrypointFunction, - @Nullable String initialRoute) { + @Nullable String initialRoute, + @Nullable List entrypointArgs) { ensureRunningOnMainThread(); ensureAttachedToNative(); FlutterJNI spawnedJNI = nativeSpawn( - nativeShellHolderId, entrypointFunctionName, pathToEntrypointFunction, initialRoute); + nativeShellHolderId, + entrypointFunctionName, + pathToEntrypointFunction, + initialRoute, + entrypointArgs); Preconditions.checkState( spawnedJNI.nativeShellHolderId != null && spawnedJNI.nativeShellHolderId != 0, "Failed to spawn new JNI connected shell from existing shell."); @@ -350,7 +393,8 @@ private native FlutterJNI nativeSpawn( long nativeSpawningShellId, @Nullable String entrypointFunctionName, @Nullable String pathToEntrypointFunction, - @Nullable String initialRoute); + @Nullable String initialRoute, + @Nullable List entrypointArgs); /** * Detaches this {@code FlutterJNI} instance from Flutter's native engine, which precludes any @@ -359,8 +403,8 @@ private native FlutterJNI nativeSpawn( *

This method must not be invoked if {@code FlutterJNI} is not already attached to native. * *

Invoking this method will result in the release of all native-side resources that were set - * up during {@link #attachToNative()} or {@link #spawn(String, String, String)}, or accumulated - * thereafter. + * up during {@link #attachToNative()} or {@link #spawn(String, String, String, List)}, or + * accumulated thereafter. * *

It is permissible to re-attach this instance to native after detaching it from native. */ @@ -368,8 +412,13 @@ private native FlutterJNI nativeSpawn( public void detachFromNativeAndReleaseResources() { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeDestroy(nativeShellHolderId); - nativeShellHolderId = null; + shellHolderLock.writeLock().lock(); + try { + nativeDestroy(nativeShellHolderId); + nativeShellHolderId = null; + } finally { + shellHolderLock.writeLock().unlock(); + } } private native void nativeDestroy(long nativeShellHolderId); @@ -647,7 +696,7 @@ public void setAccessibilityDelegate(@Nullable AccessibilityDelegate accessibili * *

The {@code buffer} and {@code strings} form a communication protocol that is implemented * here: - * https://github.com/flutter/engine/blob/master/shell/platform/android/platform_view_android.cc#L207 + * https://github.com/flutter/engine/blob/main/shell/platform/android/platform_view_android.cc#L207 */ @SuppressWarnings("unused") @UiThread @@ -668,7 +717,7 @@ private void updateSemantics( * *

The {@code buffer} and {@code strings} form a communication protocol that is implemented * here: - * https://github.com/flutter/engine/blob/master/shell/platform/android/platform_view_android.cc#L207 + * https://github.com/flutter/engine/blob/main/shell/platform/android/platform_view_android.cc#L207 * *

// TODO(cbracken): expand these docs to include more actionable information. */ @@ -807,7 +856,8 @@ public void runBundleAndSnapshotFromLibrary( @NonNull String bundlePath, @Nullable String entrypointFunctionName, @Nullable String pathToEntrypointFunction, - @NonNull AssetManager assetManager) { + @NonNull AssetManager assetManager, + @Nullable List entrypointArgs) { ensureRunningOnMainThread(); ensureAttachedToNative(); nativeRunBundleAndSnapshotFromLibrary( @@ -815,7 +865,8 @@ public void runBundleAndSnapshotFromLibrary( bundlePath, entrypointFunctionName, pathToEntrypointFunction, - assetManager); + assetManager, + entrypointArgs); } private native void nativeRunBundleAndSnapshotFromLibrary( @@ -823,7 +874,8 @@ private native void nativeRunBundleAndSnapshotFromLibrary( @NonNull String bundlePath, @Nullable String entrypointFunctionName, @Nullable String pathToEntrypointFunction, - @NonNull AssetManager manager); + @NonNull AssetManager manager, + @Nullable List entrypointArgs); // ------ End Dart Execution Support ------- // --------- Start Platform Message Support ------ @@ -858,14 +910,33 @@ public void setPlatformMessageHandler(@Nullable PlatformMessageHandler platformM this.platformMessageHandler = platformMessageHandler; } - // Called by native. + private native void nativeCleanupMessageData(long messageData); + + /** + * Destroys the resources provided sent to `handlePlatformMessage`. + * + *

This can be called on any thread. + * + * @param messageData the argument sent to handlePlatformMessage. + */ + public void cleanupMessageData(long messageData) { + // This doesn't rely on being attached like other methods. + nativeCleanupMessageData(messageData); + } + + // Called by native on the ui thread. // TODO(mattcarroll): determine if message is nonull or nullable @SuppressWarnings("unused") @VisibleForTesting public void handlePlatformMessage( - @NonNull final String channel, ByteBuffer message, final int replyId) { + @NonNull final String channel, + ByteBuffer message, + final int replyId, + final long messageData) { if (platformMessageHandler != null) { - platformMessageHandler.handleMessageFromDart(channel, message, replyId); + platformMessageHandler.handleMessageFromDart(channel, message, replyId, messageData); + } else { + nativeCleanupMessageData(messageData); } // TODO(mattcarroll): log dropped messages when in debug mode // (https://github.com/flutter/flutter/issues/25391) @@ -931,16 +1002,20 @@ private native void nativeDispatchPlatformMessage( int responseId); // TODO(mattcarroll): differentiate between channel responses and platform responses. - @UiThread public void invokePlatformMessageEmptyResponseCallback(int responseId) { - ensureRunningOnMainThread(); - if (isAttached()) { - nativeInvokePlatformMessageEmptyResponseCallback(nativeShellHolderId, responseId); - } else { - Log.w( - TAG, - "Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: " - + responseId); + // Called on any thread. + shellHolderLock.readLock().lock(); + try { + if (isAttached()) { + nativeInvokePlatformMessageEmptyResponseCallback(nativeShellHolderId, responseId); + } else { + Log.w( + TAG, + "Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: " + + responseId); + } + } finally { + shellHolderLock.readLock().unlock(); } } @@ -949,21 +1024,25 @@ private native void nativeInvokePlatformMessageEmptyResponseCallback( long nativeShellHolderId, int responseId); // TODO(mattcarroll): differentiate between channel responses and platform responses. - @UiThread public void invokePlatformMessageResponseCallback( int responseId, @NonNull ByteBuffer message, int position) { - ensureRunningOnMainThread(); + // Called on any thread. if (!message.isDirect()) { throw new IllegalArgumentException("Expected a direct ByteBuffer."); } - if (isAttached()) { - nativeInvokePlatformMessageResponseCallback( - nativeShellHolderId, responseId, message, position); - } else { - Log.w( - TAG, - "Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: " - + responseId); + shellHolderLock.readLock().lock(); + try { + if (isAttached()) { + nativeInvokePlatformMessageResponseCallback( + nativeShellHolderId, responseId, message, position); + } else { + Log.w( + TAG, + "Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: " + + responseId); + } + } finally { + shellHolderLock.readLock().unlock(); } } diff --git a/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java b/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java index 511888112ccf5..533a037fbc5cc 100644 --- a/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java +++ b/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import androidx.tracing.Trace; import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.FlutterJNI; @@ -16,6 +17,7 @@ import io.flutter.plugin.common.StringCodec; import io.flutter.view.FlutterCallbackInformation; import java.nio.ByteBuffer; +import java.util.List; /** * Configures, bootstraps, and starts executing Dart code. @@ -83,7 +85,8 @@ public DartExecutor(@NonNull FlutterJNI flutterJNI, @NonNull AssetManager assetM public void onAttachedToJNI() { Log.v( TAG, - "Attached to JNI. Registering the platform message handler for this Dart execution context."); + "Attached to JNI. Registering the platform message handler for this Dart execution" + + " context."); flutterJNI.setPlatformMessageHandler(dartMessenger); } @@ -97,7 +100,8 @@ public void onAttachedToJNI() { public void onDetachedFromJNI() { Log.v( TAG, - "Detached from JNI. De-registering the platform message handler for this Dart execution context."); + "Detached from JNI. De-registering the platform message handler for this Dart execution" + + " context."); flutterJNI.setPlatformMessageHandler(null); } @@ -118,20 +122,40 @@ public boolean isExecutingDart() { * @param dartEntrypoint specifies which Dart function to run, and where to find it */ public void executeDartEntrypoint(@NonNull DartEntrypoint dartEntrypoint) { + executeDartEntrypoint(dartEntrypoint, null); + } + + /** + * Starts executing Dart code based on the given {@code dartEntrypoint} and the {@code + * dartEntrypointArgs}. + * + *

See {@link DartEntrypoint} for configuration options. + * + * @param dartEntrypoint specifies which Dart function to run, and where to find it + * @param dartEntrypointArgs Arguments passed as a list of string to Dart's entrypoint function. + */ + public void executeDartEntrypoint( + @NonNull DartEntrypoint dartEntrypoint, @Nullable List dartEntrypointArgs) { if (isApplicationRunning) { Log.w(TAG, "Attempted to run a DartExecutor that is already running."); return; } + Trace.beginSection("DartExecutor#executeDartEntrypoint"); Log.v(TAG, "Executing Dart entrypoint: " + dartEntrypoint); - flutterJNI.runBundleAndSnapshotFromLibrary( - dartEntrypoint.pathToBundle, - dartEntrypoint.dartEntrypointFunctionName, - dartEntrypoint.dartEntrypointLibrary, - assetManager); + try { + flutterJNI.runBundleAndSnapshotFromLibrary( + dartEntrypoint.pathToBundle, + dartEntrypoint.dartEntrypointFunctionName, + dartEntrypoint.dartEntrypointLibrary, + assetManager, + dartEntrypointArgs); - isApplicationRunning = true; + isApplicationRunning = true; + } finally { + Trace.endSection(); + } } /** @@ -147,15 +171,21 @@ public void executeDartCallback(@NonNull DartCallback dartCallback) { return; } + Trace.beginSection("DartExecutor#executeDartCallback"); Log.v(TAG, "Executing Dart callback: " + dartCallback); - flutterJNI.runBundleAndSnapshotFromLibrary( - dartCallback.pathToBundle, - dartCallback.callbackHandle.callbackName, - dartCallback.callbackHandle.callbackLibraryPath, - dartCallback.androidAssetManager); + try { + flutterJNI.runBundleAndSnapshotFromLibrary( + dartCallback.pathToBundle, + dartCallback.callbackHandle.callbackName, + dartCallback.callbackHandle.callbackLibraryPath, + dartCallback.androidAssetManager, + null); - isApplicationRunning = true; + isApplicationRunning = true; + } finally { + Trace.endSection(); + } } /** @@ -168,6 +198,14 @@ public BinaryMessenger getBinaryMessenger() { } // ------ START BinaryMessenger (Deprecated: use getBinaryMessenger() instead) ----- + /** @deprecated Use {@link #getBinaryMessenger()} instead. */ + @Deprecated + @UiThread + @Override + public TaskQueue makeBackgroundTaskQueue(TaskQueueOptions options) { + return binaryMessenger.makeBackgroundTaskQueue(options); + } + /** @deprecated Use {@link #getBinaryMessenger()} instead. */ @Deprecated @Override @@ -195,6 +233,31 @@ public void setMessageHandler( @NonNull String channel, @Nullable BinaryMessenger.BinaryMessageHandler handler) { binaryMessenger.setMessageHandler(channel, handler); } + + /** @deprecated Use {@link #getBinaryMessenger()} instead. */ + @Deprecated + @Override + @UiThread + public void setMessageHandler( + @NonNull String channel, + @Nullable BinaryMessenger.BinaryMessageHandler handler, + @Nullable TaskQueue taskQueue) { + binaryMessenger.setMessageHandler(channel, handler, taskQueue); + } + + /** @deprecated Use {@link #getBinaryMessenger()} instead. */ + @Deprecated + @Override + public void enableBufferingIncomingMessages() { + dartMessenger.enableBufferingIncomingMessages(); + } + + /** @deprecated Use {@link #getBinaryMessenger()} instead. */ + @Deprecated + @Override + public void disableBufferingIncomingMessages() { + dartMessenger.disableBufferingIncomingMessages(); + } // ------ END BinaryMessenger ----- /** @@ -371,6 +434,10 @@ private DefaultBinaryMessenger(@NonNull DartMessenger messenger) { this.messenger = messenger; } + public TaskQueue makeBackgroundTaskQueue(TaskQueueOptions options) { + return messenger.makeBackgroundTaskQueue(options); + } + /** * Sends the given {@code message} from Android to Dart over the given {@code channel}. * @@ -416,5 +483,24 @@ public void setMessageHandler( @NonNull String channel, @Nullable BinaryMessenger.BinaryMessageHandler handler) { messenger.setMessageHandler(channel, handler); } + + @Override + @UiThread + public void setMessageHandler( + @NonNull String channel, + @Nullable BinaryMessenger.BinaryMessageHandler handler, + @Nullable TaskQueue taskQueue) { + messenger.setMessageHandler(channel, handler, taskQueue); + } + + @Override + public void enableBufferingIncomingMessages() { + messenger.enableBufferingIncomingMessages(); + } + + @Override + public void disableBufferingIncomingMessages() { + messenger.disableBufferingIncomingMessages(); + } } } diff --git a/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java b/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java index 2822b4060fbcd..9a7658d069a4a 100644 --- a/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java +++ b/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java @@ -7,12 +7,19 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import androidx.tracing.Trace; +import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.plugin.common.BinaryMessenger; import java.nio.ByteBuffer; import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -26,25 +33,227 @@ class DartMessenger implements BinaryMessenger, PlatformMessageHandler { private static final String TAG = "DartMessenger"; @NonNull private final FlutterJNI flutterJNI; - @NonNull private final Map messageHandlers; - @NonNull private final Map pendingReplies; + + /** + * Maps a channel name to an object that contains the task queue and the handler associated with + * the channel. + * + *

Reads and writes to this map must lock {@code handlersLock}. + */ + @NonNull private final Map messageHandlers = new HashMap<>(); + + /** + * Maps a channel name to an object that holds information about the incoming Dart message. + * + *

Reads and writes to this map must lock {@code handlersLock}. + */ + @NonNull private Map> bufferedMessages = new HashMap<>(); + + @NonNull private final Object handlersLock = new Object(); + @NonNull private final AtomicBoolean enableBufferingIncomingMessages = new AtomicBoolean(false); + + @NonNull private final Map pendingReplies = new HashMap<>(); private int nextReplyId = 1; - DartMessenger(@NonNull FlutterJNI flutterJNI) { + @NonNull private final DartMessengerTaskQueue platformTaskQueue = new PlatformTaskQueue(); + + @NonNull + private WeakHashMap createdTaskQueues = + new WeakHashMap(); + + @NonNull private TaskQueueFactory taskQueueFactory; + + DartMessenger(@NonNull FlutterJNI flutterJNI, @NonNull TaskQueueFactory taskQueueFactory) { this.flutterJNI = flutterJNI; - this.messageHandlers = new HashMap<>(); - this.pendingReplies = new HashMap<>(); + this.taskQueueFactory = taskQueueFactory; + } + + DartMessenger(@NonNull FlutterJNI flutterJNI) { + this(flutterJNI, new DefaultTaskQueueFactory()); + } + + private static class TaskQueueToken implements TaskQueue {} + + interface DartMessengerTaskQueue { + void dispatch(@NonNull Runnable runnable); + } + + interface TaskQueueFactory { + DartMessengerTaskQueue makeBackgroundTaskQueue(TaskQueueOptions options); + } + + private static class DefaultTaskQueueFactory implements TaskQueueFactory { + ExecutorService executorService; + + DefaultTaskQueueFactory() { + executorService = FlutterInjector.instance().executorService(); + } + + public DartMessengerTaskQueue makeBackgroundTaskQueue(TaskQueueOptions options) { + if (options.getIsSerial()) { + return new SerialTaskQueue(executorService); + } else { + return new ConcurrentTaskQueue(executorService); + } + } + } + + /** + * Holds information about a platform handler, such as the task queue that processes messages from + * Dart. + */ + private static class HandlerInfo { + @NonNull public final BinaryMessenger.BinaryMessageHandler handler; + @Nullable public final DartMessengerTaskQueue taskQueue; + + HandlerInfo( + @NonNull BinaryMessenger.BinaryMessageHandler handler, + @Nullable DartMessengerTaskQueue taskQueue) { + this.handler = handler; + this.taskQueue = taskQueue; + } + } + + /** + * Holds information that allows to dispatch a Dart message to a platform handler when it becomes + * available. + */ + private static class BufferedMessageInfo { + @NonNull public final ByteBuffer message; + int replyId; + long messageData; + + BufferedMessageInfo(@NonNull ByteBuffer message, int replyId, long messageData) { + this.message = message; + this.replyId = replyId; + this.messageData = messageData; + } + } + + static class ConcurrentTaskQueue implements DartMessengerTaskQueue { + @NonNull private final ExecutorService executor; + + ConcurrentTaskQueue(ExecutorService executor) { + this.executor = executor; + } + + @Override + public void dispatch(@NonNull Runnable runnable) { + executor.execute(runnable); + } + } + + /** A serial task queue that can run on a concurrent ExecutorService. */ + static class SerialTaskQueue implements DartMessengerTaskQueue { + @NonNull private final ExecutorService executor; + @NonNull private final ConcurrentLinkedQueue queue; + @NonNull private final AtomicBoolean isRunning; + + SerialTaskQueue(ExecutorService executor) { + this.executor = executor; + queue = new ConcurrentLinkedQueue<>(); + isRunning = new AtomicBoolean(false); + } + + @Override + public void dispatch(@NonNull Runnable runnable) { + queue.add(runnable); + executor.execute( + () -> { + flush(); + }); + } + + private void flush() { + // Don't execute if we are already executing (enforce serial execution). + if (isRunning.compareAndSet(false, true)) { + try { + @Nullable Runnable runnable = queue.poll(); + if (runnable != null) { + runnable.run(); + } + } finally { + isRunning.set(false); + if (!queue.isEmpty()) { + // Schedule the next event. + executor.execute( + () -> { + flush(); + }); + } + } + } + } + } + + @Override + public TaskQueue makeBackgroundTaskQueue(TaskQueueOptions options) { + DartMessengerTaskQueue taskQueue = taskQueueFactory.makeBackgroundTaskQueue(options); + TaskQueueToken token = new TaskQueueToken(); + createdTaskQueues.put(token, taskQueue); + return token; } @Override public void setMessageHandler( @NonNull String channel, @Nullable BinaryMessenger.BinaryMessageHandler handler) { + setMessageHandler(channel, handler, null); + } + + @Override + public void setMessageHandler( + @NonNull String channel, + @Nullable BinaryMessenger.BinaryMessageHandler handler, + @Nullable TaskQueue taskQueue) { if (handler == null) { Log.v(TAG, "Removing handler for channel '" + channel + "'"); - messageHandlers.remove(channel); - } else { - Log.v(TAG, "Setting handler for channel '" + channel + "'"); - messageHandlers.put(channel, handler); + synchronized (handlersLock) { + messageHandlers.remove(channel); + } + return; + } + DartMessengerTaskQueue dartMessengerTaskQueue = null; + if (taskQueue != null) { + dartMessengerTaskQueue = createdTaskQueues.get(taskQueue); + if (dartMessengerTaskQueue == null) { + throw new IllegalArgumentException( + "Unrecognized TaskQueue, use BinaryMessenger to create your TaskQueue (ex makeBackgroundTaskQueue)."); + } + } + Log.v(TAG, "Setting handler for channel '" + channel + "'"); + + List list; + synchronized (handlersLock) { + messageHandlers.put(channel, new HandlerInfo(handler, dartMessengerTaskQueue)); + list = bufferedMessages.remove(channel); + if (list == null) { + return; + } + } + for (BufferedMessageInfo info : list) { + dispatchMessageToQueue( + channel, messageHandlers.get(channel), info.message, info.replyId, info.messageData); + } + } + + @Override + public void enableBufferingIncomingMessages() { + enableBufferingIncomingMessages.set(true); + } + + @Override + public void disableBufferingIncomingMessages() { + Map> pendingMessages; + synchronized (handlersLock) { + enableBufferingIncomingMessages.set(false); + pendingMessages = bufferedMessages; + bufferedMessages = new HashMap<>(); + } + for (Map.Entry> channel : pendingMessages.entrySet()) { + for (BufferedMessageInfo info : channel.getValue()) { + dispatchMessageToQueue( + channel.getKey(), null, info.message, info.replyId, info.messageData); + } } } @@ -60,32 +269,31 @@ public void send( @NonNull String channel, @Nullable ByteBuffer message, @Nullable BinaryMessenger.BinaryReply callback) { + Trace.beginSection("DartMessenger#send on " + channel); Log.v(TAG, "Sending message with callback over channel '" + channel + "'"); - int replyId = nextReplyId++; - if (callback != null) { - pendingReplies.put(replyId, callback); - } - if (message == null) { - flutterJNI.dispatchEmptyPlatformMessage(channel, replyId); - } else { - flutterJNI.dispatchPlatformMessage(channel, message, message.position(), replyId); + + try { + int replyId = nextReplyId++; + if (callback != null) { + pendingReplies.put(replyId, callback); + } + if (message == null) { + flutterJNI.dispatchEmptyPlatformMessage(channel, replyId); + } else { + flutterJNI.dispatchPlatformMessage(channel, message, message.position(), replyId); + } + } finally { + Trace.endSection(); } } - @Override - public void handleMessageFromDart( - @NonNull final String channel, @Nullable ByteBuffer message, final int replyId) { - Log.v(TAG, "Received message from Dart over channel '" + channel + "'"); - BinaryMessenger.BinaryMessageHandler handler = messageHandlers.get(channel); - if (handler != null) { + private void invokeHandler( + @Nullable HandlerInfo handlerInfo, @Nullable ByteBuffer message, final int replyId) { + // Called from any thread. + if (handlerInfo != null) { try { Log.v(TAG, "Deferring to registered handler to process message."); - handler.onMessage(message, new Reply(flutterJNI, replyId)); - if (message != null && message.isDirect()) { - // This ensures that if a user retains an instance to the ByteBuffer and it happens to - // be direct they will get a deterministic error. - message.limit(0); - } + handlerInfo.handler.onMessage(message, new Reply(flutterJNI, replyId)); } catch (Exception ex) { Log.e(TAG, "Uncaught exception in binary message listener", ex); flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId); @@ -98,6 +306,66 @@ public void handleMessageFromDart( } } + private void dispatchMessageToQueue( + @NonNull String channel, + @Nullable HandlerInfo handlerInfo, + @Nullable ByteBuffer message, + int replyId, + long messageData) { + final DartMessengerTaskQueue taskQueue = (handlerInfo != null) ? handlerInfo.taskQueue : null; + Runnable myRunnable = + () -> { + Trace.beginSection("DartMessenger#handleMessageFromDart on " + channel); + try { + invokeHandler(handlerInfo, message, replyId); + if (message != null && message.isDirect()) { + // This ensures that if a user retains an instance to the ByteBuffer and it + // happens to be direct they will get a deterministic error. + message.limit(0); + } + } finally { + // This is deleting the data underneath the message object. + flutterJNI.cleanupMessageData(messageData); + Trace.endSection(); + } + }; + final DartMessengerTaskQueue nonnullTaskQueue = + taskQueue == null ? platformTaskQueue : taskQueue; + nonnullTaskQueue.dispatch(myRunnable); + } + + @Override + public void handleMessageFromDart( + @NonNull String channel, @Nullable ByteBuffer message, int replyId, long messageData) { + // Called from the ui thread. + Log.v(TAG, "Received message from Dart over channel '" + channel + "'"); + + HandlerInfo handlerInfo; + boolean messageDeferred; + synchronized (handlersLock) { + handlerInfo = messageHandlers.get(channel); + messageDeferred = (enableBufferingIncomingMessages.get() && handlerInfo == null); + if (messageDeferred) { + // The channel is not defined when the Dart VM sends a message before the channels are + // registered. + // + // This is possible if the Dart VM starts before channel registration, and if the thread + // that registers the channels is busy or slow at registering the channel handlers. + // + // In such cases, the task dispatchers are queued, and processed when the channel is + // defined. + if (!bufferedMessages.containsKey(channel)) { + bufferedMessages.put(channel, new LinkedList<>()); + } + List buffer = bufferedMessages.get(channel); + buffer.add(new BufferedMessageInfo(message, replyId, messageData)); + } + } + if (!messageDeferred) { + dispatchMessageToQueue(channel, handlerInfo, message, replyId, messageData); + } + } + @Override public void handlePlatformMessageResponse(int replyId, @Nullable ByteBuffer reply) { Log.v(TAG, "Received message reply from Dart."); diff --git a/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java b/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java index 22bb6f195666f..36a7a9d7eb52c 100644 --- a/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java +++ b/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java @@ -11,7 +11,10 @@ /** Handler that receives messages from Dart code. */ public interface PlatformMessageHandler { void handleMessageFromDart( - @NonNull final String channel, @Nullable ByteBuffer message, final int replyId); + @NonNull final String channel, + @Nullable ByteBuffer message, + final int replyId, + long messageData); void handlePlatformMessageResponse(int replyId, @Nullable ByteBuffer reply); } diff --git a/shell/platform/android/io/flutter/embedding/engine/dart/PlatformTaskQueue.java b/shell/platform/android/io/flutter/embedding/engine/dart/PlatformTaskQueue.java new file mode 100644 index 0000000000000..e90ab89e62223 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/dart/PlatformTaskQueue.java @@ -0,0 +1,19 @@ +// 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. + +package io.flutter.embedding.engine.dart; + +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; + +/** A BinaryMessenger.TaskQueue that posts to the platform thread (aka main thread). */ +public class PlatformTaskQueue implements DartMessenger.DartMessengerTaskQueue { + @NonNull private final Handler handler = new Handler(Looper.getMainLooper()); + + @Override + public void dispatch(@NonNull Runnable runnable) { + handler.post(runnable); + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java index 190324abc9110..0a3c81e87bb68 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -15,10 +15,10 @@ import android.os.Handler; import android.os.Looper; import android.os.SystemClock; -import android.view.Display; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.tracing.Trace; import io.flutter.BuildConfig; import io.flutter.FlutterInjector; import io.flutter.Log; @@ -42,6 +42,7 @@ public class FlutterLoader { // Must match values in flutter::switches static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; + static final String AOT_VMSERVICE_SHARED_LIBRARY_NAME = "aot-vmservice-shared-library-name"; static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data"; static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data"; @@ -51,6 +52,7 @@ public class FlutterLoader { // Resource names used for components of the precompiled snapshot. private static final String DEFAULT_LIBRARY = "libflutter.so"; private static final String DEFAULT_KERNEL_BLOB = "kernel_blob.bin"; + private static final String VMSERVICE_SNAPSHOT_LIBRARY = "libvmservice_snapshot.so"; private static FlutterLoader instance; @@ -134,51 +136,64 @@ public void startInitialization(@NonNull Context applicationContext, @NonNull Se throw new IllegalStateException("startInitialization must be called on the main thread"); } - // Ensure that the context is actually the application context. - final Context appContext = applicationContext.getApplicationContext(); + Trace.beginSection("FlutterLoader#startInitialization"); - this.settings = settings; - - initStartTimestampMillis = SystemClock.uptimeMillis(); - flutterApplicationInfo = ApplicationInfoLoader.load(appContext); - - float fps; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - final DisplayManager dm = appContext.getSystemService(DisplayManager.class); - final Display primaryDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY); - fps = primaryDisplay.getRefreshRate(); - } else { - fps = - ((WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay() - .getRefreshRate(); - } - VsyncWaiter.getInstance(fps).init(); - - // Use a background thread for initialization tasks that require disk access. - Callable initTask = - new Callable() { - @Override - public InitResult call() { - ResourceExtractor resourceExtractor = initResources(appContext); + try { + // Ensure that the context is actually the application context. + final Context appContext = applicationContext.getApplicationContext(); - flutterJNI.loadLibrary(); + this.settings = settings; - // Prefetch the default font manager as soon as possible on a background thread. - // It helps to reduce time cost of engine setup that blocks the platform thread. - executorService.execute(() -> flutterJNI.prefetchDefaultFontManager()); + initStartTimestampMillis = SystemClock.uptimeMillis(); + flutterApplicationInfo = ApplicationInfoLoader.load(appContext); - if (resourceExtractor != null) { - resourceExtractor.waitForCompletion(); + VsyncWaiter waiter; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 /* 17 */) { + final DisplayManager dm = + (DisplayManager) appContext.getSystemService(Context.DISPLAY_SERVICE); + waiter = VsyncWaiter.getInstance(dm, flutterJNI); + } else { + float fps = + ((WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay() + .getRefreshRate(); + waiter = VsyncWaiter.getInstance(fps, flutterJNI); + } + waiter.init(); + + // Use a background thread for initialization tasks that require disk access. + Callable initTask = + new Callable() { + @Override + public InitResult call() { + Trace.beginSection("FlutterLoader initTask"); + + try { + ResourceExtractor resourceExtractor = initResources(appContext); + + flutterJNI.loadLibrary(); + + // Prefetch the default font manager as soon as possible on a background thread. + // It helps to reduce time cost of engine setup that blocks the platform thread. + executorService.execute(() -> flutterJNI.prefetchDefaultFontManager()); + + if (resourceExtractor != null) { + resourceExtractor.waitForCompletion(); + } + + return new InitResult( + PathUtils.getFilesDir(appContext), + PathUtils.getCacheDirectory(appContext), + PathUtils.getDataDirectory(appContext)); + } finally { + Trace.endSection(); + } } - - return new InitResult( - PathUtils.getFilesDir(appContext), - PathUtils.getCacheDirectory(appContext), - PathUtils.getDataDirectory(appContext)); - } - }; - initResultFuture = executorService.submit(initTask); + }; + initResultFuture = executorService.submit(initTask); + } finally { + Trace.endSection(); + } } /** @@ -202,6 +217,8 @@ public void ensureInitializationComplete( throw new IllegalStateException( "ensureInitializationComplete must be called after startInitialization"); } + Trace.beginSection("FlutterLoader#ensureInitializationComplete"); + try { InitResult result = initResultFuture.get(); @@ -240,6 +257,13 @@ public void ensureInitializationComplete( + flutterApplicationInfo.nativeLibraryDir + File.separator + flutterApplicationInfo.aotSharedLibraryName); + + // In profile mode, provide a separate library containing a snapshot for + // launching the Dart VM service isolate. + if (BuildConfig.PROFILE) { + shellArgs.add( + "--" + AOT_VMSERVICE_SHARED_LIBRARY_NAME + "=" + VMSERVICE_SNAPSHOT_LIBRARY); + } } shellArgs.add("--cache-dir-path=" + result.engineCachesPath); @@ -289,6 +313,8 @@ public void ensureInitializationComplete( } catch (Exception e) { Log.e(TAG, "Flutter initialization failed.", e); throw new RuntimeException(e); + } finally { + Trace.endSection(); } } diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java index e76023471e2bd..87e7e36fe8d9a 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java @@ -9,6 +9,7 @@ import io.flutter.Log; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.plugin.common.JSONMethodCodec; +import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; /** TODO(mattcarroll): fill in javadoc for NavigationChannel. */ @@ -19,8 +20,19 @@ public class NavigationChannel { public NavigationChannel(@NonNull DartExecutor dartExecutor) { this.channel = new MethodChannel(dartExecutor, "flutter/navigation", JSONMethodCodec.INSTANCE); + channel.setMethodCallHandler(defaultHandler); } + // Provide a default handler that returns an empty response to any messages + // on this channel. + private final MethodChannel.MethodCallHandler defaultHandler = + new MethodChannel.MethodCallHandler() { + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + result.success(null); + } + }; + public void setInitialRoute(@NonNull String initialRoute) { Log.v(TAG, "Sending message to set initial route to '" + initialRoute + "'"); channel.invokeMethod("setInitialRoute", initialRoute); diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java index 2c484365b377a..7249620ecdd1d 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java @@ -62,7 +62,7 @@ public RestorationChannel( */ public final boolean waitForRestorationData; - // Holds the the most current restoration data which may have been provided by the engine + // Holds the most current restoration data which may have been provided by the engine // via "setRestorationData" or by the framework via the method channel. This is the data the // framework should be restored to in case the app is terminated. private byte[] restorationData; diff --git a/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java b/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java index 55eb3109af5bb..b1f06cb80c92f 100644 --- a/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java @@ -36,6 +36,7 @@ public final class BasicMessageChannel { @NonNull private final BinaryMessenger messenger; @NonNull private final String name; @NonNull private final MessageCodec codec; + @Nullable private final BinaryMessenger.TaskQueue taskQueue; /** * Creates a new channel associated with the specified {@link BinaryMessenger} and with the @@ -47,6 +48,25 @@ public final class BasicMessageChannel { */ public BasicMessageChannel( @NonNull BinaryMessenger messenger, @NonNull String name, @NonNull MessageCodec codec) { + this(messenger, name, codec, null); + } + + /** + * Creates a new channel associated with the specified {@link BinaryMessenger} and with the + * specified name and {@link MessageCodec}. + * + * @param messenger a {@link BinaryMessenger}. + * @param name a channel name String. + * @param codec a {@link MessageCodec}. + * @param taskQueue a {@link BinaryMessenger.TaskQueue} that specifies what thread will execute + * the handler. Specifying null means execute on the platform thread. See also {@link + * BinaryMessenger#makeBackgroundTaskQueue()}. + */ + public BasicMessageChannel( + @NonNull BinaryMessenger messenger, + @NonNull String name, + @NonNull MessageCodec codec, + BinaryMessenger.TaskQueue taskQueue) { if (BuildConfig.DEBUG) { if (messenger == null) { Log.e(TAG, "Parameter messenger must not be null."); @@ -61,6 +81,7 @@ public BasicMessageChannel( this.messenger = messenger; this.name = name; this.codec = codec; + this.taskQueue = taskQueue; } /** @@ -101,7 +122,16 @@ public void send(@Nullable T message, @Nullable final Reply callback) { */ @UiThread public void setMessageHandler(@Nullable final MessageHandler handler) { - messenger.setMessageHandler(name, handler == null ? null : new IncomingMessageHandler(handler)); + // We call the 2 parameter variant specifically to avoid breaking changes in + // mock verify calls. + // See https://github.com/flutter/flutter/issues/92582. + if (taskQueue != null) { + messenger.setMessageHandler( + name, handler == null ? null : new IncomingMessageHandler(handler), taskQueue); + } else { + messenger.setMessageHandler( + name, handler == null ? null : new IncomingMessageHandler(handler)); + } } /** diff --git a/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java b/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java index fb1a22e6f4d3c..2466a01ade303 100644 --- a/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java +++ b/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java @@ -26,6 +26,60 @@ * @see EventChannel , which supports communication using event streams. */ public interface BinaryMessenger { + /** + * An abstraction over the threading policy used to invoke message handlers. + * + *

These are generated by calling methods like {@link + * BinaryMessenger#makeBackgroundTaskQueue(TaskQueueOptions)} and can be passed into platform + * channels' constructors to control the threading policy for handling platform channels' + * messages. + */ + public interface TaskQueue {} + + /** Options that control how a TaskQueue should operate and be created. */ + public static class TaskQueueOptions { + private boolean isSerial = true; + + public boolean getIsSerial() { + return isSerial; + } + + /** + * Setter for `isSerial` property. + * + *

When this is true all tasks performed by the TaskQueue will be forced to happen serially + * (one completes before the other begins). + */ + public TaskQueueOptions setIsSerial(boolean isSerial) { + this.isSerial = isSerial; + return this; + } + } + + /** + * Creates a TaskQueue that executes the tasks serially on a background thread. + * + *

There is no guarantee that the tasks will execute on the same thread, just that execution is + * serial. This is could be problematic if your code relies on ThreadLocal storage or + * introspection about what thread is actually executing. + */ + @UiThread + default TaskQueue makeBackgroundTaskQueue() { + return makeBackgroundTaskQueue(new TaskQueueOptions()); + } + + /** + * Creates a TaskQueue that executes the tasks serially on a background thread. + * + *

There is no guarantee that the tasks will execute on the same thread, just that execution is + * serial. + */ + @UiThread + default TaskQueue makeBackgroundTaskQueue(TaskQueueOptions options) { + // TODO(92582): Remove default implementation when it is safe for Google Flutter users. + throw new UnsupportedOperationException("makeBackgroundTaskQueue not implemented."); + } + /** * Sends a binary message to the Flutter application. * @@ -66,6 +120,54 @@ public interface BinaryMessenger { @UiThread void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler); + /** + * Registers a handler to be invoked when the Flutter application sends a message to its host + * platform. + * + *

Registration overwrites any previous registration for the same channel name. Use a null + * handler to deregister. + * + *

If no handler has been registered for a particular channel, any incoming message on that + * channel will be handled silently by sending a null reply. + * + * @param channel the name {@link String} of the channel. + * @param handler a {@link BinaryMessageHandler} to be invoked on incoming messages, or null. + * @param taskQueue a {@link BinaryMessenger.TaskQueue} that specifies what thread will execute + * the handler. Specifying null means execute on the platform thread. + */ + @UiThread + default void setMessageHandler( + @NonNull String channel, + @Nullable BinaryMessageHandler handler, + @Nullable TaskQueue taskQueue) { + // TODO(92582): Remove default implementation when it is safe for Google Flutter users. + if (taskQueue != null) { + throw new UnsupportedOperationException( + "setMessageHandler called with nonnull taskQueue is not supported."); + } + setMessageHandler(channel, handler); + } + + /** + * Enables the ability to queue messages received from Dart. + * + *

This is useful when there are pending channel handler registrations. For example, Dart may + * be initialized concurrently, and prior to the registration of the channel handlers. This + * implies that Dart may start sending messages while plugins are being registered. + */ + default void enableBufferingIncomingMessages() { + throw new UnsupportedOperationException("enableBufferingIncomingMessages not implemented."); + } + + /** + * Disables the ability to queue messages received from Dart. + * + *

This can be used after all pending channel handlers have been registered. + */ + default void disableBufferingIncomingMessages() { + throw new UnsupportedOperationException("disableBufferingIncomingMessages not implemented."); + } + /** Handler for incoming binary messages from Flutter. */ interface BinaryMessageHandler { /** @@ -97,7 +199,6 @@ interface BinaryReply { * outgoing replies must place the reply bytes between position zero and current position. * Reply receivers can read from the buffer directly. */ - @UiThread void reply(@Nullable ByteBuffer reply); } } diff --git a/shell/platform/android/io/flutter/plugin/common/EventChannel.java b/shell/platform/android/io/flutter/plugin/common/EventChannel.java index 984b251b29d65..f8835f90904c4 100644 --- a/shell/platform/android/io/flutter/plugin/common/EventChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/EventChannel.java @@ -4,6 +4,7 @@ package io.flutter.plugin.common; +import androidx.annotation.Nullable; import androidx.annotation.UiThread; import io.flutter.BuildConfig; import io.flutter.Log; @@ -34,6 +35,7 @@ public final class EventChannel { private final BinaryMessenger messenger; private final String name; private final MethodCodec codec; + @Nullable private final BinaryMessenger.TaskQueue taskQueue; /** * Creates a new channel associated with the specified {@link BinaryMessenger} and with the @@ -55,6 +57,25 @@ public EventChannel(BinaryMessenger messenger, String name) { * @param codec a {@link MessageCodec}. */ public EventChannel(BinaryMessenger messenger, String name, MethodCodec codec) { + this(messenger, name, codec, null); + } + + /** + * Creates a new channel associated with the specified {@link BinaryMessenger} and with the + * specified name and {@link MethodCodec}. + * + * @param messenger a {@link BinaryMessenger}. + * @param name a channel name String. + * @param codec a {@link MessageCodec}. + * @param taskQueue a {@link BinaryMessenger.TaskQueue} that specifies what thread will execute + * the handler. Specifying null means execute on the platform thread. See also {@link + * BinaryMessenger#makeBackgroundTaskQueue()}. + */ + public EventChannel( + BinaryMessenger messenger, + String name, + MethodCodec codec, + BinaryMessenger.TaskQueue taskQueue) { if (BuildConfig.DEBUG) { if (messenger == null) { Log.e(TAG, "Parameter messenger must not be null."); @@ -69,6 +90,7 @@ public EventChannel(BinaryMessenger messenger, String name, MethodCodec codec) { this.messenger = messenger; this.name = name; this.codec = codec; + this.taskQueue = taskQueue; } /** @@ -83,8 +105,16 @@ public EventChannel(BinaryMessenger messenger, String name, MethodCodec codec) { */ @UiThread public void setStreamHandler(final StreamHandler handler) { - messenger.setMessageHandler( - name, handler == null ? null : new IncomingStreamRequestHandler(handler)); + // We call the 2 parameter variant specifically to avoid breaking changes in + // mock verify calls. + // See https://github.com/flutter/flutter/issues/92582. + if (taskQueue != null) { + messenger.setMessageHandler( + name, handler == null ? null : new IncomingStreamRequestHandler(handler), taskQueue); + } else { + messenger.setMessageHandler( + name, handler == null ? null : new IncomingStreamRequestHandler(handler)); + } } /** diff --git a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java index 901299b943019..3c0149754fccb 100644 --- a/shell/platform/android/io/flutter/plugin/common/MethodChannel.java +++ b/shell/platform/android/io/flutter/plugin/common/MethodChannel.java @@ -35,6 +35,7 @@ public class MethodChannel { private final BinaryMessenger messenger; private final String name; private final MethodCodec codec; + private final BinaryMessenger.TaskQueue taskQueue; /** * Creates a new channel associated with the specified {@link BinaryMessenger} and with the @@ -56,6 +57,25 @@ public MethodChannel(BinaryMessenger messenger, String name) { * @param codec a {@link MessageCodec}. */ public MethodChannel(BinaryMessenger messenger, String name, MethodCodec codec) { + this(messenger, name, codec, null); + } + + /** + * Creates a new channel associated with the specified {@link BinaryMessenger} and with the + * specified name and {@link MethodCodec}. + * + * @param messenger a {@link BinaryMessenger}. + * @param name a channel name String. + * @param codec a {@link MessageCodec}. + * @param taskQueue a {@link BinaryMessenger.TaskQueue} that specifies what thread will execute + * the handler. Specifying null means execute on the platform thread. See also {@link + * BinaryMessenger#makeBackgroundTaskQueue()}. + */ + public MethodChannel( + BinaryMessenger messenger, + String name, + MethodCodec codec, + @Nullable BinaryMessenger.TaskQueue taskQueue) { if (BuildConfig.DEBUG) { if (messenger == null) { Log.e(TAG, "Parameter messenger must not be null."); @@ -70,6 +90,7 @@ public MethodChannel(BinaryMessenger messenger, String name, MethodCodec codec) this.messenger = messenger; this.name = name; this.codec = codec; + this.taskQueue = taskQueue; } /** @@ -116,8 +137,16 @@ public void invokeMethod(String method, @Nullable Object arguments, @Nullable Re */ @UiThread public void setMethodCallHandler(final @Nullable MethodCallHandler handler) { - messenger.setMessageHandler( - name, handler == null ? null : new IncomingMethodCallHandler(handler)); + // We call the 2 parameter variant specifically to avoid breaking changes in + // mock verify calls. + // See https://github.com/flutter/flutter/issues/92582. + if (taskQueue != null) { + messenger.setMessageHandler( + name, handler == null ? null : new IncomingMethodCallHandler(handler), taskQueue); + } else { + messenger.setMessageHandler( + name, handler == null ? null : new IncomingMethodCallHandler(handler)); + } } /** diff --git a/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java b/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java index 1fb2e720367b6..0a6f0ef3f64db 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java +++ b/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java @@ -17,7 +17,7 @@ import java.util.List; // Loosely based off of -// https://github.com/android/user-interface-samples/blob/master/WindowInsetsAnimation/app/src/main/java/com/google/android/samples/insetsanimation/RootViewDeferringInsetsCallback.kt +// https://github.com/android/user-interface-samples/blob/main/WindowInsetsAnimation/app/src/main/java/com/google/android/samples/insetsanimation/RootViewDeferringInsetsCallback.kt // // When the IME is shown or hidden, it immediately sends an onApplyWindowInsets call // with the final state of the IME. This initial call disrupts the animation, which diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index ad425d461c574..1496253a25c39 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -20,6 +20,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.core.view.WindowInsetsControllerCompat; import io.flutter.Log; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import java.io.FileNotFoundException; @@ -237,11 +238,7 @@ public void onSystemUiVisibilityChange(int visibility) { } private void setSystemChromeEnabledSystemUIMode(PlatformChannel.SystemUiMode systemUiMode) { - int enabledOverlays = - DEFAULT_SYSTEM_UI - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + int enabledOverlays; if (systemUiMode == PlatformChannel.SystemUiMode.LEAN_BACK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { @@ -300,6 +297,9 @@ private void setSystemChromeEnabledSystemUIMode(PlatformChannel.SystemUiMode sys View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + } else { + // When none of the conditions are matched, return without updating the system UI overlays. + return; } mEnabledOverlays = enabledOverlays; @@ -364,7 +364,8 @@ private void setSystemChromeSystemUIOverlayStyle( PlatformChannel.SystemChromeStyle systemChromeStyle) { Window window = activity.getWindow(); View view = window.getDecorView(); - int flags = view.getSystemUiVisibility(); + WindowInsetsControllerCompat windowInsetsControllerCompat = + new WindowInsetsControllerCompat(window, view); // SYSTEM STATUS BAR ------------------------------------------------------------------- // You can't change the color of the system status bar until SDK 21, and you can't change the @@ -377,11 +378,14 @@ private void setSystemChromeSystemUIOverlayStyle( if (systemChromeStyle.statusBarIconBrightness != null) { switch (systemChromeStyle.statusBarIconBrightness) { case DARK: - // View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - flags |= 0x2000; + // Dark status bar icon brightness. + // Light status bar appearance. + windowInsetsControllerCompat.setAppearanceLightStatusBars(true); break; case LIGHT: - flags &= ~0x2000; + // Light status bar icon brightness. + // Dark status bar appearance. + windowInsetsControllerCompat.setAppearanceLightStatusBars(false); break; } } @@ -408,11 +412,14 @@ private void setSystemChromeSystemUIOverlayStyle( if (systemChromeStyle.systemNavigationBarIconBrightness != null) { switch (systemChromeStyle.systemNavigationBarIconBrightness) { case DARK: - // View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR - flags |= 0x10; + // Dark navigation bar icon brightness. + // Light navigation bar appearance. + windowInsetsControllerCompat.setAppearanceLightNavigationBars(true); break; case LIGHT: - flags &= ~0x10; + // Light navigation bar icon brightness. + // Dark navigation bar appearance. + windowInsetsControllerCompat.setAppearanceLightNavigationBars(false); break; } } @@ -438,7 +445,6 @@ private void setSystemChromeSystemUIOverlayStyle( systemChromeStyle.systemNavigationBarContrastEnforced); } - view.setSystemUiVisibility(flags); currentTheme = systemChromeStyle; } diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 8590abf260291..227dbc8829dda 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -2023,7 +2023,7 @@ public interface OnAccessibilityChangeListener { } // Must match SemanticsActions in semantics.dart - // https://github.com/flutter/engine/blob/master/lib/ui/semantics.dart + // https://github.com/flutter/engine/blob/main/lib/ui/semantics.dart public enum Action { TAP(1 << 0), LONG_PRESS(1 << 1), @@ -2056,7 +2056,7 @@ public enum Action { } // Must match SemanticsFlag in semantics.dart - // https://github.com/flutter/engine/blob/master/lib/ui/semantics.dart + // https://github.com/flutter/engine/blob/main/lib/ui/semantics.dart /* Package */ enum Flag { HAS_CHECKED_STATE(1 << 0), IS_CHECKED(1 << 1), @@ -2196,7 +2196,7 @@ private static class LocaleStringAttribute extends StringAttribute { * analogous concept within Flutter. * *

To see how this {@code SemanticsNode}'s fields correspond to Flutter's semantics system, see - * semantics.dart: https://github.com/flutter/engine/blob/master/lib/ui/semantics.dart + * semantics.dart: https://github.com/flutter/engine/blob/main/lib/ui/semantics.dart */ private static class SemanticsNode { private static boolean nullableHasAncestor( diff --git a/shell/platform/android/io/flutter/view/FlutterNativeView.java b/shell/platform/android/io/flutter/view/FlutterNativeView.java index 63005dc8bb534..190a9ecbb160e 100644 --- a/shell/platform/android/io/flutter/view/FlutterNativeView.java +++ b/shell/platform/android/io/flutter/view/FlutterNativeView.java @@ -111,7 +111,11 @@ public void runFromBundle(FlutterRunArguments args) { if (applicationIsRunning) throw new AssertionError("This Flutter engine instance is already running an application"); mFlutterJNI.runBundleAndSnapshotFromLibrary( - args.bundlePath, args.entrypoint, args.libraryPath, mContext.getResources().getAssets()); + args.bundlePath, + args.entrypoint, + args.libraryPath, + mContext.getResources().getAssets(), + null); applicationIsRunning = true; } @@ -124,6 +128,12 @@ public static String getObservatoryUri() { return FlutterJNI.getObservatoryUri(); } + @Override + @UiThread + public TaskQueue makeBackgroundTaskQueue(TaskQueueOptions options) { + return dartExecutor.getBinaryMessenger().makeBackgroundTaskQueue(options); + } + @Override @UiThread public void send(String channel, ByteBuffer message) { @@ -147,6 +157,18 @@ public void setMessageHandler(String channel, BinaryMessageHandler handler) { dartExecutor.getBinaryMessenger().setMessageHandler(channel, handler); } + @Override + @UiThread + public void setMessageHandler(String channel, BinaryMessageHandler handler, TaskQueue taskQueue) { + dartExecutor.getBinaryMessenger().setMessageHandler(channel, handler, taskQueue); + } + + @Override + public void enableBufferingIncomingMessages() {} + + @Override + public void disableBufferingIncomingMessages() {} + /*package*/ FlutterJNI getFlutterJNI() { return mFlutterJNI; } @@ -157,7 +179,7 @@ private void attach(FlutterNativeView view) { } private final class EngineLifecycleListenerImpl implements EngineLifecycleListener { - // Called by native to notify when the engine is restarted (cold reload). + // Called by native to notify right before the engine is restarted (cold reload). @SuppressWarnings("unused") public void onPreEngineRestart() { if (mFlutterView != null) { diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 4876f1b67c04c..fd60b04812075 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -347,6 +347,12 @@ public void removeFirstFrameListener(FirstFrameListener listener) { mFirstFrameListeners.remove(listener); } + @Override + public void enableBufferingIncomingMessages() {} + + @Override + public void disableBufferingIncomingMessages() {} + /** * Reverts this back to the {@link SurfaceView} defaults, at the back of its window and opaque. */ @@ -822,6 +828,12 @@ public PointerIcon getSystemPointerIcon(int type) { return PointerIcon.getSystemIcon(getContext(), type); } + @Override + @UiThread + public TaskQueue makeBackgroundTaskQueue(TaskQueueOptions options) { + return null; + } + @Override @UiThread public void send(String channel, ByteBuffer message) { @@ -844,6 +856,12 @@ public void setMessageHandler(String channel, BinaryMessageHandler handler) { mNativeView.setMessageHandler(channel, handler); } + @Override + @UiThread + public void setMessageHandler(String channel, BinaryMessageHandler handler, TaskQueue taskQueue) { + mNativeView.setMessageHandler(channel, handler, taskQueue); + } + /** Listener will be called on the Android UI thread once when Flutter renders the first frame. */ public interface FirstFrameListener { void onFirstFrame(); diff --git a/shell/platform/android/io/flutter/view/VsyncWaiter.java b/shell/platform/android/io/flutter/view/VsyncWaiter.java index 714511f5bb3fa..200eb784675a0 100644 --- a/shell/platform/android/io/flutter/view/VsyncWaiter.java +++ b/shell/platform/android/io/flutter/view/VsyncWaiter.java @@ -4,24 +4,85 @@ package io.flutter.view; +import android.annotation.TargetApi; +import android.hardware.display.DisplayManager; import android.view.Choreographer; +import android.view.Display; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.FlutterJNI; // TODO(mattcarroll): add javadoc. public class VsyncWaiter { + @TargetApi(17) + class DisplayListener implements DisplayManager.DisplayListener { + DisplayListener(DisplayManager displayManager) { + this.displayManager = displayManager; + } + + private DisplayManager displayManager; + + void register() { + displayManager.registerDisplayListener(this, null); + } + + @Override + public void onDisplayAdded(int displayId) {} + + @Override + public void onDisplayRemoved(int displayId) {} + + @Override + public void onDisplayChanged(int displayId) { + if (displayId == Display.DEFAULT_DISPLAY) { + final Display primaryDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); + float fps = primaryDisplay.getRefreshRate(); + VsyncWaiter.this.refreshPeriodNanos = (long) (1000000000.0 / fps); + VsyncWaiter.this.flutterJNI.setRefreshRateFPS(fps); + } + } + } + private static VsyncWaiter instance; + private static DisplayListener listener; + private long refreshPeriodNanos = -1; + private FlutterJNI flutterJNI; @NonNull - public static VsyncWaiter getInstance(float fps) { + public static VsyncWaiter getInstance(float fps, FlutterJNI flutterJNI) { if (instance == null) { - instance = new VsyncWaiter(fps); + instance = new VsyncWaiter(flutterJNI); } + flutterJNI.setRefreshRateFPS(fps); + instance.refreshPeriodNanos = (long) (1000000000.0 / fps); return instance; } - private final float fps; - private final long refreshPeriodNanos; + @TargetApi(17) + @NonNull + public static VsyncWaiter getInstance(DisplayManager displayManager, FlutterJNI flutterJNI) { + if (instance == null) { + instance = new VsyncWaiter(flutterJNI); + } + if (listener == null) { + listener = instance.new DisplayListener(displayManager); + listener.register(); + } + if (instance.refreshPeriodNanos == -1) { + final Display primaryDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); + float fps = primaryDisplay.getRefreshRate(); + instance.refreshPeriodNanos = (long) (1000000000.0 / fps); + flutterJNI.setRefreshRateFPS(fps); + } + return instance; + } + + // For tests, to reset the singleton between tests. + @VisibleForTesting + public static void reset() { + instance = null; + listener = null; + } private final FlutterJNI.AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate = new FlutterJNI.AsyncWaitForVsyncDelegate() { @@ -36,21 +97,17 @@ public void doFrame(long frameTimeNanos) { if (delay < 0) { delay = 0; } - FlutterJNI.nativeOnVsync(delay, refreshPeriodNanos, cookie); + flutterJNI.nativeOnVsync(delay, refreshPeriodNanos, cookie); } }); } }; - private VsyncWaiter(float fps) { - this.fps = fps; - refreshPeriodNanos = (long) (1000000000.0 / fps); + private VsyncWaiter(FlutterJNI flutterJNI) { + this.flutterJNI = flutterJNI; } public void init() { - FlutterJNI.setAsyncWaitForVsyncDelegate(asyncWaitForVsyncDelegate); - - // TODO(mattcarroll): look into moving FPS reporting to a plugin - FlutterJNI.setRefreshRateFPS(fps); + flutterJNI.setAsyncWaitForVsyncDelegate(asyncWaitForVsyncDelegate); } } diff --git a/shell/platform/android/platform_message_handler_android.cc b/shell/platform/android/platform_message_handler_android.cc new file mode 100644 index 0000000000000..2179611a2f90e --- /dev/null +++ b/shell/platform/android/platform_message_handler_android.cc @@ -0,0 +1,64 @@ + +#include "flutter/shell/platform/android/platform_message_handler_android.h" + +namespace flutter { + +PlatformMessageHandlerAndroid::PlatformMessageHandlerAndroid( + const std::shared_ptr& jni_facade) + : jni_facade_(jni_facade) {} + +void PlatformMessageHandlerAndroid::InvokePlatformMessageResponseCallback( + int response_id, + std::unique_ptr mapping) { + // Called from any thread. + if (!response_id) { + return; + } + // TODO(gaaclarke): Move the jump to the ui thread here from + // PlatformMessageResponseDart so we won't need to use a mutex anymore. + fml::RefPtr message_response; + { + std::lock_guard lock(pending_responses_mutex_); + auto it = pending_responses_.find(response_id); + if (it == pending_responses_.end()) + return; + message_response = std::move(it->second); + pending_responses_.erase(it); + } + + message_response->Complete(std::move(mapping)); +} + +void PlatformMessageHandlerAndroid::InvokePlatformMessageEmptyResponseCallback( + int response_id) { + // Called from any thread. + if (!response_id) { + return; + } + fml::RefPtr message_response; + { + std::lock_guard lock(pending_responses_mutex_); + auto it = pending_responses_.find(response_id); + if (it == pending_responses_.end()) + return; + message_response = std::move(it->second); + pending_responses_.erase(it); + } + message_response->CompleteEmpty(); +} + +// |PlatformView| +void PlatformMessageHandlerAndroid::HandlePlatformMessage( + std::unique_ptr message) { + // Called from the ui thread. + int response_id = next_response_id_++; + if (auto response = message->response()) { + std::lock_guard lock(pending_responses_mutex_); + pending_responses_[response_id] = response; + } + // This call can re-enter in InvokePlatformMessageXxxResponseCallback. + jni_facade_->FlutterViewHandlePlatformMessage(std::move(message), + response_id); +} + +} // namespace flutter diff --git a/shell/platform/android/platform_message_handler_android.h b/shell/platform/android/platform_message_handler_android.h new file mode 100644 index 0000000000000..dc36013db69c1 --- /dev/null +++ b/shell/platform/android/platform_message_handler_android.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 SHELL_PLATFORM_ANDROID_PLATFORM_MESSAGE_HANDLER_H_ +#define SHELL_PLATFORM_ANDROID_PLATFORM_MESSAGE_HANDLER_H_ + +#include +#include +#include +#include + +#include "flutter/lib/ui/window/platform_message.h" +#include "flutter/shell/common/platform_message_handler.h" +#include "flutter/shell/platform/android/jni/platform_view_android_jni.h" + +namespace flutter { +class PlatformMessageHandlerAndroid : public PlatformMessageHandler { + public: + PlatformMessageHandlerAndroid( + const std::shared_ptr& jni_facade); + void HandlePlatformMessage(std::unique_ptr message) override; + void InvokePlatformMessageResponseCallback( + int response_id, + std::unique_ptr mapping) override; + + void InvokePlatformMessageEmptyResponseCallback(int response_id) override; + + private: + const std::shared_ptr jni_facade_; + int next_response_id_ = 1; + std::unordered_map> + pending_responses_; + std::mutex pending_responses_mutex_; +}; +} // namespace flutter + +#endif diff --git a/shell/platform/android/platform_view_android.cc b/shell/platform/android/platform_view_android.cc index 50b9d55c6ca4a..c3d4c696628f4 100644 --- a/shell/platform/android/platform_view_android.cc +++ b/shell/platform/android/platform_view_android.cc @@ -46,13 +46,14 @@ std::unique_ptr AndroidSurfaceFactoryImpl::CreateSurface() { } static std::shared_ptr CreateAndroidContext( - bool use_software_rendering) { + bool use_software_rendering, + const flutter::TaskRunners task_runners) { if (use_software_rendering) { return std::make_shared(AndroidRenderingAPI::kSoftware); } return std::make_unique( AndroidRenderingAPI::kOpenGLES, - fml::MakeRefCounted()); + fml::MakeRefCounted(), task_runners); } PlatformViewAndroid::PlatformViewAndroid( @@ -60,10 +61,11 @@ PlatformViewAndroid::PlatformViewAndroid( flutter::TaskRunners task_runners, std::shared_ptr jni_facade, bool use_software_rendering) - : PlatformViewAndroid(delegate, - std::move(task_runners), - std::move(jni_facade), - CreateAndroidContext(use_software_rendering)) {} + : PlatformViewAndroid( + delegate, + std::move(task_runners), + std::move(jni_facade), + CreateAndroidContext(use_software_rendering, task_runners)) {} PlatformViewAndroid::PlatformViewAndroid( PlatformView::Delegate& delegate, @@ -73,9 +75,8 @@ PlatformViewAndroid::PlatformViewAndroid( : PlatformView(delegate, std::move(task_runners)), jni_facade_(jni_facade), android_context_(std::move(android_context)), - platform_view_android_delegate_(jni_facade) { - // TODO(dnfield): always create a pbuffer surface for background use to - // resolve https://github.com/flutter/flutter/issues/73675 + platform_view_android_delegate_(jni_facade), + platform_message_handler_(new PlatformMessageHandlerAndroid(jni_facade)) { if (android_context_) { FML_CHECK(android_context_->IsValid()) << "Could not create surface from invalid Android context."; @@ -190,51 +191,11 @@ void PlatformViewAndroid::DispatchEmptyPlatformMessage(JNIEnv* env, std::move(response))); } -void PlatformViewAndroid::InvokePlatformMessageResponseCallback( - JNIEnv* env, - jint response_id, - jobject java_response_data, - jint java_response_position) { - if (!response_id) - return; - auto it = pending_responses_.find(response_id); - if (it == pending_responses_.end()) - return; - uint8_t* response_data = - static_cast(env->GetDirectBufferAddress(java_response_data)); - FML_DCHECK(response_data != nullptr); - std::vector response = std::vector( - response_data, response_data + java_response_position); - auto message_response = std::move(it->second); - pending_responses_.erase(it); - message_response->Complete( - std::make_unique(std::move(response))); -} - -void PlatformViewAndroid::InvokePlatformMessageEmptyResponseCallback( - JNIEnv* env, - jint response_id) { - if (!response_id) - return; - auto it = pending_responses_.find(response_id); - if (it == pending_responses_.end()) - return; - auto message_response = std::move(it->second); - pending_responses_.erase(it); - message_response->CompleteEmpty(); -} - // |PlatformView| void PlatformViewAndroid::HandlePlatformMessage( std::unique_ptr message) { - int response_id = next_response_id_++; - if (auto response = message->response()) { - pending_responses_[response_id] = response; - } - // This call can re-enter in InvokePlatformMessageXxxResponseCallback. - jni_facade_->FlutterViewHandlePlatformMessage(std::move(message), - response_id); - message = nullptr; + // Called from the ui thread. + platform_message_handler_->HandlePlatformMessage(std::move(message)); } // |PlatformView| @@ -286,7 +247,8 @@ std::unique_ptr PlatformViewAndroid::CreateRenderingSurface() { if (!android_surface_) { return nullptr; } - return android_surface_->CreateGPUSurface(); + return android_surface_->CreateGPUSurface( + android_context_->GetMainSkiaContext().get()); } // |PlatformView| diff --git a/shell/platform/android/platform_view_android.h b/shell/platform/android/platform_view_android.h index 1bd959ef855dd..0ff654f0fba72 100644 --- a/shell/platform/android/platform_view_android.h +++ b/shell/platform/android/platform_view_android.h @@ -17,6 +17,7 @@ #include "flutter/shell/common/snapshot_surface_producer.h" #include "flutter/shell/platform/android/context/android_context.h" #include "flutter/shell/platform/android/jni/platform_view_android_jni.h" +#include "flutter/shell/platform/android/platform_message_handler_android.h" #include "flutter/shell/platform/android/platform_view_android_delegate/platform_view_android_delegate.h" #include "flutter/shell/platform/android/surface/android_native_window.h" #include "flutter/shell/platform/android/surface/android_surface.h" @@ -79,14 +80,6 @@ class PlatformViewAndroid final : public PlatformView { std::string name, jint response_id); - void InvokePlatformMessageResponseCallback(JNIEnv* env, - jint response_id, - jobject java_response_data, - jint java_response_position); - - void InvokePlatformMessageEmptyResponseCallback(JNIEnv* env, - jint response_id); - void DispatchSemanticsAction(JNIEnv* env, jint id, jint action, @@ -116,6 +109,11 @@ class PlatformViewAndroid final : public PlatformView { return android_context_; } + std::shared_ptr GetPlatformMessageHandler() + const override { + return platform_message_handler_; + } + private: const std::shared_ptr jni_facade_; std::shared_ptr android_context_; @@ -124,11 +122,7 @@ class PlatformViewAndroid final : public PlatformView { PlatformViewAndroidDelegate platform_view_android_delegate_; std::unique_ptr android_surface_; - - // We use id 0 to mean that no response is expected. - int next_response_id_ = 1; - std::unordered_map> - pending_responses_; + std::shared_ptr platform_message_handler_; // |PlatformView| void UpdateSemantics( diff --git a/shell/platform/android/platform_view_android_delegate/platform_view_android_delegate.h b/shell/platform/android/platform_view_android_delegate/platform_view_android_delegate.h index 3f76f34c51d80..e1e55a2300e20 100644 --- a/shell/platform/android/platform_view_android_delegate/platform_view_android_delegate.h +++ b/shell/platform/android/platform_view_android_delegate/platform_view_android_delegate.h @@ -16,7 +16,7 @@ namespace flutter { class PlatformViewAndroidDelegate { public: - PlatformViewAndroidDelegate( + explicit PlatformViewAndroidDelegate( std::shared_ptr jni_facade); void UpdateSemantics(flutter::SemanticsNodeUpdates update, flutter::CustomAccessibilityActionUpdates actions); diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc index 376ac155516d0..454969b2172de 100644 --- a/shell/platform/android/platform_view_android_jni_impl.cc +++ b/shell/platform/android/platform_view_android_jni_impl.cc @@ -155,7 +155,8 @@ static jobject SpawnJNI(JNIEnv* env, jlong shell_holder, jstring jEntrypoint, jstring jLibraryUrl, - jstring jInitialRoute) { + jstring jInitialRoute, + jobject jEntrypointArgs) { jobject jni = env->NewObject(g_flutter_jni_class->obj(), g_jni_constructor); if (jni == nullptr) { FML_LOG(ERROR) << "Could not create a FlutterJNI instance"; @@ -169,9 +170,10 @@ static jobject SpawnJNI(JNIEnv* env, auto entrypoint = fml::jni::JavaStringToString(env, jEntrypoint); auto libraryUrl = fml::jni::JavaStringToString(env, jLibraryUrl); auto initial_route = fml::jni::JavaStringToString(env, jInitialRoute); + auto entrypoint_args = fml::jni::StringListToVector(env, jEntrypointArgs); auto spawned_shell_holder = ANDROID_SHELL_HOLDER->Spawn( - jni_facade, entrypoint, libraryUrl, initial_route); + jni_facade, entrypoint, libraryUrl, initial_route, entrypoint_args); if (spawned_shell_holder == nullptr || !spawned_shell_holder->IsValid()) { FML_LOG(ERROR) << "Could not spawn Shell"; @@ -237,7 +239,8 @@ static void RunBundleAndSnapshotFromLibrary(JNIEnv* env, jstring jBundlePath, jstring jEntrypoint, jstring jLibraryUrl, - jobject jAssetManager) { + jobject jAssetManager, + jobject jEntrypointArgs) { auto asset_manager = std::make_shared(); asset_manager->PushBack(std::make_unique( @@ -248,8 +251,10 @@ static void RunBundleAndSnapshotFromLibrary(JNIEnv* env, auto entrypoint = fml::jni::JavaStringToString(env, jEntrypoint); auto libraryUrl = fml::jni::JavaStringToString(env, jLibraryUrl); + auto entrypoint_args = fml::jni::StringListToVector(env, jEntrypointArgs); - ANDROID_SHELL_HOLDER->Launch(asset_manager, entrypoint, libraryUrl); + ANDROID_SHELL_HOLDER->Launch(asset_manager, entrypoint, libraryUrl, + entrypoint_args); } static jobject LookupCallbackInformation(JNIEnv* env, @@ -405,6 +410,13 @@ static void DispatchEmptyPlatformMessage(JNIEnv* env, ); } +static void CleanupMessageData(JNIEnv* env, + jobject jcaller, + jlong message_data) { + // Called from any thread. + free(reinterpret_cast(message_data)); +} + static void DispatchPointerDataPacket(JNIEnv* env, jobject jcaller, jlong shell_holder, @@ -483,22 +495,21 @@ static void InvokePlatformMessageResponseCallback(JNIEnv* env, jint responseId, jobject message, jint position) { - ANDROID_SHELL_HOLDER->GetPlatformView() - ->InvokePlatformMessageResponseCallback(env, // - responseId, // - message, // - position // - ); + uint8_t* response_data = + static_cast(env->GetDirectBufferAddress(message)); + FML_DCHECK(response_data != nullptr); + auto mapping = std::make_unique( + fml::MallocMapping::Copy(response_data, response_data + position)); + ANDROID_SHELL_HOLDER->GetPlatformMessageHandler() + ->InvokePlatformMessageResponseCallback(responseId, std::move(mapping)); } static void InvokePlatformMessageEmptyResponseCallback(JNIEnv* env, jobject jcaller, jlong shell_holder, jint responseId) { - ANDROID_SHELL_HOLDER->GetPlatformView() - ->InvokePlatformMessageEmptyResponseCallback(env, // - responseId // - ); + ANDROID_SHELL_HOLDER->GetPlatformMessageHandler() + ->InvokePlatformMessageEmptyResponseCallback(responseId); } static void NotifyLowMemoryWarning(JNIEnv* env, @@ -623,14 +634,15 @@ bool RegisterApi(JNIEnv* env) { { .name = "nativeSpawn", .signature = "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/" - "String;)Lio/flutter/" + "String;Ljava/util/List;)Lio/flutter/" "embedding/engine/FlutterJNI;", .fnPtr = reinterpret_cast(&SpawnJNI), }, { .name = "nativeRunBundleAndSnapshotFromLibrary", .signature = "(JLjava/lang/String;Ljava/lang/String;" - "Ljava/lang/String;Landroid/content/res/AssetManager;)V", + "Ljava/lang/String;Landroid/content/res/" + "AssetManager;Ljava/util/List;)V", .fnPtr = reinterpret_cast(&RunBundleAndSnapshotFromLibrary), }, { @@ -638,6 +650,11 @@ bool RegisterApi(JNIEnv* env) { .signature = "(JLjava/lang/String;I)V", .fnPtr = reinterpret_cast(&DispatchEmptyPlatformMessage), }, + { + .name = "nativeCleanupMessageData", + .signature = "(J)V", + .fnPtr = reinterpret_cast(&CleanupMessageData), + }, { .name = "nativeDispatchPlatformMessage", .signature = "(JLjava/lang/String;Ljava/nio/ByteBuffer;II)V", @@ -819,7 +836,7 @@ bool RegisterApi(JNIEnv* env) { g_handle_platform_message_method = env->GetMethodID(g_flutter_jni_class->obj(), "handlePlatformMessage", - "(Ljava/lang/String;Ljava/nio/ByteBuffer;I)V"); + "(Ljava/lang/String;Ljava/nio/ByteBuffer;IJ)V"); if (g_handle_platform_message_method == nullptr) { FML_LOG(ERROR) << "Could not locate handlePlatformMessage method"; @@ -1102,6 +1119,7 @@ PlatformViewAndroidJNIImpl::~PlatformViewAndroidJNIImpl() = default; void PlatformViewAndroidJNIImpl::FlutterViewHandlePlatformMessage( std::unique_ptr message, int responseId) { + // Called from the ui thread. JNIEnv* env = fml::jni::AttachCurrentThread(); auto java_object = java_object_.get(env); @@ -1117,11 +1135,14 @@ void PlatformViewAndroidJNIImpl::FlutterViewHandlePlatformMessage( env, env->NewDirectByteBuffer( const_cast(message->data().GetMapping()), message->data().GetSize())); + // Message data is deleted in CleanupMessageData. + fml::MallocMapping mapping = message->releaseData(); env->CallVoidMethod(java_object.obj(), g_handle_platform_message_method, - java_channel.obj(), message_array.obj(), responseId); + java_channel.obj(), message_array.obj(), responseId, + mapping.Release()); } else { env->CallVoidMethod(java_object.obj(), g_handle_platform_message_method, - java_channel.obj(), nullptr, responseId); + java_channel.obj(), nullptr, responseId, nullptr); } FML_CHECK(fml::jni::CheckException(env)); diff --git a/shell/platform/android/surface/BUILD.gn b/shell/platform/android/surface/BUILD.gn index 73cab0833c192..c3e2b9026057a 100644 --- a/shell/platform/android/surface/BUILD.gn +++ b/shell/platform/android/surface/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/shell/config.gni") + source_set("surface") { sources = [ "android_surface.cc", @@ -54,4 +56,8 @@ source_set("surface_mock") { "//third_party/googletest:gtest", "//third_party/skia", ] + + if (shell_enable_vulkan) { + deps += [ "//flutter/vulkan" ] + } } diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java index a574932034ed5..0458ec29d8ca7 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java @@ -26,6 +26,7 @@ import android.media.ImageReader; import android.os.Build; import android.view.DisplayCutout; +import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; @@ -270,10 +271,7 @@ public void setPaddingTopToZeroForFullscreenMode() { // Verify. verify(flutterRenderer, times(3)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingTop); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingBottom); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingLeft); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingRight); + validateViewportMetricPadding(viewportMetricsCaptor, 100, 100, 100, 100); } // This test uses the pre-API 30 Algorithm for window insets. @@ -303,18 +301,12 @@ public void setPaddingTopToZeroForFullscreenModeLegacy() { // Then we simulate the system applying a window inset. WindowInsets windowInsets = mock(WindowInsets.class); - when(windowInsets.getSystemWindowInsetTop()).thenReturn(100); - when(windowInsets.getSystemWindowInsetBottom()).thenReturn(100); - when(windowInsets.getSystemWindowInsetLeft()).thenReturn(100); - when(windowInsets.getSystemWindowInsetRight()).thenReturn(100); + mockSystemWindowInsets(windowInsets, 100, 100, 100, 100); flutterView.onApplyWindowInsets(windowInsets); // Verify. verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingTop); - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingLeft); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingRight); + validateViewportMetricPadding(viewportMetricsCaptor, 100, 0, 100, 0); } // This test uses the API 30+ Algorithm for window insets. The legacy algorithm is @@ -354,10 +346,7 @@ public void reportSystemInsetWhenNotFullscreen() { // Verify. verify(flutterRenderer, times(3)).setViewportMetrics(viewportMetricsCaptor.capture()); // Top padding is reported as-is. - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingTop); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingBottom); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingLeft); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingRight); + validateViewportMetricPadding(viewportMetricsCaptor, 100, 100, 100, 100); } // This test uses the pre-API 30 Algorithm for window insets. @@ -385,19 +374,13 @@ public void reportSystemInsetWhenNotFullscreenLegacy() { // Then we simulate the system applying a window inset. WindowInsets windowInsets = mock(WindowInsets.class); - when(windowInsets.getSystemWindowInsetTop()).thenReturn(100); - when(windowInsets.getSystemWindowInsetBottom()).thenReturn(100); - when(windowInsets.getSystemWindowInsetLeft()).thenReturn(100); - when(windowInsets.getSystemWindowInsetRight()).thenReturn(100); + mockSystemWindowInsets(windowInsets, 100, 100, 100, 100); flutterView.onApplyWindowInsets(windowInsets); // Verify. verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture()); // Top padding is reported as-is. - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingTop); - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingLeft); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingRight); + validateViewportMetricPadding(viewportMetricsCaptor, 100, 100, 100, 0); } @Test @@ -405,12 +388,7 @@ public void reportSystemInsetWhenNotFullscreenLegacy() { public void systemInsetHandlesFullscreenNavbarRight() { RuntimeEnvironment.setQualifiers("+land"); FlutterView flutterView = spy(new FlutterView(RuntimeEnvironment.systemContext)); - ShadowDisplay display = - Shadows.shadowOf( - ((WindowManager) - RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay()); - display.setRotation(1); + setExpectedDisplayRotation(Surface.ROTATION_90); assertEquals(0, flutterView.getSystemUiVisibility()); when(flutterView.getWindowSystemUiVisibility()) .thenReturn(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); @@ -431,24 +409,16 @@ public void systemInsetHandlesFullscreenNavbarRight() { // Then we simulate the system applying a window inset. WindowInsets windowInsets = mock(WindowInsets.class); - when(windowInsets.getSystemWindowInsetTop()).thenReturn(100); - when(windowInsets.getSystemWindowInsetBottom()).thenReturn(100); - when(windowInsets.getSystemWindowInsetLeft()).thenReturn(100); - when(windowInsets.getSystemWindowInsetRight()).thenReturn(100); - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - when(windowInsets.getSystemGestureInsets()).thenReturn(Insets.NONE); - } + mockSystemWindowInsets(windowInsets, 100, 100, 100, 100); + mockSystemGestureInsetsIfNeed(windowInsets); flutterView.onApplyWindowInsets(windowInsets); verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture()); // Top padding is removed due to full screen. - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingTop); - // Bottom padding is removed due to hide navigation. - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingLeft); // Right padding is zero because the rotation is 90deg - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingRight); + // Bottom padding is removed due to hide navigation. + validateViewportMetricPadding(viewportMetricsCaptor, 100, 0, 0, 0); } @Test @@ -456,12 +426,7 @@ public void systemInsetHandlesFullscreenNavbarRight() { public void systemInsetHandlesFullscreenNavbarRightBelowSDK23() { RuntimeEnvironment.setQualifiers("+land"); FlutterView flutterView = spy(new FlutterView(RuntimeEnvironment.systemContext)); - ShadowDisplay display = - Shadows.shadowOf( - ((WindowManager) - RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay()); - display.setRotation(3); + setExpectedDisplayRotation(Surface.ROTATION_270); assertEquals(0, flutterView.getSystemUiVisibility()); when(flutterView.getWindowSystemUiVisibility()) .thenReturn(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); @@ -482,24 +447,16 @@ public void systemInsetHandlesFullscreenNavbarRightBelowSDK23() { // Then we simulate the system applying a window inset. WindowInsets windowInsets = mock(WindowInsets.class); - when(windowInsets.getSystemWindowInsetTop()).thenReturn(100); - when(windowInsets.getSystemWindowInsetBottom()).thenReturn(100); - when(windowInsets.getSystemWindowInsetLeft()).thenReturn(100); - when(windowInsets.getSystemWindowInsetRight()).thenReturn(100); - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - when(windowInsets.getSystemGestureInsets()).thenReturn(Insets.NONE); - } + mockSystemWindowInsets(windowInsets, 100, 100, 100, 100); + mockSystemGestureInsetsIfNeed(windowInsets); flutterView.onApplyWindowInsets(windowInsets); verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture()); // Top padding is removed due to full screen. - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingTop); - // Bottom padding is removed due to hide navigation. - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom); // Right padding is zero because the rotation is 270deg under SDK 23 - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingRight); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingLeft); + // Bottom padding is removed due to hide navigation. + validateViewportMetricPadding(viewportMetricsCaptor, 100, 0, 0, 0); } @Test @@ -507,12 +464,7 @@ public void systemInsetHandlesFullscreenNavbarRightBelowSDK23() { public void systemInsetHandlesFullscreenNavbarLeft() { RuntimeEnvironment.setQualifiers("+land"); FlutterView flutterView = spy(new FlutterView(RuntimeEnvironment.systemContext)); - ShadowDisplay display = - Shadows.shadowOf( - ((WindowManager) - RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay()); - display.setRotation(3); + setExpectedDisplayRotation(Surface.ROTATION_270); assertEquals(0, flutterView.getSystemUiVisibility()); when(flutterView.getWindowSystemUiVisibility()) .thenReturn(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); @@ -533,24 +485,16 @@ public void systemInsetHandlesFullscreenNavbarLeft() { // Then we simulate the system applying a window inset. WindowInsets windowInsets = mock(WindowInsets.class); - when(windowInsets.getSystemWindowInsetTop()).thenReturn(100); - when(windowInsets.getSystemWindowInsetBottom()).thenReturn(100); - when(windowInsets.getSystemWindowInsetLeft()).thenReturn(100); - when(windowInsets.getSystemWindowInsetRight()).thenReturn(100); - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - when(windowInsets.getSystemGestureInsets()).thenReturn(Insets.NONE); - } + mockSystemWindowInsets(windowInsets, 100, 100, 100, 100); + mockSystemGestureInsetsIfNeed(windowInsets); flutterView.onApplyWindowInsets(windowInsets); verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture()); + // Left padding is zero because the rotation is 270deg // Top padding is removed due to full screen. - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingTop); // Bottom padding is removed due to hide navigation. - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom); - // Left padding is zero because the rotation is 270deg - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingLeft); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingRight); + validateViewportMetricPadding(viewportMetricsCaptor, 0, 0, 100, 0); } // This test uses the API 30+ Algorithm for window insets. The legacy algorithm is @@ -561,12 +505,7 @@ public void systemInsetHandlesFullscreenNavbarLeft() { public void systemInsetGetInsetsFullscreen() { RuntimeEnvironment.setQualifiers("+land"); FlutterView flutterView = spy(new FlutterView(RuntimeEnvironment.systemContext)); - ShadowDisplay display = - Shadows.shadowOf( - ((WindowManager) - RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay()); - display.setRotation(3); + setExpectedDisplayRotation(Surface.ROTATION_270); assertEquals(0, flutterView.getSystemUiVisibility()); when(flutterView.getWindowSystemUiVisibility()) .thenReturn(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); @@ -588,19 +527,13 @@ public void systemInsetGetInsetsFullscreen() { Insets insets = Insets.of(10, 20, 30, 40); // Then we simulate the system applying a window inset. WindowInsets windowInsets = mock(WindowInsets.class); - when(windowInsets.getSystemWindowInsetTop()).thenReturn(-1); - when(windowInsets.getSystemWindowInsetBottom()).thenReturn(-1); - when(windowInsets.getSystemWindowInsetLeft()).thenReturn(-1); - when(windowInsets.getSystemWindowInsetRight()).thenReturn(-1); + mockSystemWindowInsets(windowInsets, -1, -1, -1, -1); when(windowInsets.getInsets(anyInt())).thenReturn(insets); flutterView.onApplyWindowInsets(windowInsets); verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(10, viewportMetricsCaptor.getValue().viewPaddingLeft); - assertEquals(20, viewportMetricsCaptor.getValue().viewPaddingTop); - assertEquals(30, viewportMetricsCaptor.getValue().viewPaddingRight); - assertEquals(40, viewportMetricsCaptor.getValue().viewPaddingBottom); + validateViewportMetricPadding(viewportMetricsCaptor, 10, 20, 30, 40); } // This test uses the pre-API 30 Algorithm for window insets. @@ -610,12 +543,7 @@ public void systemInsetGetInsetsFullscreen() { public void systemInsetGetInsetsFullscreenLegacy() { RuntimeEnvironment.setQualifiers("+land"); FlutterView flutterView = spy(new FlutterView(RuntimeEnvironment.systemContext)); - ShadowDisplay display = - Shadows.shadowOf( - ((WindowManager) - RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay()); - display.setRotation(3); + setExpectedDisplayRotation(Surface.ROTATION_270); assertEquals(0, flutterView.getSystemUiVisibility()); when(flutterView.getWindowSystemUiVisibility()) .thenReturn(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); @@ -636,21 +564,15 @@ public void systemInsetGetInsetsFullscreenLegacy() { // Then we simulate the system applying a window inset. WindowInsets windowInsets = mock(WindowInsets.class); - when(windowInsets.getSystemWindowInsetTop()).thenReturn(100); - when(windowInsets.getSystemWindowInsetBottom()).thenReturn(101); - when(windowInsets.getSystemWindowInsetLeft()).thenReturn(102); - when(windowInsets.getSystemWindowInsetRight()).thenReturn(103); + mockSystemWindowInsets(windowInsets, 102, 100, 103, 101); flutterView.onApplyWindowInsets(windowInsets); verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture()); + // Left padding is zero because the rotation is 270deg // Top padding is removed due to full screen. - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingTop); // Bottom padding is removed due to hide navigation. - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom); - // Left padding is zero because the rotation is 270deg - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingLeft); - assertEquals(103, viewportMetricsCaptor.getValue().viewPaddingRight); + validateViewportMetricPadding(viewportMetricsCaptor, 0, 0, 103, 0); } // This test uses the API 30+ Algorithm for window insets. The legacy algorithm is @@ -661,11 +583,6 @@ public void systemInsetGetInsetsFullscreenLegacy() { public void systemInsetDisplayCutoutSimple() { RuntimeEnvironment.setQualifiers("+land"); FlutterView flutterView = spy(new FlutterView(RuntimeEnvironment.systemContext)); - ShadowDisplay display = - Shadows.shadowOf( - ((WindowManager) - RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay()); assertEquals(0, flutterView.getSystemUiVisibility()); when(flutterView.getWindowSystemUiVisibility()).thenReturn(0); when(flutterView.getContext()).thenReturn(RuntimeEnvironment.systemContext); @@ -688,10 +605,7 @@ public void systemInsetDisplayCutoutSimple() { // Then we simulate the system applying a window inset. WindowInsets windowInsets = mock(WindowInsets.class); DisplayCutout displayCutout = mock(DisplayCutout.class); - when(windowInsets.getSystemWindowInsetTop()).thenReturn(-1); - when(windowInsets.getSystemWindowInsetBottom()).thenReturn(-1); - when(windowInsets.getSystemWindowInsetLeft()).thenReturn(-1); - when(windowInsets.getSystemWindowInsetRight()).thenReturn(-1); + mockSystemWindowInsets(windowInsets, -1, -1, -1, -1); when(windowInsets.getInsets(anyInt())).thenReturn(insets); when(windowInsets.getSystemGestureInsets()).thenReturn(systemGestureInsets); when(windowInsets.getDisplayCutout()).thenReturn(displayCutout); @@ -706,10 +620,7 @@ public void systemInsetDisplayCutoutSimple() { flutterView.onApplyWindowInsets(windowInsets); verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(150, viewportMetricsCaptor.getValue().viewPaddingTop); - assertEquals(150, viewportMetricsCaptor.getValue().viewPaddingBottom); - assertEquals(200, viewportMetricsCaptor.getValue().viewPaddingLeft); - assertEquals(200, viewportMetricsCaptor.getValue().viewPaddingRight); + validateViewportMetricPadding(viewportMetricsCaptor, 200, 150, 200, 150); assertEquals(100, viewportMetricsCaptor.getValue().viewInsetTop); } @@ -913,6 +824,41 @@ public void ViewportMetrics_initializedPhysicalTouchSlop() { assertFalse(-1 == viewportMetricsCaptor.getValue().physicalTouchSlop); } + private void setExpectedDisplayRotation(int rotation) { + ShadowDisplay display = + Shadows.shadowOf( + ((WindowManager) + RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay()); + display.setRotation(rotation); + } + + private void validateViewportMetricPadding( + ArgumentCaptor viewportMetricsCaptor, + int left, + int top, + int right, + int bottom) { + assertEquals(left, viewportMetricsCaptor.getValue().viewPaddingLeft); + assertEquals(top, viewportMetricsCaptor.getValue().viewPaddingTop); + assertEquals(right, viewportMetricsCaptor.getValue().viewPaddingRight); + assertEquals(bottom, viewportMetricsCaptor.getValue().viewPaddingBottom); + } + + private void mockSystemWindowInsets( + WindowInsets windowInsets, int left, int top, int right, int bottom) { + when(windowInsets.getSystemWindowInsetLeft()).thenReturn(left); + when(windowInsets.getSystemWindowInsetTop()).thenReturn(top); + when(windowInsets.getSystemWindowInsetRight()).thenReturn(right); + when(windowInsets.getSystemWindowInsetBottom()).thenReturn(bottom); + } + + private void mockSystemGestureInsetsIfNeed(WindowInsets windowInsets) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + when(windowInsets.getSystemGestureInsets()).thenReturn(Insets.NONE); + } + } + /* * A custom shadow that reports fullscreen flag for system UI visibility */ diff --git a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java index e836560e009c9..aa6ffb00ab75e 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java @@ -24,6 +24,8 @@ import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.embedding.engine.systemchannels.NavigationChannel; import io.flutter.plugins.GeneratedPluginRegistrant; +import java.util.ArrayList; +import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -121,7 +123,11 @@ public void canSpawnMoreEngines() { doReturn(mock(FlutterEngine.class)) .when(firstEngine) - .spawn(any(Context.class), any(DartEntrypoint.class), nullable(String.class)); + .spawn( + any(Context.class), + any(DartEntrypoint.class), + nullable(String.class), + nullable(List.class)); FlutterEngine secondEngine = engineGroupUnderTest.createAndRunEngine( @@ -133,7 +139,11 @@ public void canSpawnMoreEngines() { // Now the second spawned engine is the only one left and it will be called to spawn the next // engine in the chain. - when(secondEngine.spawn(any(Context.class), any(DartEntrypoint.class), nullable(String.class))) + when(secondEngine.spawn( + any(Context.class), + any(DartEntrypoint.class), + nullable(String.class), + nullable(List.class))) .thenReturn(mock(FlutterEngine.class)); FlutterEngine thirdEngine = @@ -156,7 +166,8 @@ public void canCreateAndRunCustomEntrypoints() { eq("some/path/to/flutter_assets"), eq("other entrypoint"), isNull(String.class), - any(AssetManager.class)); + any(AssetManager.class), + nullable(List.class)); } @Test @@ -176,7 +187,11 @@ public void canCreateAndRunWithCustomInitialRoute() { doAnswer(invocation -> jniAttached = true).when(secondMockflutterJNI).attachToNative(); doReturn(secondMockflutterJNI) .when(mockflutterJNI) - .spawn(nullable(String.class), nullable(String.class), nullable(String.class)); + .spawn( + nullable(String.class), + nullable(String.class), + nullable(String.class), + nullable(List.class)); FlutterEngine secondEngine = engineGroupUnderTest.createAndRunEngine( @@ -184,6 +199,51 @@ public void canCreateAndRunWithCustomInitialRoute() { assertEquals(2, engineGroupUnderTest.activeEngines.size()); verify(mockflutterJNI, times(1)) - .spawn(nullable(String.class), nullable(String.class), eq("/bar")); + .spawn(nullable(String.class), nullable(String.class), eq("/bar"), nullable(List.class)); + } + + @Test + public void canCreateAndRunWithCustomEntrypointArgs() { + List firstDartEntrypointArgs = new ArrayList(); + FlutterEngine firstEngine = + engineGroupUnderTest.createAndRunEngine( + new FlutterEngineGroup.Options(RuntimeEnvironment.application) + .setDartEntrypoint(mock(DartEntrypoint.class)) + .setDartEntrypointArgs(firstDartEntrypointArgs)); + assertEquals(1, engineGroupUnderTest.activeEngines.size()); + verify(mockflutterJNI, times(1)) + .runBundleAndSnapshotFromLibrary( + nullable(String.class), + nullable(String.class), + isNull(String.class), + any(AssetManager.class), + eq(firstDartEntrypointArgs)); + + when(mockflutterJNI.isAttached()).thenReturn(true); + jniAttached = false; + FlutterJNI secondMockflutterJNI = mock(FlutterJNI.class); + when(secondMockflutterJNI.isAttached()).thenAnswer(invocation -> jniAttached); + doAnswer(invocation -> jniAttached = true).when(secondMockflutterJNI).attachToNative(); + doReturn(secondMockflutterJNI) + .when(mockflutterJNI) + .spawn( + nullable(String.class), + nullable(String.class), + nullable(String.class), + nullable(List.class)); + List secondDartEntrypointArgs = new ArrayList(); + FlutterEngine secondEngine = + engineGroupUnderTest.createAndRunEngine( + new FlutterEngineGroup.Options(RuntimeEnvironment.application) + .setDartEntrypoint(mock(DartEntrypoint.class)) + .setDartEntrypointArgs(secondDartEntrypointArgs)); + + assertEquals(2, engineGroupUnderTest.activeEngines.size()); + verify(mockflutterJNI, times(1)) + .spawn( + nullable(String.class), + nullable(String.class), + nullable(String.class), + eq(secondDartEntrypointArgs)); } } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java b/shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java index 5e1922103afbf..1585aaa0b6d70 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java @@ -1,20 +1,32 @@ package io.flutter.embedding.engine.dart; +import static android.os.Looper.getMainLooper; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertArrayEquals; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.robolectric.Shadows.shadowOf; import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.dart.DartMessenger.DartMessengerTaskQueue; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -22,6 +34,8 @@ @Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) public class DartMessengerTest { + SynchronousTaskQueue synchronousTaskQueue = new SynchronousTaskQueue(); + private static class ReportingUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { public Throwable latestException; @@ -32,6 +46,12 @@ public void uncaughtException(Thread t, Throwable e) { } } + private static class SynchronousTaskQueue implements DartMessengerTaskQueue { + public void dispatch(Runnable runnable) { + runnable.run(); + } + } + @Test public void itHandlesErrors() { // Setup test. @@ -44,14 +64,15 @@ public void itHandlesErrors() { currentThread.setUncaughtExceptionHandler(reportingHandler); // Create object under test. - final DartMessenger messenger = new DartMessenger(fakeFlutterJni); + final DartMessenger messenger = + new DartMessenger(fakeFlutterJni, (options) -> synchronousTaskQueue); final BinaryMessageHandler throwingHandler = mock(BinaryMessageHandler.class); Mockito.doThrow(AssertionError.class) .when(throwingHandler) .onMessage(any(ByteBuffer.class), any(DartMessenger.Reply.class)); - - messenger.setMessageHandler("test", throwingHandler); - messenger.handleMessageFromDart("test", ByteBuffer.allocate(0), 0); + BinaryMessenger.TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + messenger.setMessageHandler("test", throwingHandler, taskQueue); + messenger.handleMessageFromDart("test", ByteBuffer.allocate(0), 0, 0); assertNotNull(reportingHandler.latestException); assertTrue(reportingHandler.latestException instanceof AssertionError); currentThread.setUncaughtExceptionHandler(savedHandler); @@ -61,21 +82,23 @@ public void itHandlesErrors() { public void givesDirectByteBuffer() { // Setup test. final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); - final DartMessenger messenger = new DartMessenger(fakeFlutterJni); + final DartMessenger messenger = + new DartMessenger(fakeFlutterJni, (options) -> synchronousTaskQueue); final String channel = "foobar"; final boolean[] wasDirect = {false}; final BinaryMessenger.BinaryMessageHandler handler = (message, reply) -> { wasDirect[0] = message.isDirect(); }; - messenger.setMessageHandler(channel, handler); + BinaryMessenger.TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + messenger.setMessageHandler(channel, handler, taskQueue); final ByteBuffer message = ByteBuffer.allocateDirect(4 * 2); message.rewind(); message.putChar('a'); message.putChar('b'); message.putChar('c'); message.putChar('d'); - messenger.handleMessageFromDart(channel, message, /*replyId=*/ 123); + messenger.handleMessageFromDart(channel, message, /*replyId=*/ 123, 0); assertTrue(wasDirect[0]); } @@ -83,7 +106,8 @@ public void givesDirectByteBuffer() { public void directByteBufferLimitZeroAfterUsage() { // Setup test. final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); - final DartMessenger messenger = new DartMessenger(fakeFlutterJni); + final DartMessenger messenger = + new DartMessenger(fakeFlutterJni, (options) -> synchronousTaskQueue); final String channel = "foobar"; final ByteBuffer[] byteBuffers = {null}; final int bufferSize = 4 * 2; @@ -92,14 +116,15 @@ public void directByteBufferLimitZeroAfterUsage() { byteBuffers[0] = message; assertEquals(bufferSize, byteBuffers[0].limit()); }; - messenger.setMessageHandler(channel, handler); + BinaryMessenger.TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + messenger.setMessageHandler(channel, handler, taskQueue); final ByteBuffer message = ByteBuffer.allocateDirect(bufferSize); message.rewind(); message.putChar('a'); message.putChar('b'); message.putChar('c'); message.putChar('d'); - messenger.handleMessageFromDart(channel, message, /*replyId=*/ 123); + messenger.handleMessageFromDart(channel, message, /*replyId=*/ 123, 0); assertNotNull(byteBuffers[0]); assertTrue(byteBuffers[0].isDirect()); assertEquals(0, byteBuffers[0].limit()); @@ -139,4 +164,192 @@ public void replyIdIncrementsOnNullReply() { messenger.send(channel, null, null); verify(fakeFlutterJni, times(1)).dispatchEmptyPlatformMessage(eq("foobar"), eq(2)); } + + @Test + public void cleansUpMessageData() throws InterruptedException { + final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); + final DartMessenger messenger = + new DartMessenger(fakeFlutterJni, (options) -> synchronousTaskQueue); + BinaryMessenger.TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + String channel = "foobar"; + BinaryMessenger.BinaryMessageHandler handler = + (ByteBuffer message, BinaryMessenger.BinaryReply reply) -> { + reply.reply(null); + }; + messenger.setMessageHandler(channel, handler, taskQueue); + final ByteBuffer message = ByteBuffer.allocateDirect(4 * 2); + final int replyId = 1; + final long messageData = 1234; + messenger.handleMessageFromDart(channel, message, replyId, messageData); + verify(fakeFlutterJni).cleanupMessageData(eq(messageData)); + } + + @Test + public void cleansUpMessageDataOnError() throws InterruptedException { + final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); + final DartMessenger messenger = + new DartMessenger(fakeFlutterJni, (options) -> synchronousTaskQueue); + BinaryMessenger.TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + String channel = "foobar"; + BinaryMessenger.BinaryMessageHandler handler = + (ByteBuffer message, BinaryMessenger.BinaryReply reply) -> { + throw new RuntimeException("hello"); + }; + messenger.setMessageHandler(channel, handler, taskQueue); + final ByteBuffer message = ByteBuffer.allocateDirect(4 * 2); + final int replyId = 1; + final long messageData = 1234; + + messenger.handleMessageFromDart(channel, message, replyId, messageData); + verify(fakeFlutterJni).cleanupMessageData(eq(messageData)); + } + + @Test + public void emptyResponseWhenHandlerIsNotSet() throws InterruptedException { + final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); + final DartMessenger messenger = + new DartMessenger(fakeFlutterJni, (options) -> synchronousTaskQueue); + final String channel = "foobar"; + final ByteBuffer message = ByteBuffer.allocateDirect(4 * 2); + final int replyId = 1; + final long messageData = 1234; + + messenger.handleMessageFromDart(channel, message, replyId, messageData); + shadowOf(getMainLooper()).idle(); + verify(fakeFlutterJni).invokePlatformMessageEmptyResponseCallback(replyId); + } + + @Test + public void buffersResponseWhenHandlerIsNotSet() throws InterruptedException { + final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); + final DartMessenger messenger = + new DartMessenger(fakeFlutterJni, (options) -> synchronousTaskQueue); + final BinaryMessenger.TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + final String channel = "foobar"; + final ByteBuffer message = ByteBuffer.allocateDirect(4 * 2); + final int replyId = 1; + final long messageData = 1234; + + messenger.enableBufferingIncomingMessages(); + messenger.handleMessageFromDart(channel, message, replyId, messageData); + + shadowOf(getMainLooper()).idle(); + verify(fakeFlutterJni, never()).invokePlatformMessageEmptyResponseCallback(eq(replyId)); + + final BinaryMessenger.BinaryMessageHandler handler = + (ByteBuffer msg, BinaryMessenger.BinaryReply reply) -> { + reply.reply(ByteBuffer.wrap("done".getBytes())); + }; + messenger.setMessageHandler(channel, handler, taskQueue); + + shadowOf(getMainLooper()).idle(); + verify(fakeFlutterJni, never()).invokePlatformMessageEmptyResponseCallback(eq(replyId)); + + final ArgumentCaptor response = ArgumentCaptor.forClass(ByteBuffer.class); + verify(fakeFlutterJni) + .invokePlatformMessageResponseCallback(anyInt(), response.capture(), anyInt()); + assertArrayEquals("done".getBytes(), response.getValue().array()); + } + + @Test + public void disableBufferingTriggersEmptyResponseForPendingMessages() + throws InterruptedException { + final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); + final DartMessenger messenger = + new DartMessenger(fakeFlutterJni, (options) -> synchronousTaskQueue); + final String channel = "foobar"; + final ByteBuffer message = ByteBuffer.allocateDirect(4 * 2); + final int replyId = 1; + final long messageData = 1234; + + messenger.enableBufferingIncomingMessages(); + messenger.handleMessageFromDart(channel, message, replyId, messageData); + shadowOf(getMainLooper()).idle(); + verify(fakeFlutterJni, never()).invokePlatformMessageEmptyResponseCallback(replyId); + + messenger.disableBufferingIncomingMessages(); + shadowOf(getMainLooper()).idle(); + verify(fakeFlutterJni).invokePlatformMessageEmptyResponseCallback(replyId); + } + + @Test + public void emptyResponseWhenHandlerIsUnregistered() throws InterruptedException { + final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); + final DartMessenger messenger = + new DartMessenger(fakeFlutterJni, (options) -> synchronousTaskQueue); + final BinaryMessenger.TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + final String channel = "foobar"; + final ByteBuffer message = ByteBuffer.allocateDirect(4 * 2); + final int replyId = 1; + final long messageData = 1234; + + messenger.enableBufferingIncomingMessages(); + messenger.handleMessageFromDart(channel, message, replyId, messageData); + + shadowOf(getMainLooper()).idle(); + verify(fakeFlutterJni, never()).invokePlatformMessageEmptyResponseCallback(eq(replyId)); + + final BinaryMessenger.BinaryMessageHandler handler = + (ByteBuffer msg, BinaryMessenger.BinaryReply reply) -> { + reply.reply(ByteBuffer.wrap("done".getBytes())); + }; + messenger.setMessageHandler(channel, handler, taskQueue); + + shadowOf(getMainLooper()).idle(); + verify(fakeFlutterJni, never()).invokePlatformMessageEmptyResponseCallback(eq(replyId)); + + final ArgumentCaptor response = ArgumentCaptor.forClass(ByteBuffer.class); + verify(fakeFlutterJni) + .invokePlatformMessageResponseCallback(anyInt(), response.capture(), anyInt()); + assertArrayEquals("done".getBytes(), response.getValue().array()); + + messenger.disableBufferingIncomingMessages(); + messenger.setMessageHandler(channel, null, null); // Unregister handler. + + messenger.handleMessageFromDart(channel, message, replyId, messageData); + shadowOf(getMainLooper()).idle(); + verify(fakeFlutterJni).invokePlatformMessageEmptyResponseCallback(replyId); + } + + @Test + public void testSerialTaskQueue() throws InterruptedException { + final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class); + final DartMessenger messenger = new DartMessenger(fakeFlutterJni); + final ExecutorService taskQueuePool = Executors.newFixedThreadPool(4); + final DartMessengerTaskQueue taskQueue = new DartMessenger.SerialTaskQueue(taskQueuePool); + final int count = 5000; + final LinkedList ints = new LinkedList<>(); + Random rand = new Random(); + for (int i = 0; i < count; ++i) { + final int value = i; + taskQueue.dispatch( + () -> { + try { + Thread.sleep(rand.nextInt(10)); + } catch (InterruptedException ex) { + System.out.println(ex.toString()); + } + ints.add(value); + }); + taskQueuePool.execute( + () -> { + // Add some extra noise to make sure we aren't always handling on the same thread. + try { + Thread.sleep(rand.nextInt(10)); + } catch (InterruptedException ex) { + System.out.println(ex.toString()); + } + }); + } + CountDownLatch latch = new CountDownLatch(1); + taskQueue.dispatch( + () -> { + latch.countDown(); + }); + latch.await(); + assertEquals(count, ints.size()); + for (int i = 0; i < count - 1; ++i) { + assertEquals((int) ints.get(i), (int) (ints.get(i + 1)) - 1); + } + } } diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java index 83b76bcf7f690..3800ddfbff98e 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -1,5 +1,7 @@ package io.flutter.plugin.platform; +import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; +import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -20,6 +22,7 @@ import android.os.Build; import android.view.View; import android.view.Window; +import android.view.WindowInsetsController; import androidx.activity.OnBackPressedCallback; import androidx.fragment.app.FragmentActivity; import io.flutter.embedding.engine.systemchannels.PlatformChannel; @@ -216,6 +219,98 @@ public void setNavigationBarDividerColor() { } } + @Config(sdk = 30) + @Test + public void setNavigationBarIconBrightness() { + if (Build.VERSION.SDK_INT >= 30) { + View fakeDecorView = mock(View.class); + WindowInsetsController fakeWindowInsetsController = mock(WindowInsetsController.class); + Window fakeWindow = mock(Window.class); + when(fakeWindow.getDecorView()).thenReturn(fakeDecorView); + when(fakeWindow.getInsetsController()).thenReturn(fakeWindowInsetsController); + Activity fakeActivity = mock(Activity.class); + when(fakeActivity.getWindow()).thenReturn(fakeWindow); + PlatformChannel fakePlatformChannel = mock(PlatformChannel.class); + PlatformPlugin platformPlugin = new PlatformPlugin(fakeActivity, fakePlatformChannel); + + SystemChromeStyle style = + new SystemChromeStyle( + null, // statusBarColor + null, // statusBarIconBrightness + null, // systemStatusBarContrastEnforced + null, // systemNavigationBarColor + Brightness.LIGHT, // systemNavigationBarIconBrightness + null, // systemNavigationBarDividerColor + null); // systemNavigationBarContrastEnforced + + platformPlugin.mPlatformMessageHandler.setSystemUiOverlayStyle(style); + + verify(fakeWindowInsetsController) + .setSystemBarsAppearance(0, APPEARANCE_LIGHT_NAVIGATION_BARS); + + style = + new SystemChromeStyle( + null, // statusBarColor + null, // statusBarIconBrightness + null, // systemStatusBarContrastEnforced + null, // systemNavigationBarColor + Brightness.DARK, // systemNavigationBarIconBrightness + null, // systemNavigationBarDividerColor + null); // systemNavigationBarContrastEnforced + + platformPlugin.mPlatformMessageHandler.setSystemUiOverlayStyle(style); + + verify(fakeWindowInsetsController) + .setSystemBarsAppearance( + APPEARANCE_LIGHT_NAVIGATION_BARS, APPEARANCE_LIGHT_NAVIGATION_BARS); + } + } + + @Config(sdk = 30) + @Test + public void setStatusBarIconBrightness() { + if (Build.VERSION.SDK_INT >= 30) { + View fakeDecorView = mock(View.class); + WindowInsetsController fakeWindowInsetsController = mock(WindowInsetsController.class); + Window fakeWindow = mock(Window.class); + when(fakeWindow.getDecorView()).thenReturn(fakeDecorView); + when(fakeWindow.getInsetsController()).thenReturn(fakeWindowInsetsController); + Activity fakeActivity = mock(Activity.class); + when(fakeActivity.getWindow()).thenReturn(fakeWindow); + PlatformChannel fakePlatformChannel = mock(PlatformChannel.class); + PlatformPlugin platformPlugin = new PlatformPlugin(fakeActivity, fakePlatformChannel); + + SystemChromeStyle style = + new SystemChromeStyle( + null, // statusBarColor + Brightness.LIGHT, // statusBarIconBrightness + null, // systemStatusBarContrastEnforced + null, // systemNavigationBarColor + null, // systemNavigationBarIconBrightness + null, // systemNavigationBarDividerColor + null); // systemNavigationBarContrastEnforced + + platformPlugin.mPlatformMessageHandler.setSystemUiOverlayStyle(style); + + verify(fakeWindowInsetsController).setSystemBarsAppearance(0, APPEARANCE_LIGHT_STATUS_BARS); + + style = + new SystemChromeStyle( + null, // statusBarColor + Brightness.DARK, // statusBarIconBrightness + null, // systemStatusBarContrastEnforced + null, // systemNavigationBarColor + null, // systemNavigationBarIconBrightness + null, // systemNavigationBarDividerColor + null); // systemNavigationBarContrastEnforced + + platformPlugin.mPlatformMessageHandler.setSystemUiOverlayStyle(style); + + verify(fakeWindowInsetsController) + .setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS); + } + } + @Config(sdk = 29) @Test public void setSystemUiMode() { @@ -272,6 +367,26 @@ public void setSystemUiMode() { } } + @Config(sdk = 28) + @Test + public void doNotEnableEdgeToEdgeOnOlderSdk() { + View fakeDecorView = mock(View.class); + Window fakeWindow = mock(Window.class); + when(fakeWindow.getDecorView()).thenReturn(fakeDecorView); + Activity fakeActivity = mock(Activity.class); + when(fakeActivity.getWindow()).thenReturn(fakeWindow); + PlatformChannel fakePlatformChannel = mock(PlatformChannel.class); + PlatformPlugin platformPlugin = new PlatformPlugin(fakeActivity, fakePlatformChannel); + + platformPlugin.mPlatformMessageHandler.showSystemUiMode( + PlatformChannel.SystemUiMode.EDGE_TO_EDGE); + verify(fakeDecorView, never()) + .setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + @Test public void popSystemNavigatorFlutterActivity() { Activity mockActivity = mock(Activity.class); diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 21c392879d688..38c919cae33dc 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -218,7 +218,7 @@ public void itUsesActionEventTypeFromMotionEventForHybridPlatformViews() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void getPlatformViewById__hybridComposition() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -246,7 +246,7 @@ public void getPlatformViewById__hybridComposition() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void createPlatformViewMessage__initializesAndroidView() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -268,7 +268,7 @@ public void createPlatformViewMessage__initializesAndroidView() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void createPlatformViewMessage__throwsIfViewIsNull() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -296,7 +296,7 @@ public void createPlatformViewMessage__throwsIfViewIsNull() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void onDetachedFromJNI_clearsPlatformViewContext() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -326,7 +326,7 @@ public void onDetachedFromJNI_clearsPlatformViewContext() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void onPreEngineRestart_clearsPlatformViewContext() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -356,7 +356,7 @@ public void onPreEngineRestart_clearsPlatformViewContext() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void createPlatformViewMessage__throwsIfViewHasParent() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -386,7 +386,7 @@ public void createPlatformViewMessage__throwsIfViewHasParent() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void disposeAndroidView__hybridComposition() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -427,7 +427,12 @@ public void disposeAndroidView__hybridComposition() { } @Test - @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) + @Config( + shadows = { + ShadowFlutterSurfaceView.class, + ShadowFlutterJNI.class, + ShadowPlatformTaskQueue.class + }) public void onEndFrame__destroysOverlaySurfaceAfterFrameOnFlutterSurfaceView() { final PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -525,7 +530,12 @@ public void onEndFrame__removesPlatformView() { } @Test - @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) + @Config( + shadows = { + ShadowFlutterSurfaceView.class, + ShadowFlutterJNI.class, + ShadowPlatformTaskQueue.class + }) public void onEndFrame__removesPlatformViewParent() { final PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -563,7 +573,12 @@ public void onEndFrame__removesPlatformViewParent() { } @Test - @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) + @Config( + shadows = { + ShadowFlutterSurfaceView.class, + ShadowFlutterJNI.class, + ShadowPlatformTaskQueue.class + }) public void detach__destroysOverlaySurfaces() { final PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -695,7 +710,7 @@ public void checkInputConnectionProxy__falseIfViewIsNull() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void convertPlatformViewRenderSurfaceAsDefault() { final PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -741,7 +756,7 @@ public void convertPlatformViewRenderSurfaceAsDefault() { } @Test - @Config(shadows = {ShadowFlutterJNI.class}) + @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void dontConverRenderSurfaceWhenFlagIsTrue() { final PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -812,7 +827,10 @@ private static void createPlatformView( new MethodCall("create", platformViewCreateArguments); jni.handlePlatformMessage( - "flutter/platform_views", encodeMethodCall(platformCreateMethodCall), /*replyId=*/ 0); + "flutter/platform_views", + encodeMethodCall(platformCreateMethodCall), + /*replyId=*/ 0, + /*messageData=*/ 0); } private static void disposePlatformView( @@ -826,7 +844,10 @@ private static void disposePlatformView( new MethodCall("dispose", platformViewDisposeArguments); jni.handlePlatformMessage( - "flutter/platform_views", encodeMethodCall(platformDisposeMethodCall), /*replyId=*/ 0); + "flutter/platform_views", + encodeMethodCall(platformDisposeMethodCall), + /*replyId=*/ 0, + /*messageData=*/ 0); } private static void synchronizeToNativeViewHierarchy( @@ -835,7 +856,10 @@ private static void synchronizeToNativeViewHierarchy( final MethodCall convertMethodCall = new MethodCall("synchronizeToNativeViewHierarchy", yes); jni.handlePlatformMessage( - "flutter/platform_views", encodeMethodCall(convertMethodCall), /*replyId=*/ 0); + "flutter/platform_views", + encodeMethodCall(convertMethodCall), + /*replyId=*/ 0, + /*messageData=*/ 0); } private static FlutterView attach( @@ -901,6 +925,20 @@ public FlutterImageView createImageView() { return view; } + /** + * For convenience when writing tests, this allows us to make fake messages from Flutter via + * Platform Channels. Typically those calls happen on the ui thread which dispatches to the + * platform thread. Since tests run on the platform thread it makes it difficult to test without + * this, but isn't technically required. + */ + @Implements(io.flutter.embedding.engine.dart.PlatformTaskQueue.class) + public static class ShadowPlatformTaskQueue { + @Implementation + public void dispatch(Runnable runnable) { + runnable.run(); + } + } + @Implements(FlutterJNI.class) public static class ShadowFlutterJNI { private static SparseArray replies = new SparseArray<>(); diff --git a/shell/platform/android/test/io/flutter/view/VsyncWaiterTest.java b/shell/platform/android/test/io/flutter/view/VsyncWaiterTest.java new file mode 100644 index 0000000000000..7b08967b31c9d --- /dev/null +++ b/shell/platform/android/test/io/flutter/view/VsyncWaiterTest.java @@ -0,0 +1,103 @@ +// 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. + +package io.flutter.view; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +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 static org.robolectric.Shadows.shadowOf; + +import android.annotation.TargetApi; +import android.hardware.display.DisplayManager; +import android.os.Looper; +import android.view.Display; +import io.flutter.embedding.engine.FlutterJNI; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class VsyncWaiterTest { + @Before + public void setUp() { + VsyncWaiter.reset(); + } + + @Test + public void itSetsFpsBelowApi17() { + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + VsyncWaiter waiter = VsyncWaiter.getInstance(10.0f, mockFlutterJNI); + verify(mockFlutterJNI, times(1)).setRefreshRateFPS(10.0f); + + waiter.init(); + + ArgumentCaptor delegateCaptor = + ArgumentCaptor.forClass(FlutterJNI.AsyncWaitForVsyncDelegate.class); + verify(mockFlutterJNI, times(1)).setAsyncWaitForVsyncDelegate(delegateCaptor.capture()); + delegateCaptor.getValue().asyncWaitForVsync(1); + shadowOf(Looper.getMainLooper()).idle(); + verify(mockFlutterJNI, times(1)).nativeOnVsync(anyLong(), eq(1000000000l / 10l), eq(1l)); + } + + @TargetApi(17) + @Test + public void itSetsFpsWhenDisplayManagerUpdates() { + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DisplayManager mockDisplayManager = mock(DisplayManager.class); + Display mockDisplay = mock(Display.class); + ArgumentCaptor displayListenerCaptor = + ArgumentCaptor.forClass(VsyncWaiter.DisplayListener.class); + when(mockDisplayManager.getDisplay(Display.DEFAULT_DISPLAY)).thenReturn(mockDisplay); + + VsyncWaiter waiter = VsyncWaiter.getInstance(mockDisplayManager, mockFlutterJNI); + verify(mockDisplayManager, times(1)) + .registerDisplayListener(displayListenerCaptor.capture(), isNull()); + + when(mockDisplay.getRefreshRate()).thenReturn(90.0f); + displayListenerCaptor.getValue().onDisplayChanged(Display.DEFAULT_DISPLAY); + verify(mockFlutterJNI, times(1)).setRefreshRateFPS(90.0f); + + waiter.init(); + + ArgumentCaptor delegateCaptor = + ArgumentCaptor.forClass(FlutterJNI.AsyncWaitForVsyncDelegate.class); + verify(mockFlutterJNI, times(1)).setAsyncWaitForVsyncDelegate(delegateCaptor.capture()); + delegateCaptor.getValue().asyncWaitForVsync(1); + shadowOf(Looper.getMainLooper()).idle(); + verify(mockFlutterJNI, times(1)).nativeOnVsync(anyLong(), eq(1000000000l / 90l), eq(1l)); + + when(mockDisplay.getRefreshRate()).thenReturn(60.0f); + displayListenerCaptor.getValue().onDisplayChanged(Display.DEFAULT_DISPLAY); + verify(mockFlutterJNI, times(1)).setRefreshRateFPS(60.0f); + + delegateCaptor.getValue().asyncWaitForVsync(1); + shadowOf(Looper.getMainLooper()).idle(); + verify(mockFlutterJNI, times(1)).nativeOnVsync(anyLong(), eq(1000000000l / 60l), eq(1l)); + } + + @TargetApi(17) + @Test + public void itSetsFpsWhenDisplayManagerDoesNotUpdate() { + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DisplayManager mockDisplayManager = mock(DisplayManager.class); + Display mockDisplay = mock(Display.class); + when(mockDisplayManager.getDisplay(Display.DEFAULT_DISPLAY)).thenReturn(mockDisplay); + when(mockDisplay.getRefreshRate()).thenReturn(90.0f); + + VsyncWaiter waiter = VsyncWaiter.getInstance(mockDisplayManager, mockFlutterJNI); + verify(mockDisplayManager, times(1)).registerDisplayListener(any(), isNull()); + + verify(mockFlutterJNI, times(1)).setRefreshRateFPS(90.0f); + } +} diff --git a/shell/platform/android/test_runner/build.gradle b/shell/platform/android/test_runner/build.gradle index 1cd44364bb34b..688014acbd283 100644 --- a/shell/platform/android/test_runner/build.gradle +++ b/shell/platform/android/test_runner/build.gradle @@ -37,10 +37,12 @@ android { dependencies { testImplementation files(project.property("flutter_jar")) testImplementation "androidx.annotation:annotation:1.1.0" + testImplementation "androidx.core:core:1.6.0" testImplementation "androidx.fragment:fragment:1.1.0" testImplementation "androidx.lifecycle:lifecycle-runtime:2.2.0" testImplementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" testImplementation "androidx.test:core:1.4.0" + testImplementation "androidx.tracing:tracing:1.0.0" testImplementation "com.google.android.play:core:1.8.0" testImplementation "com.ibm.icu:icu4j:69.1" testImplementation "org.mockito:mockito-core:3.11.2" diff --git a/shell/platform/common/BUILD.gn b/shell/platform/common/BUILD.gn index 8b3cf4c25a597..013e73a6462bd 100644 --- a/shell/platform/common/BUILD.gn +++ b/shell/platform/common/BUILD.gn @@ -39,11 +39,15 @@ copy("publish_headers") { source_set("common_cpp_input") { public = [ + "text_editing_delta.h", "text_input_model.h", "text_range.h", ] - sources = [ "text_input_model.cc" ] + sources = [ + "text_editing_delta.cc", + "text_input_model.cc", + ] configs += [ ":desktop_library_implementation" ] @@ -167,6 +171,7 @@ if (enable_unittests) { "geometry_unittests.cc", "json_message_codec_unittests.cc", "json_method_codec_unittests.cc", + "text_editing_delta_unittests.cc", "text_input_model_unittests.cc", "text_range_unittests.cc", ] diff --git a/shell/platform/common/client_wrapper/include/flutter/encodable_value.h b/shell/platform/common/client_wrapper/include/flutter/encodable_value.h index dec1bdfc4b8a6..3b46f99f63c33 100644 --- a/shell/platform/common/client_wrapper/include/flutter/encodable_value.h +++ b/shell/platform/common/client_wrapper/include/flutter/encodable_value.h @@ -63,7 +63,9 @@ class CustomEncodableValue { ~CustomEncodableValue() = default; // Allow implicit conversion to std::any to allow direct use of any_cast. + // NOLINTNEXTLINE(google-explicit-constructor) operator std::any&() { return value_; } + // NOLINTNEXTLINE(google-explicit-constructor) operator const std::any&() const { return value_; } #if defined(FLUTTER_ENABLE_RTTI) && FLUTTER_ENABLE_RTTI @@ -182,6 +184,7 @@ class EncodableValue : public internal::EncodableValueVariant { // to use it with EncodableValue, so the risk of unintended conversions is // minimal, and it avoids the need for the verbose: // EncodableValue(CustomEncodableValue(...)). + // NOLINTNEXTLINE(google-explicit-constructor) EncodableValue(const CustomEncodableValue& v) : super(v) {} // Override the conversion constructors from std::variant to make them diff --git a/shell/platform/common/client_wrapper/include/flutter/engine_method_result.h b/shell/platform/common/client_wrapper/include/flutter/engine_method_result.h index c29ff09b77592..3cc8b6a22c35b 100644 --- a/shell/platform/common/client_wrapper/include/flutter/engine_method_result.h +++ b/shell/platform/common/client_wrapper/include/flutter/engine_method_result.h @@ -21,7 +21,7 @@ namespace internal { // vary based on the template type. class ReplyManager { public: - ReplyManager(BinaryReply reply_handler_); + explicit ReplyManager(BinaryReply reply_handler_); ~ReplyManager(); // Prevent copying. diff --git a/shell/platform/common/client_wrapper/include/flutter/texture_registrar.h b/shell/platform/common/client_wrapper/include/flutter/texture_registrar.h index 5b59829f24283..6aa2c6b1d818f 100644 --- a/shell/platform/common/client_wrapper/include/flutter/texture_registrar.h +++ b/shell/platform/common/client_wrapper/include/flutter/texture_registrar.h @@ -27,7 +27,7 @@ class PixelBufferTexture { // As the callback is usually invoked from the render thread, the callee must // take care of proper synchronization. It also needs to be ensured that the // returned buffer isn't released prior to unregistering this texture. - PixelBufferTexture(CopyBufferCallback copy_buffer_callback) + explicit PixelBufferTexture(CopyBufferCallback copy_buffer_callback) : copy_buffer_callback_(copy_buffer_callback) {} // Returns the callback-provided FlutterDesktopPixelBuffer that contains the diff --git a/shell/platform/common/client_wrapper/standard_codec.cc b/shell/platform/common/client_wrapper/standard_codec.cc index f16a0f9792ac2..807e06816bee3 100644 --- a/shell/platform/common/client_wrapper/standard_codec.cc +++ b/shell/platform/common/client_wrapper/standard_codec.cc @@ -231,11 +231,11 @@ size_t StandardCodecSerializer::ReadSize(ByteStreamReader* stream) const { if (byte < 254) { return byte; } else if (byte == 254) { - uint16_t value; + uint16_t value = 0; stream->ReadBytes(reinterpret_cast(&value), 2); return value; } else { - uint32_t value; + uint32_t value = 0; stream->ReadBytes(reinterpret_cast(&value), 4); return value; } diff --git a/shell/platform/common/client_wrapper/testing/stub_flutter_api.h b/shell/platform/common/client_wrapper/testing/stub_flutter_api.h index 71edec1bc1dcf..6d40719fd4eb0 100644 --- a/shell/platform/common/client_wrapper/testing/stub_flutter_api.h +++ b/shell/platform/common/client_wrapper/testing/stub_flutter_api.h @@ -87,7 +87,7 @@ class StubFlutterApi { class ScopedStubFlutterApi { public: // Calls SetTestFlutterStub with |stub|. - ScopedStubFlutterApi(std::unique_ptr stub); + explicit ScopedStubFlutterApi(std::unique_ptr stub); // Restores the previous test stub. ~ScopedStubFlutterApi(); diff --git a/shell/platform/common/json_message_codec.cc b/shell/platform/common/json_message_codec.cc index b91e57e412c64..ae362f736cde6 100644 --- a/shell/platform/common/json_message_codec.cc +++ b/shell/platform/common/json_message_codec.cc @@ -37,9 +37,7 @@ std::unique_ptr JsonMessageCodec::DecodeMessageInternal( auto json_message = std::make_unique(); rapidjson::ParseResult result = json_message->Parse(raw_message, message_size); - bool parsing_successful = - result == rapidjson::ParseErrorCode::kParseErrorNone; - if (!parsing_successful) { + if (result.IsError()) { std::cerr << "Unable to parse JSON message:" << std::endl << rapidjson::GetParseError_En(result.Code()) << std::endl; return nullptr; diff --git a/shell/platform/common/text_editing_delta.cc b/shell/platform/common/text_editing_delta.cc new file mode 100644 index 0000000000000..e07cdcb30a1b6 --- /dev/null +++ b/shell/platform/common/text_editing_delta.cc @@ -0,0 +1,34 @@ +// 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/text_editing_delta.h" + +namespace flutter { + +TextEditingDelta::TextEditingDelta(const std::u16string& text_before_change, + TextRange range, + const std::u16string& text) + : old_text_(text_before_change), + delta_text_(text), + delta_start_(range.start()), + delta_end_(range.start() + range.length()) {} + +TextEditingDelta::TextEditingDelta(const std::string& text_before_change, + TextRange range, + const std::string& text) + : old_text_(Utf8ToUtf16(text_before_change)), + delta_text_(Utf8ToUtf16(text)), + delta_start_(range.start()), + delta_end_(range.start() + range.length()) {} + +TextEditingDelta::TextEditingDelta(const std::u16string& text) + : old_text_(text), delta_text_(u""), delta_start_(-1), delta_end_(-1) {} + +TextEditingDelta::TextEditingDelta(const std::string& text) + : old_text_(Utf8ToUtf16(text)), + delta_text_(u""), + delta_start_(-1), + delta_end_(-1) {} + +} // namespace flutter diff --git a/shell/platform/common/text_editing_delta.h b/shell/platform/common/text_editing_delta.h new file mode 100644 index 0000000000000..61c9875bc92e6 --- /dev/null +++ b/shell/platform/common/text_editing_delta.h @@ -0,0 +1,92 @@ +// 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_TEXT_EDITING_DELTA_H_ +#define FLUTTER_SHELL_PLATFORM_COMMON_TEXT_EDITING_DELTA_H_ +#define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING + +#include +#include + +#include "flutter/shell/platform/common/text_range.h" + +namespace flutter { + +/// A change in the state of an input field. +struct TextEditingDelta { + TextEditingDelta(const std::u16string& text_before_change, + TextRange range, + const std::u16string& text); + + TextEditingDelta(const std::string& text_before_change, + TextRange range, + const std::string& text); + + TextEditingDelta(const std::u16string& text); + + TextEditingDelta(const std::string& text); + + virtual ~TextEditingDelta() = default; + + /// Get the old_text_ value. + /// + /// All strings are stored as UTF16 but converted to UTF8 when accessed. + std::string old_text() const { return Utf16ToUtf8(old_text_); } + + /// Get the delta_text value. + /// + /// All strings are stored as UTF16 but converted to UTF8 when accessed. + std::string delta_text() const { return Utf16ToUtf8(delta_text_); } + + /// Get the delta_start_ value. + int delta_start() const { return delta_start_; } + + /// Get the delta_end_ value. + int delta_end() const { return delta_end_; } + + bool operator==(const TextEditingDelta& rhs) const { + return old_text_ == rhs.old_text_ && delta_text_ == rhs.delta_text_ && + delta_start_ == rhs.delta_start_ && delta_end_ == rhs.delta_end_; + } + + bool operator!=(const TextEditingDelta& rhs) const { return !(*this == rhs); } + + TextEditingDelta(const TextEditingDelta& other) = default; + + TextEditingDelta& operator=(const TextEditingDelta& other) = default; + + private: + std::u16string old_text_; + std::u16string delta_text_; + int delta_start_; + int delta_end_; + + void set_old_text(const std::u16string& old_text) { old_text_ = old_text; } + + void set_delta_text(const std::u16string& delta_text) { + delta_text_ = delta_text; + } + + void set_delta_start(int delta_start) { delta_start_ = delta_start; } + + void set_delta_end(int delta_end) { delta_end_ = delta_end; } + + // Given a UTF16-encoded string, returns the string encoded in UTF8. + static std::string Utf16ToUtf8(const std::u16string& string) { + std::wstring_convert, char16_t> + utf8_converter; + return utf8_converter.to_bytes(string); + } + + // Given a UTF8-encoded string, returns the string encoded in UTF16. + static std::u16string Utf8ToUtf16(const std::string& string) { + std::wstring_convert, char16_t> + utf16_converter; + return utf16_converter.from_bytes(string); + } +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_COMMON_TEXT_EDITING_DELTA_H_ diff --git a/shell/platform/common/text_editing_delta_unittests.cc b/shell/platform/common/text_editing_delta_unittests.cc new file mode 100644 index 0000000000000..393c1a4a64563 --- /dev/null +++ b/shell/platform/common/text_editing_delta_unittests.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/text_editing_delta.h" + +#include "gtest/gtest.h" + +namespace flutter { + +TEST(TextEditingDeltaTest, TestTextEditingDeltaConstructor) { + // Here we are simulating inserting an "o" at the end of "hell". + std::string old_text = "hell"; + std::string replacement_text = "hello"; + TextRange range(0, 4); + + TextEditingDelta delta = TextEditingDelta(old_text, range, replacement_text); + + EXPECT_EQ(delta.old_text(), old_text); + EXPECT_EQ(delta.delta_text(), "hello"); + EXPECT_EQ(delta.delta_start(), 0); + EXPECT_EQ(delta.delta_end(), 4); +} + +TEST(TextEditingDeltaTest, TestTextEditingDeltaNonTextConstructor) { + // Here we are simulating inserting an "o" at the end of "hell". + std::string old_text = "hello"; + + TextEditingDelta delta = TextEditingDelta(old_text); + + EXPECT_EQ(delta.old_text(), old_text); + EXPECT_EQ(delta.delta_text(), ""); + EXPECT_EQ(delta.delta_start(), -1); + EXPECT_EQ(delta.delta_end(), -1); +} + +} // namespace flutter diff --git a/shell/platform/darwin/common/buffer_conversions.mm b/shell/platform/darwin/common/buffer_conversions.mm index c0c5e43469f34..6ba9b1e4a53ab 100644 --- a/shell/platform/darwin/common/buffer_conversions.mm +++ b/shell/platform/darwin/common/buffer_conversions.mm @@ -10,7 +10,7 @@ namespace { class NSDataMapping : public fml::Mapping { public: - NSDataMapping(NSData* data) : data_([data retain]) {} + explicit NSDataMapping(NSData* data) : data_([data retain]) {} size_t GetSize() const override { return [data_.get() length]; } @@ -18,6 +18,8 @@ return static_cast([data_.get() bytes]); } + bool IsDontNeedSafe() const override { return false; } + private: fml::scoped_nsobject data_; FML_DISALLOW_COPY_AND_ASSIGN(NSDataMapping); diff --git a/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h b/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h index 9c32ed9986117..6b39687162e60 100644 --- a/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h +++ b/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h @@ -87,7 +87,7 @@ FLUTTER_DARWIN_EXPORT * * @param connection The result from `setMessageHandlerOnChannel:binaryMessageHandler:`. */ -- (void)cleanupConnection:(FlutterBinaryMessengerConnection)connection; +- (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection; @end NS_ASSUME_NONNULL_END #endif // FLUTTER_FLUTTERBINARYMESSENGER_H_ diff --git a/shell/platform/darwin/common/framework/Headers/FlutterTexture.h b/shell/platform/darwin/common/framework/Headers/FlutterTexture.h index 7ff23739186d7..59d2ced808df1 100644 --- a/shell/platform/darwin/common/framework/Headers/FlutterTexture.h +++ b/shell/platform/darwin/common/framework/Headers/FlutterTexture.h @@ -50,7 +50,7 @@ FLUTTER_DARWIN_EXPORT - (void)textureFrameAvailable:(int64_t)textureId; /** * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures - * must be unregistered on the the platform thread. + * must be unregistered on the platform thread. * * @param textureId The result that was previously returned from `registerTexture:`. */ diff --git a/shell/platform/darwin/common/framework/Source/FlutterChannels.mm b/shell/platform/darwin/common/framework/Source/FlutterChannels.mm index f5527a438ed9e..b59098d646d19 100644 --- a/shell/platform/darwin/common/framework/Source/FlutterChannels.mm +++ b/shell/platform/darwin/common/framework/Source/FlutterChannels.mm @@ -61,8 +61,9 @@ - (void)sendMessage:(id)message { - (void)sendMessage:(id)message reply:(FlutterReply)callback { FlutterBinaryReply reply = ^(NSData* data) { - if (callback) + if (callback) { callback([_codec decode:data]); + } }; [_messenger sendOnChannel:_name message:[_codec encode:message] binaryReply:reply]; } @@ -70,7 +71,7 @@ - (void)sendMessage:(id)message reply:(FlutterReply)callback { - (void)setMessageHandler:(FlutterMessageHandler)handler { if (!handler) { if (_connection > 0) { - [_messenger cleanupConnection:_connection]; + [_messenger cleanUpConnection:_connection]; _connection = 0; } else { [_messenger setMessageHandlerOnChannel:_name binaryMessageHandler:nil]; @@ -118,10 +119,12 @@ - (void)dealloc { } - (BOOL)isEqual:(id)object { - if (self == object) + if (self == object) { return YES; - if (![object isKindOfClass:[FlutterError class]]) + } + if (![object isKindOfClass:[FlutterError class]]) { return NO; + } FlutterError* other = (FlutterError*)object; return [self.code isEqual:other.code] && ((!self.message && !other.message) || [self.message isEqual:other.message]) && @@ -154,10 +157,12 @@ - (void)dealloc { } - (BOOL)isEqual:(id)object { - if (self == object) + if (self == object) { return YES; - if (![object isKindOfClass:[FlutterMethodCall class]]) + } + if (![object isKindOfClass:[FlutterMethodCall class]]) { return NO; + } FlutterMethodCall* other = (FlutterMethodCall*)object; return [self.method isEqual:[other method]] && ((!self.arguments && !other.arguments) || [self.arguments isEqual:other.arguments]); @@ -168,7 +173,7 @@ - (NSUInteger)hash { } @end -NSObject const* FlutterMethodNotImplemented = [NSObject new]; +NSObject const* FlutterMethodNotImplemented = [[NSObject alloc] init]; @implementation FlutterMethodChannel { NSObject* _messenger; @@ -230,7 +235,7 @@ - (void)invokeMethod:(NSString*)method arguments:(id)arguments result:(FlutterRe - (void)setMethodCallHandler:(FlutterMethodCallHandler)handler { if (!handler) { if (_connection > 0) { - [_messenger cleanupConnection:_connection]; + [_messenger cleanUpConnection:_connection]; _connection = 0; } else { [_messenger setMessageHandlerOnChannel:_name binaryMessageHandler:nil]; @@ -242,12 +247,13 @@ - (void)setMethodCallHandler:(FlutterMethodCallHandler)handler { FlutterBinaryMessageHandler messageHandler = ^(NSData* message, FlutterBinaryReply callback) { FlutterMethodCall* call = [codec decodeMethodCall:message]; handler(call, ^(id result) { - if (result == FlutterMethodNotImplemented) + if (result == FlutterMethodNotImplemented) { callback(nil); - else if ([result isKindOfClass:[FlutterError class]]) + } else if ([result isKindOfClass:[FlutterError class]]) { callback([codec encodeErrorEnvelope:(FlutterError*)result]); - else + } else { callback([codec encodeSuccessEnvelope:result]); + } }); }; _connection = [_messenger setMessageHandlerOnChannel:_name binaryMessageHandler:messageHandler]; @@ -261,7 +267,7 @@ - (void)resizeChannelBuffer:(NSInteger)newSize { #pragma mark - Event channel -NSObject const* FlutterEndOfEventStream = [NSObject new]; +NSObject const* FlutterEndOfEventStream = [[NSObject alloc] init]; @implementation FlutterEventChannel { NSObject* _messenger; @@ -309,23 +315,26 @@ static void SetStreamHandlerMessageHandlerOnChannel(NSObject*)entrypointArgs; + /** * Destroy running context for an engine. * diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterEngineGroup.h b/shell/platform/darwin/ios/framework/Headers/FlutterEngineGroup.h index 7bf295ddbb1cc..097ffd6669fee 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterEngineGroup.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterEngineGroup.h @@ -8,6 +8,36 @@ NS_ASSUME_NONNULL_BEGIN +/** Options that control how a FlutterEngine should be created. */ +FLUTTER_DARWIN_EXPORT +@interface FlutterEngineGroupOptions : NSObject + +/** + * The name of a top-level function from a Dart library. If this is FlutterDefaultDartEntrypoint + * (or nil); this will default to `main()`. If it is not the app's main() function, that function + * must be decorated with `@pragma(vm:entry-point)` to ensure themethod is not tree-shaken by the + * Dart compiler. + */ +@property(nonatomic, copy, nullable) NSString* entrypoint; + +/** + * The URI of the Dart library which contains the entrypoint method. If nil, this will default to + * the same library as the `main()` function in the Dart program. + */ +@property(nonatomic, copy, nullable) NSString* libraryURI; + +/** + * The name of the initial Flutter `Navigator` `Route` to load. If this is + * FlutterDefaultInitialRoute (or nil), it will default to the "/" route. + */ +@property(nonatomic, copy, nullable) NSString* initialRoute; + +/** + * Arguments passed as a list of string to Dart's entrypoint function. + */ +@property(nonatomic, retain, nullable) NSArray* entrypointArgs; +@end + /** * Represents a collection of FlutterEngines who share resources which allows * them to be created with less time const and occupy less memory than just @@ -66,6 +96,15 @@ FLUTTER_DARWIN_EXPORT - (FlutterEngine*)makeEngineWithEntrypoint:(nullable NSString*)entrypoint libraryURI:(nullable NSString*)libraryURI initialRoute:(nullable NSString*)initialRoute; + +/** + * Creates a running `FlutterEngine` that shares components with this group. + * + * @param options Options that control how a FlutterEngine should be created. + * + * @see FlutterEngineGroupOptions + */ +- (FlutterEngine*)makeEngineWithOptions:(nullable FlutterEngineGroupOptions*)options; @end NS_ASSUME_NONNULL_END diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h b/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h index 783c6a35f1e58..8b212c3e4da00 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h @@ -83,6 +83,12 @@ NS_ASSUME_NONNULL_BEGIN - (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken; +/** + * Called if this has been registered for `UIApplicationDelegate` callbacks. + */ +- (void)application:(UIApplication*)application + didFailToRegisterForRemoteNotificationsWithError:(NSError*)error; + /** * Called if this has been registered for `UIApplicationDelegate` callbacks. * diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterPluginAppLifeCycleDelegate.h b/shell/platform/darwin/ios/framework/Headers/FlutterPluginAppLifeCycleDelegate.h index 3d7d090c2cd7d..dd6489c6f6355 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterPluginAppLifeCycleDelegate.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterPluginAppLifeCycleDelegate.h @@ -57,6 +57,12 @@ FLUTTER_DARWIN_EXPORT - (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken; +/** + * Calls all plugins registered for `UIApplicationDelegate` callbacks. + */ +- (void)application:(UIApplication*)application + didFailToRegisterForRemoteNotificationsWithError:(NSError*)error; + /** * Calls all plugins registered for `UIApplicationDelegate` callbacks. */ diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h index 7a5219fb71927..c0950360546ba 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h @@ -11,6 +11,7 @@ #import "FlutterBinaryMessenger.h" #import "FlutterDartProject.h" #import "FlutterEngine.h" +#import "FlutterEngineGroup.h" #import "FlutterMacros.h" #import "FlutterPlugin.h" #import "FlutterTexture.h" @@ -68,6 +69,23 @@ FLUTTER_DARWIN_EXPORT nibName:(nullable NSString*)nibName bundle:(nullable NSBundle*)nibBundle NS_DESIGNATED_INITIALIZER; +/** + * Initializes this FlutterViewController with the specified `FlutterEngineGroup` + * + * FlutterViewController will create an new `FlutterEngine` with `options` and use this engine do + * the same thing like initWithEngine:nibName:bundle:, and when this `FlutterViewController` + * pop,the engine will be gone. + * + * @param engineGroup The specified `FlutterEngineGroup` to use to create new `FlutterEngine`. + * @param options The specified options to give FlutterEngineGroup to create new `FlutterEngine`. + * @param nibName The NIB name to initialize this UIViewController with. + * @param nibBundle The NIB bundle. + */ +- (instancetype)initWithEngineGroup:(FlutterEngineGroup*)engineGroup + options:(nullable FlutterEngineGroupOptions*)options + nibName:(nullable NSString*)nibName + bundle:(nullable NSBundle*)nibBundle NS_DESIGNATED_INITIALIZER; + /** * Initializes a new FlutterViewController and `FlutterEngine` with the specified * `FlutterDartProject`. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index fa68266619ced..a50924e1661ce 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -34,6 +34,7 @@ - (instancetype)init { - (void)dealloc { [_lifeCycleDelegate release]; [_rootFlutterViewControllerGetter release]; + [_window release]; [super dealloc]; } @@ -95,6 +96,12 @@ - (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; } +- (void)application:(UIApplication*)application + didFailToRegisterForRemoteNotificationsWithError:(NSError*)error { + [_lifeCycleDelegate application:application + didFailToRegisterForRemoteNotificationsWithError:error]; +} + #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - (void)application:(UIApplication*)application diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm index c8c4aa700c91d..33b4e423ce3dd 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm @@ -121,6 +121,22 @@ - (void)testLaunchUrlWithFragmentNoQueryParameter { arguments:@"/custom/route#fragment"]); } +- (void)testReleasesWindowOnDealloc { + __weak UIWindow* weakWindow; + @autoreleasepool { + id mockWindow = OCMClassMock([UIWindow class]); + FlutterAppDelegate* appDelegate = [[FlutterAppDelegate alloc] init]; + appDelegate.window = mockWindow; + weakWindow = mockWindow; + XCTAssertNotNil(weakWindow); + [mockWindow stopMocking]; + mockWindow = nil; + appDelegate = nil; + } + // App delegate has released the window. + XCTAssertNil(weakWindow); +} + #pragma mark - Deep linking - (void)testUniversalLinkPushRoute { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.mm b/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.mm index d0f59a5df98ef..24f9beb2d2a19 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.mm @@ -46,9 +46,9 @@ - (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString*)channe } } -- (void)cleanupConnection:(FlutterBinaryMessengerConnection)connection { +- (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection { if (self.parent) { - return [self.parent cleanupConnection:connection]; + return [self.parent cleanUpConnection:connection]; } else { FML_LOG(WARNING) << "Communicating on a dead channel."; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache.mm b/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache.mm index 8c89044f72658..1aea364a58aa7 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache.mm @@ -7,6 +7,14 @@ #include "flutter/lib/ui/plugins/callback_cache.h" @implementation FlutterCallbackInformation + +- (void)dealloc { + [_callbackName release]; + [_callbackClassName release]; + [_callbackLibraryPath release]; + [super dealloc]; +} + @end @implementation FlutterCallbackCache diff --git a/shell/platform/darwin/ios/framework/Source/FlutterChannelKeyResponder.mm b/shell/platform/darwin/ios/framework/Source/FlutterChannelKeyResponder.mm index e1579024a715c..4e9131711bc31 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterChannelKeyResponder.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterChannelKeyResponder.mm @@ -132,7 +132,7 @@ - (void)handlePress:(nonnull FlutterUIPressProxy*)press NSString* characters = getEventCharacters(press.key.characters, press.key.keyCode); NSString* charactersIgnoringModifiers = getEventCharacters(press.key.charactersIgnoringModifiers, press.key.keyCode); - NSMutableDictionary* keyMessage = [@{ + NSMutableDictionary* keyMessage = [[@{ @"keymap" : @"ios", @"type" : type, @"keyCode" : @(press.key.keyCode), @@ -141,7 +141,7 @@ - (void)handlePress:(nonnull FlutterUIPressProxy*)press @"charactersIgnoringModifiers" : charactersIgnoringModifiers == nil ? @"" : charactersIgnoringModifiers, - } mutableCopy]; + } mutableCopy] autorelease]; [self.channel sendMessage:keyMessage reply:^(id reply) { bool handled = reply ? [[reply valueForKey:@"handled"] boolValue] : true; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm b/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm index 2d650a773645a..f9069751edce0 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm @@ -240,6 +240,15 @@ - (instancetype)initWithSettings:(const flutter::Settings&)settings { - (flutter::RunConfiguration)runConfigurationForEntrypoint:(nullable NSString*)entrypointOrNil libraryOrNil:(nullable NSString*)dartLibraryOrNil { + return [self runConfigurationForEntrypoint:entrypointOrNil + libraryOrNil:dartLibraryOrNil + entrypointArgs:nil]; +} + +- (flutter::RunConfiguration)runConfigurationForEntrypoint:(nullable NSString*)entrypointOrNil + libraryOrNil:(nullable NSString*)dartLibraryOrNil + entrypointArgs: + (nullable NSArray*)entrypointArgs { auto config = flutter::RunConfiguration::InferFromSettings(_settings); if (dartLibraryOrNil && entrypointOrNil) { config.SetEntrypointAndLibrary(std::string([entrypointOrNil UTF8String]), @@ -248,6 +257,15 @@ - (instancetype)initWithSettings:(const flutter::Settings&)settings { } else if (entrypointOrNil) { config.SetEntrypoint(std::string([entrypointOrNil UTF8String])); } + + if (entrypointArgs.count) { + std::vector cppEntrypointArgs; + for (NSString* arg in entrypointArgs) { + cppEntrypointArgs.push_back(std::string([arg UTF8String])); + } + config.SetEntrypointArgs(cppEntrypointArgs); + } + return config; } @@ -273,7 +291,7 @@ + (NSString*)domainNetworkPolicy:(NSDictionary*)appTransportSecurity { if (exceptionDomains == nil) { return @""; } - NSMutableArray* networkConfigArray = [[NSMutableArray alloc] init]; + NSMutableArray* networkConfigArray = [[[NSMutableArray alloc] init] autorelease]; for (NSString* domain in exceptionDomains) { NSDictionary* domainConfiguration = [exceptionDomains objectForKey:domain]; // Default value is false. @@ -288,7 +306,7 @@ + (NSString*)domainNetworkPolicy:(NSDictionary*)appTransportSecurity { NSData* jsonData = [NSJSONSerialization dataWithJSONObject:networkConfigArray options:0 error:NULL]; - return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + return [[[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] autorelease]; } + (bool)allowsArbitraryLoads:(NSDictionary*)appTransportSecurity { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h index 1efdf75880281..74203864f42bc 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h @@ -27,6 +27,10 @@ flutter::Settings FLTDefaultSettingsForBundle(NSBundle* bundle = nil); - (flutter::RunConfiguration)runConfigurationForEntrypoint:(nullable NSString*)entrypointOrNil; - (flutter::RunConfiguration)runConfigurationForEntrypoint:(nullable NSString*)entrypointOrNil libraryOrNil:(nullable NSString*)dartLibraryOrNil; +- (flutter::RunConfiguration)runConfigurationForEntrypoint:(nullable NSString*)entrypointOrNil + libraryOrNil:(nullable NSString*)dartLibraryOrNil + entrypointArgs: + (nullable NSArray*)entrypointArgs; + (NSString*)flutterAssetsName:(NSBundle*)bundle; + (NSString*)domainNetworkPolicy:(NSDictionary*)appTransportSecurity; @@ -35,7 +39,7 @@ flutter::Settings FLTDefaultSettingsForBundle(NSBundle* bundle = nil); /** * The embedder can specify data that the isolate can request synchronously on launch. Engines * launched using this configuration can access the persistent isolate data via the - * `Window.getPersistentIsolateData` accessor. + * `PlatformDispatcher.getPersistentIsolateData` accessor. * * @param data The persistent isolate data. This data is persistent for the duration of the Flutter * application and is available even after isolate restarts. Because of this lifecycle, diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.mm b/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.mm index ddf21772a7e05..2bb10c182d84f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.mm @@ -70,7 +70,7 @@ static uint64_t KeyOfPlane(uint64_t baseKey, uint64_t plane) { static uint64_t GetPhysicalKeyForKeyCode(UInt32 keyCode) { auto physicalKey = keyCodeToPhysicalKey.find(keyCode); if (physicalKey == keyCodeToPhysicalKey.end()) { - return 0; + return KeyOfPlane(keyCode, kIosPlane); } return physicalKey->second; } @@ -158,7 +158,7 @@ static uint64_t GetLogicalKeyForEvent(FlutterUIPressProxy* press, uint64_t physi const char* characters = getEventCharacters(press.key.charactersIgnoringModifiers, press.key.keyCode); NSString* keyLabel = - characters == nullptr ? nil : [[NSString alloc] initWithUTF8String:characters]; + characters == nullptr ? nil : [[[NSString alloc] initWithUTF8String:characters] autorelease]; NSUInteger keyLabelLength = [keyLabel length]; // If this key is printable, generate the logical key from its Unicode // value. Control keys such as ESC, CTRL, and SHIFT are not printable. HOME, @@ -349,7 +349,7 @@ @interface FlutterEmbedderKeyResponder () * * Set by the initializer. */ -@property(nonatomic) FlutterSendKeyEvent sendEvent; +@property(nonatomic, copy, readonly) FlutterSendKeyEvent sendEvent; /** * A map of pressed keys. @@ -357,7 +357,7 @@ @interface FlutterEmbedderKeyResponder () * The keys of the dictionary are physical keys, while the values are the logical keys * of the key down event. */ -@property(nonatomic) NSMutableDictionary* pressingRecords; +@property(nonatomic, retain, readonly) NSMutableDictionary* pressingRecords; /** * A constant mask for NSEvent.modifierFlags that Flutter synchronizes with. @@ -396,7 +396,8 @@ @interface FlutterEmbedderKeyResponder () * Its values are |responseId|s, and keys are the callback that was received * along with the event. */ -@property(nonatomic) NSMutableDictionary* pendingResponses; +@property(nonatomic, retain, readonly) + NSMutableDictionary* pendingResponses; /** * Compare the last modifier flags and the current, and dispatch synthesized @@ -514,11 +515,11 @@ - (void)handlePress:(nonnull FlutterUIPressProxy*)press FlutterKeyCallbackGuard* guardedCallback = nil; switch (press.phase) { case UIPressPhaseBegan: - guardedCallback = [[FlutterKeyCallbackGuard alloc] initWithCallback:callback]; + guardedCallback = [[[FlutterKeyCallbackGuard alloc] initWithCallback:callback] autorelease]; [self handlePressBegin:press callback:guardedCallback]; break; case UIPressPhaseEnded: - guardedCallback = [[FlutterKeyCallbackGuard alloc] initWithCallback:callback]; + guardedCallback = [[[FlutterKeyCallbackGuard alloc] initWithCallback:callback] autorelease]; [self handlePressEnd:press callback:guardedCallback]; break; case UIPressPhaseChanged: @@ -623,7 +624,7 @@ - (void)sendPrimaryFlutterEvent:(const FlutterKeyEvent&)event _responseId += 1; uint64_t responseId = _responseId; FlutterKeyPendingResponse* pending = - [[FlutterKeyPendingResponse alloc] initWithHandler:self responseId:responseId]; + [[[FlutterKeyPendingResponse alloc] initWithHandler:self responseId:responseId] autorelease]; [callback pendTo:_pendingResponses withId:responseId]; _sendEvent(event, HandleResponse, pending); } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm index 6de6c840486be..5c14a5196befe 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm @@ -88,6 +88,8 @@ - (void)dealloc { constexpr UIKeyboardHIDUsage kKeyCodeF1 = (UIKeyboardHIDUsage)0x3a; API_AVAILABLE(ios(13.4)) constexpr UIKeyboardHIDUsage kKeyCodeAltRight = (UIKeyboardHIDUsage)0xe6; +API_AVAILABLE(ios(13.4)) +constexpr UIKeyboardHIDUsage kKeyCodeEject = (UIKeyboardHIDUsage)0xb8; constexpr uint64_t kPhysicalKeyUndefined = 0x00070003; @@ -170,6 +172,64 @@ - (void)testBasicKeyEvent API_AVAILABLE(ios(13.4)) { [events removeAllObjects]; } +- (void)testIosKeyPlane API_AVAILABLE(ios(13.4)) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + __block BOOL last_handled = TRUE; + FlutterKeyEvent* event; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + last_handled = FALSE; + // Verify that the eject key (keycode 0xb8, which is not present in the keymap) + // should be translated to the right logical and physical keys. + [responder handlePress:keyDownEvent(kKeyCodeEject, kModifierFlagNone, 123.0f) + callback:^(BOOL handled) { + last_handled = handled; + }]; + + XCTAssertEqual([events count], 1u); + event = [events lastObject].data; + XCTAssertEqual(event->type, kFlutterKeyEventTypeDown); + XCTAssertEqual(event->physical, kKeyCodeEject | kIosPlane); + XCTAssertEqual(event->logical, kKeyCodeEject | kIosPlane); + XCTAssertEqual(event->character, nullptr); + XCTAssertEqual(event->synthesized, false); + + XCTAssertEqual(last_handled, FALSE); + XCTAssert([[events lastObject] hasCallback]); + [[events lastObject] respond:TRUE]; + XCTAssertEqual(last_handled, TRUE); + + [events removeAllObjects]; + + last_handled = TRUE; + [responder handlePress:keyUpEvent(kKeyCodeEject, kModifierFlagNone, 123.0f) + callback:^(BOOL handled) { + last_handled = handled; + }]; + + XCTAssertEqual([events count], 1u); + event = [events lastObject].data; + XCTAssertEqual(event->type, kFlutterKeyEventTypeUp); + XCTAssertEqual(event->physical, kKeyCodeEject | kIosPlane); + XCTAssertEqual(event->logical, kKeyCodeEject | kIosPlane); + XCTAssertEqual(event->character, nullptr); + XCTAssertEqual(event->synthesized, false); + + XCTAssertEqual(last_handled, TRUE); + XCTAssert([[events lastObject] hasCallback]); + [[events lastObject] respond:FALSE]; // Check if responding FALSE works + XCTAssertEqual(last_handled, FALSE); + + [events removeAllObjects]; +} + - (void)testOutOfOrderModifiers API_AVAILABLE(ios(13.4)) { __block NSMutableArray* events = [[NSMutableArray alloc] init]; FlutterKeyEvent* event; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index c19ea2a00c966..03fe0a06e8e39 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -20,6 +20,7 @@ #import "flutter/shell/platform/darwin/common/command_line.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterIndirectScribbleDelegate.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h" @@ -35,6 +36,7 @@ NSString* const FlutterDefaultDartEntrypoint = nil; NSString* const FlutterDefaultInitialRoute = nil; NSString* const FlutterEngineWillDealloc = @"FlutterEngineWillDealloc"; +NSString* const FlutterKeyDataChannel = @"flutter/keydata"; static constexpr int kNumProfilerSamplesPerSec = 5; @interface FlutterEngineRegistrar : NSObject @@ -42,7 +44,9 @@ @interface FlutterEngineRegistrar : NSObject - (instancetype)initWithPlugin:(NSString*)pluginKey flutterEngine:(FlutterEngine*)flutterEngine; @end -@interface FlutterEngine () +@interface FlutterEngine () // Maintains a dictionary of plugin names that have registered with the engine. Used by // FlutterEngineRegistrar to implement a FlutterPluginRegistrar. @property(nonatomic, readonly) NSMutableDictionary* pluginPublications; @@ -196,6 +200,10 @@ - (void)dealloc { object:self userInfo:nil]; + // It will be destroyed and invalidate its weak pointers + // before any other members are destroyed. + _weakFactory.reset(); + /// nil out weak references. [_registrars enumerateKeysAndObjectsUsingBlock:^(id key, FlutterEngineRegistrar* registrar, BOOL* stop) { @@ -208,6 +216,7 @@ - (void)dealloc { [_registrars release]; _binaryMessenger.parent = nil; [_binaryMessenger release]; + [_isolateId release]; NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; if (_flutterViewControllerWillDeallocObserver) { @@ -293,13 +302,20 @@ - (void)sendKeyEvent:(const FlutterKeyEvent&)event key_data.synthesized = event.synthesized; auto packet = std::make_unique(key_data, character); + NSData* message = [NSData dataWithBytes:packet->data().data() length:packet->data().size()]; - auto response = [callback, userData](bool handled) { - if (callback != nullptr) { - callback(handled, userData); + auto response = ^(NSData* reply) { + if (callback == nullptr) { + return; + } + BOOL handled = FALSE; + if (reply.length == 1 && *reinterpret_cast(reply.bytes) == 1) { + handled = TRUE; } + callback(handled, userData); }; - self.platformView->DispatchKeyDataPacket(std::move(packet), std::move(response)); + + [self sendOnChannel:FlutterKeyDataChannel message:message binaryReply:response]; } - (void)ensureSemanticsEnabled { @@ -312,6 +328,7 @@ - (void)setViewController:(FlutterViewController*)viewController { viewController ? [viewController getWeakPtr] : fml::WeakPtr(); self.iosPlatformView->SetOwnerViewController(_viewController); [self maybeSetupPlatformViewChannels]; + _textInputPlugin.get().viewController = viewController; if (viewController) { __block FlutterEngine* blockSelf = self; @@ -330,6 +347,7 @@ - (void)setViewController:(FlutterViewController*)viewController { - (void)attachView { self.iosPlatformView->attachView(); + [_textInputPlugin.get() setupIndirectScribbleInteraction:self.viewController]; } - (void)setFlutterViewControllerWillDeallocObserver:(id)observer { @@ -345,6 +363,7 @@ - (void)setFlutterViewControllerWillDeallocObserver:(id)observer { - (void)notifyViewControllerDeallocated { [[self lifecycleChannel] sendMessage:@"AppLifecycleState.detached"]; + _textInputPlugin.get().viewController = nil; if (!_allowHeadlessExecution) { [self destroyContext]; } else { @@ -353,6 +372,7 @@ - (void)notifyViewControllerDeallocated { platform_view->SetOwnerViewController({}); } } + [_textInputPlugin.get() resetViewResponder]; _viewController.reset(); } @@ -512,6 +532,8 @@ - (void)setupChannels { _textInputPlugin.reset([[FlutterTextInputPlugin alloc] init]); _textInputPlugin.get().textInputDelegate = self; + _textInputPlugin.get().indirectScribbleDelegate = self; + [_textInputPlugin.get() setupIndirectScribbleInteraction:self.viewController]; _platformPlugin.reset([[FlutterPlatformPlugin alloc] initWithEngine:[self getWeakPtr]]); @@ -547,10 +569,13 @@ - (void)maybeSetupPlatformViewChannels { return self.shell.Screenshot(type, base64Encode); } -- (void)launchEngine:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil { +- (void)launchEngine:(NSString*)entrypoint + libraryURI:(NSString*)libraryOrNil + entrypointArgs:(NSArray*)entrypointArgs { // Launch the Dart application with the inferred run configuration. self.shell.RunEngine([_dartProject.get() runConfigurationForEntrypoint:entrypoint - libraryOrNil:libraryOrNil]); + libraryOrNil:libraryOrNil + entrypointArgs:entrypointArgs]); } - (void)setupShell:(std::unique_ptr)shell @@ -675,8 +700,9 @@ - (BOOL)createShell:(NSString*)entrypoint - (void)initializeDisplays { double refresh_rate = [DisplayLinkManager displayRefreshRate]; - auto display = flutter::Display(refresh_rate); - _shell->OnDisplayUpdates(flutter::DisplayUpdateType::kStartup, {display}); + std::vector> displays; + displays.push_back(std::make_unique(refresh_rate)); + _shell->OnDisplayUpdates(flutter::DisplayUpdateType::kStartup, std::move(displays)); } - (BOOL)run { @@ -702,8 +728,18 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint initialRoute:(NSString*)initialR - (BOOL)runWithEntrypoint:(NSString*)entrypoint libraryURI:(NSString*)libraryURI initialRoute:(NSString*)initialRoute { + return [self runWithEntrypoint:entrypoint + libraryURI:libraryURI + initialRoute:initialRoute + entrypointArgs:nil]; +} + +- (BOOL)runWithEntrypoint:(NSString*)entrypoint + libraryURI:(NSString*)libraryURI + initialRoute:(NSString*)initialRoute + entrypointArgs:(NSArray*)entrypointArgs { if ([self createShell:entrypoint libraryURI:libraryURI initialRoute:initialRoute]) { - [self launchEngine:entrypoint libraryURI:libraryURI]; + [self launchEngine:entrypoint libraryURI:libraryURI entrypointArgs:entrypointArgs]; } return _shell != nullptr; @@ -718,29 +754,30 @@ - (void)notifyLowMemory { #pragma mark - Text input delegate -- (void)handlePressEvent:(FlutterUIPressProxy*)press - nextAction:(void (^)())next API_AVAILABLE(ios(13.4)) { - if (_viewController.get() != nullptr) { - [_viewController.get() handlePressEvent:press nextAction:next]; - } -} - -- (void)updateEditingClient:(int)client withState:(NSDictionary*)state { +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + updateEditingClient:(int)client + withState:(NSDictionary*)state { [_textInputChannel.get() invokeMethod:@"TextInputClient.updateEditingState" arguments:@[ @(client), state ]]; } -- (void)updateEditingClient:(int)client withState:(NSDictionary*)state withTag:(NSString*)tag { +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + updateEditingClient:(int)client + withState:(NSDictionary*)state + withTag:(NSString*)tag { [_textInputChannel.get() invokeMethod:@"TextInputClient.updateEditingStateWithTag" arguments:@[ @(client), @{tag : state} ]]; } -- (void)updateEditingClient:(int)client withDelta:(NSDictionary*)delta { +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + updateEditingClient:(int)client + withDelta:(NSDictionary*)delta { [_textInputChannel.get() invokeMethod:@"TextInputClient.updateEditingStateWithDeltas" arguments:@[ @(client), delta ]]; } -- (void)updateFloatingCursor:(FlutterFloatingCursorDragState)state +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + updateFloatingCursor:(FlutterFloatingCursorDragState)state withClient:(int)client withPosition:(NSDictionary*)position { NSString* stateString; @@ -759,7 +796,9 @@ - (void)updateFloatingCursor:(FlutterFloatingCursorDragState)state arguments:@[ @(client), stateString, position ]]; } -- (void)performAction:(FlutterTextInputAction)action withClient:(int)client { +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + performAction:(FlutterTextInputAction)action + withClient:(int)client { NSString* actionString; switch (action) { case FlutterTextInputActionUnspecified: @@ -804,15 +843,63 @@ - (void)performAction:(FlutterTextInputAction)action withClient:(int)client { arguments:@[ @(client), actionString ]]; } -- (void)showAutocorrectionPromptRectForStart:(NSUInteger)start - end:(NSUInteger)end - withClient:(int)client { +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + showAutocorrectionPromptRectForStart:(NSUInteger)start + end:(NSUInteger)end + withClient:(int)client { [_textInputChannel.get() invokeMethod:@"TextInputClient.showAutocorrectionPromptRect" arguments:@[ @(client), @(start), @(end) ]]; } #pragma mark - FlutterViewEngineDelegate +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView showToolbar:(int)client { + [_textInputChannel.get() invokeMethod:@"TextInputClient.showToolbar" arguments:@[ @(client) ]]; +} + +- (void)flutterTextInputPlugin:(FlutterTextInputPlugin*)textInputPlugin + focusElement:(UIScribbleElementIdentifier)elementIdentifier + atPoint:(CGPoint)referencePoint + result:(FlutterResult)callback { + [_textInputChannel.get() + invokeMethod:@"TextInputClient.focusElement" + arguments:@[ elementIdentifier, @(referencePoint.x), @(referencePoint.y) ] + result:callback]; +} + +- (void)flutterTextInputPlugin:(FlutterTextInputPlugin*)textInputPlugin + requestElementsInRect:(CGRect)rect + result:(FlutterResult)callback { + [_textInputChannel.get() + invokeMethod:@"TextInputClient.requestElementsInRect" + arguments:@[ @(rect.origin.x), @(rect.origin.y), @(rect.size.width), @(rect.size.height) ] + result:callback]; +} + +- (void)flutterTextInputViewScribbleInteractionBegan:(FlutterTextInputView*)textInputView { + [_textInputChannel.get() invokeMethod:@"TextInputClient.scribbleInteractionBegan" arguments:nil]; +} + +- (void)flutterTextInputViewScribbleInteractionFinished:(FlutterTextInputView*)textInputView { + [_textInputChannel.get() invokeMethod:@"TextInputClient.scribbleInteractionFinished" + arguments:nil]; +} + +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + insertTextPlaceholderWithSize:(CGSize)size + withClient:(int)client { + [_textInputChannel.get() invokeMethod:@"TextInputClient.insertTextPlaceholder" + arguments:@[ @(client), @(size.width), @(size.height) ]]; +} + +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + removeTextPlaceholder:(int)client { + [_textInputChannel.get() invokeMethod:@"TextInputClient.removeTextPlaceholder" + arguments:@[ @(client) ]]; +} + +#pragma mark - Screenshot Delegate + - (flutter::Rasterizer::Screenshot)takeScreenshot:(flutter::Rasterizer::ScreenshotType)type asBase64Encoded:(BOOL)base64Encode { FML_DCHECK(_shell) << "Cannot takeScreenshot without a shell"; @@ -879,7 +966,7 @@ - (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString*)channe } } -- (void)cleanupConnection:(FlutterBinaryMessengerConnection)connection { +- (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection { if (_shell && _shell->IsSetup()) { std::string channel = _connections->CleanupConnection(connection); if (!channel.empty()) { @@ -999,14 +1086,16 @@ - (void)waitForFirstFrame:(NSTimeInterval)timeout - (FlutterEngine*)spawnWithEntrypoint:(/*nullable*/ NSString*)entrypoint libraryURI:(/*nullable*/ NSString*)libraryURI - initialRoute:(/*nullable*/ NSString*)initialRoute { + initialRoute:(/*nullable*/ NSString*)initialRoute + entrypointArgs:(/*nullable*/ NSArray*)entrypointArgs { NSAssert(_shell, @"Spawning from an engine without a shell (possibly not run)."); FlutterEngine* result = [[FlutterEngine alloc] initWithName:_labelPrefix project:_dartProject.get() allowHeadlessExecution:_allowHeadlessExecution]; - flutter::RunConfiguration configuration = - [_dartProject.get() runConfigurationForEntrypoint:entrypoint libraryOrNil:libraryURI]; + [_dartProject.get() runConfigurationForEntrypoint:entrypoint + libraryOrNil:libraryURI + entrypointArgs:entrypointArgs]; fml::WeakPtr platform_view = _shell->GetPlatformView(); FML_DCHECK(platform_view); @@ -1039,8 +1128,9 @@ - (FlutterEngine*)spawnWithEntrypoint:(/*nullable*/ NSString*)entrypoint result->_threadHost = _threadHost; result->_profiler = _profiler; result->_profiler_metrics = _profiler_metrics; + result->_isGpuDisabled = _isGpuDisabled; [result setupShell:std::move(shell) withObservatoryPublication:NO]; - return result; + return [result autorelease]; } - (const flutter::ThreadHost&)threadHost { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngineGroup.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngineGroup.mm index 6c03558b146b9..5c02f8ad588ba 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngineGroup.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngineGroup.mm @@ -5,10 +5,22 @@ #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngineGroup.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" +@implementation FlutterEngineGroupOptions + +- (void)dealloc { + [_entrypoint release]; + [_libraryURI release]; + [_initialRoute release]; + [_entrypointArgs release]; + [super dealloc]; +} + +@end + @interface FlutterEngineGroup () @property(nonatomic, copy) NSString* name; -@property(nonatomic, strong) NSMutableArray* engines; -@property(nonatomic, strong) FlutterDartProject* project; +@property(nonatomic, retain) NSMutableArray* engines; +@property(nonatomic, retain) FlutterDartProject* project; @end @implementation FlutterEngineGroup { @@ -18,9 +30,9 @@ @implementation FlutterEngineGroup { - (instancetype)initWithName:(NSString*)name project:(nullable FlutterDartProject*)project { self = [super init]; if (self) { - self.name = name; - self.engines = [[NSMutableArray alloc] init]; - self.project = project; + _name = [name copy]; + _engines = [[NSMutableArray alloc] init]; + _project = [project retain]; } return self; } @@ -30,6 +42,7 @@ - (void)dealloc { [center removeObserver:self]; [_name release]; [_engines release]; + [_project release]; [super dealloc]; } @@ -41,16 +54,32 @@ - (FlutterEngine*)makeEngineWithEntrypoint:(nullable NSString*)entrypoint - (FlutterEngine*)makeEngineWithEntrypoint:(nullable NSString*)entrypoint libraryURI:(nullable NSString*)libraryURI initialRoute:(nullable NSString*)initialRoute { - NSString* engineName = [NSString stringWithFormat:@"%@.%d", self.name, ++_enginesCreatedCount]; + FlutterEngineGroupOptions* options = [[[FlutterEngineGroupOptions alloc] init] autorelease]; + options.entrypoint = entrypoint; + options.libraryURI = libraryURI; + options.initialRoute = initialRoute; + return [self makeEngineWithOptions:options]; +} + +- (FlutterEngine*)makeEngineWithOptions:(nullable FlutterEngineGroupOptions*)options { + NSString* entrypoint = options.entrypoint; + NSString* libraryURI = options.libraryURI; + NSString* initialRoute = options.initialRoute; + NSArray* entrypointArgs = options.entrypointArgs; + FlutterEngine* engine; if (self.engines.count <= 0) { - engine = [[FlutterEngine alloc] initWithName:engineName project:self.project]; - [engine runWithEntrypoint:entrypoint libraryURI:libraryURI initialRoute:initialRoute]; + engine = [self makeEngine]; + [engine runWithEntrypoint:entrypoint + libraryURI:libraryURI + initialRoute:initialRoute + entrypointArgs:entrypointArgs]; } else { FlutterEngine* spawner = (FlutterEngine*)[self.engines[0] pointerValue]; engine = [spawner spawnWithEntrypoint:entrypoint libraryURI:libraryURI - initialRoute:initialRoute]; + initialRoute:initialRoute + entrypointArgs:entrypointArgs]; } [_engines addObject:[NSValue valueWithPointer:engine]]; @@ -60,7 +89,13 @@ - (FlutterEngine*)makeEngineWithEntrypoint:(nullable NSString*)entrypoint name:FlutterEngineWillDealloc object:engine]; - return [engine autorelease]; + return engine; +} + +- (FlutterEngine*)makeEngine { + NSString* engineName = [NSString stringWithFormat:@"%@.%d", self.name, ++_enginesCreatedCount]; + FlutterEngine* result = [[FlutterEngine alloc] initWithName:engineName project:self.project]; + return [result autorelease]; } - (void)onEngineWillBeDealloced:(NSNotification*)notification { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngineGroupTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngineGroupTest.mm index dd34a667f80c1..db43205c051f8 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngineGroupTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngineGroupTest.mm @@ -10,6 +10,10 @@ FLUTTER_ASSERT_ARC +@interface FlutterEngineGroup () +- (FlutterEngine*)makeEngine; +@end + @interface FlutterEngineGroupTest : XCTestCase @end @@ -24,10 +28,12 @@ - (void)testMake { - (void)testSpawn { FlutterEngineGroup* group = [[FlutterEngineGroup alloc] initWithName:@"foo" project:nil]; FlutterEngine* spawner = [group makeEngineWithEntrypoint:nil libraryURI:nil]; + spawner.isGpuDisabled = YES; FlutterEngine* spawnee = [group makeEngineWithEntrypoint:nil libraryURI:nil]; XCTAssertNotNil(spawner); XCTAssertNotNil(spawnee); XCTAssertEqual(&spawner.threadHost, &spawnee.threadHost); + XCTAssertEqual(spawner.isGpuDisabled, spawnee.isGpuDisabled); } - (void)testDeleteLastEngine { @@ -40,4 +46,98 @@ - (void)testDeleteLastEngine { XCTAssertNotNil(spawnee); } +- (void)testCustomEntrypoint { + FlutterEngineGroup* group = OCMPartialMock([[FlutterEngineGroup alloc] initWithName:@"foo" + project:nil]); + FlutterEngine* mockEngine = OCMClassMock([FlutterEngine class]); + OCMStub([group makeEngine]).andReturn(mockEngine); + OCMStub([mockEngine spawnWithEntrypoint:[OCMArg any] + libraryURI:[OCMArg any] + initialRoute:[OCMArg any] + entrypointArgs:[OCMArg any]]) + .andReturn(OCMClassMock([FlutterEngine class])); + FlutterEngine* spawner = [group makeEngineWithEntrypoint:@"firstEntrypoint" + libraryURI:@"firstLibraryURI"]; + XCTAssertNotNil(spawner); + OCMVerify([spawner runWithEntrypoint:@"firstEntrypoint" + libraryURI:@"firstLibraryURI" + initialRoute:nil + entrypointArgs:nil]); + + FlutterEngine* spawnee = [group makeEngineWithEntrypoint:@"secondEntrypoint" + libraryURI:@"secondLibraryURI"]; + XCTAssertNotNil(spawnee); + OCMVerify([spawner spawnWithEntrypoint:@"secondEntrypoint" + libraryURI:@"secondLibraryURI" + initialRoute:nil + entrypointArgs:nil]); +} + +- (void)testCustomInitialRoute { + FlutterEngineGroup* group = OCMPartialMock([[FlutterEngineGroup alloc] initWithName:@"foo" + project:nil]); + FlutterEngine* mockEngine = OCMClassMock([FlutterEngine class]); + OCMStub([group makeEngine]).andReturn(mockEngine); + OCMStub([mockEngine spawnWithEntrypoint:[OCMArg any] + libraryURI:[OCMArg any] + initialRoute:[OCMArg any] + entrypointArgs:[OCMArg any]]) + .andReturn(OCMClassMock([FlutterEngine class])); + FlutterEngine* spawner = [group makeEngineWithEntrypoint:nil libraryURI:nil initialRoute:@"foo"]; + XCTAssertNotNil(spawner); + OCMVerify([spawner runWithEntrypoint:nil libraryURI:nil initialRoute:@"foo" entrypointArgs:nil]); + + FlutterEngine* spawnee = [group makeEngineWithEntrypoint:nil libraryURI:nil initialRoute:@"bar"]; + XCTAssertNotNil(spawnee); + OCMVerify([spawner spawnWithEntrypoint:nil + libraryURI:nil + initialRoute:@"bar" + entrypointArgs:nil]); +} + +- (void)testCustomEntrypointArgs { + FlutterEngineGroup* group = OCMPartialMock([[FlutterEngineGroup alloc] initWithName:@"foo" + project:nil]); + FlutterEngine* mockEngine = OCMClassMock([FlutterEngine class]); + OCMStub([group makeEngine]).andReturn(mockEngine); + OCMStub([mockEngine spawnWithEntrypoint:[OCMArg any] + libraryURI:[OCMArg any] + initialRoute:[OCMArg any] + entrypointArgs:[OCMArg any]]) + .andReturn(OCMClassMock([FlutterEngine class])); + FlutterEngineGroupOptions* firstOptions = [[FlutterEngineGroupOptions alloc] init]; + NSArray* firstEntrypointArgs = @[ @"foo", @"first" ]; + firstOptions.entrypointArgs = firstEntrypointArgs; + FlutterEngine* spawner = [group makeEngineWithOptions:firstOptions]; + XCTAssertNotNil(spawner); + OCMVerify([spawner runWithEntrypoint:nil + libraryURI:nil + initialRoute:nil + entrypointArgs:firstEntrypointArgs]); + + NSArray* secondEntrypointArgs = @[ @"bar", @"second" ]; + FlutterEngineGroupOptions* secondOptions = [[FlutterEngineGroupOptions alloc] init]; + secondOptions.entrypointArgs = secondEntrypointArgs; + FlutterEngine* spawnee = [group makeEngineWithOptions:secondOptions]; + XCTAssertNotNil(spawnee); + OCMVerify([spawner spawnWithEntrypoint:nil + libraryURI:nil + initialRoute:nil + entrypointArgs:secondEntrypointArgs]); +} + +- (void)testReleasesProjectOnDealloc { + __weak FlutterDartProject* weakProject; + @autoreleasepool { + FlutterDartProject* mockProject = OCMClassMock([FlutterDartProject class]); + FlutterEngineGroup* group = [[FlutterEngineGroup alloc] initWithName:@"foo" + project:mockProject]; + weakProject = mockProject; + XCTAssertNotNil(weakProject); + group = nil; + mockProject = nil; + } + XCTAssertNil(weakProject); +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm index 25bfa3ac5a05b..4a4a8877ce877 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm @@ -26,8 +26,6 @@ void OnPlatformViewSetViewportMetrics(const ViewportMetrics& metrics) override { void OnPlatformViewDispatchPlatformMessage(std::unique_ptr message) override {} void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr packet) override { } - void OnPlatformViewDispatchKeyDataPacket(std::unique_ptr packet, - std::function callback) override {} void OnPlatformViewDispatchSemanticsAction(int32_t id, SemanticsAction action, fml::MallocMapping args) override {} diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index 1263b71adf707..0f7fa74fad041 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -194,7 +194,10 @@ - (void)testWaitForFirstFrameTimeout { - (void)testSpawn { FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"]; [engine run]; - FlutterEngine* spawn = [engine spawnWithEntrypoint:nil libraryURI:nil initialRoute:nil]; + FlutterEngine* spawn = [engine spawnWithEntrypoint:nil + libraryURI:nil + initialRoute:nil + entrypointArgs:nil]; XCTAssertNotNil(spawn); } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest_mrc.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest_mrc.mm index 612404f926ff4..1acf3755f07b5 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest_mrc.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest_mrc.mm @@ -38,7 +38,10 @@ - (void)tearDown { - (void)testSpawnsShareGpuContext { FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"]; [engine run]; - FlutterEngine* spawn = [engine spawnWithEntrypoint:nil libraryURI:nil initialRoute:nil]; + FlutterEngine* spawn = [engine spawnWithEntrypoint:nil + libraryURI:nil + initialRoute:nil + entrypointArgs:nil]; XCTAssertNotNil(spawn); XCTAssertTrue([engine iosPlatformView] != nullptr); XCTAssertTrue([spawn iosPlatformView] != nullptr); @@ -50,7 +53,6 @@ - (void)testSpawnsShareGpuContext { XCTAssertTrue(engine_context->GetMainContext() != nullptr); XCTAssertEqual(engine_context->GetMainContext(), spawn_context->GetMainContext()); [engine release]; - [spawn release]; } - (void)testEnableSemanticsWhenFlutterViewAccessibilityDidCall { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h index d81eaf6a6c2e7..18ecb7c78963f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h @@ -21,6 +21,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterIndirectScribbleDelegate.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.h" @@ -49,7 +50,9 @@ extern NSString* _Nonnull const FlutterEngineWillDealloc; - (std::shared_ptr&)platformViewsController; - (nonnull FlutterTextInputPlugin*)textInputPlugin; - (nonnull FlutterRestorationPlugin*)restorationPlugin; -- (void)launchEngine:(nullable NSString*)entrypoint libraryURI:(nullable NSString*)libraryOrNil; +- (void)launchEngine:(nullable NSString*)entrypoint + libraryURI:(nullable NSString*)libraryOrNil + entrypointArgs:(nullable NSArray*)entrypointArgs; - (BOOL)createShell:(nullable NSString*)entrypoint libraryURI:(nullable NSString*)libraryOrNil initialRoute:(nullable NSString*)initialRoute; @@ -68,7 +71,8 @@ extern NSString* _Nonnull const FlutterEngineWillDealloc; */ - (nonnull FlutterEngine*)spawnWithEntrypoint:(nullable NSString*)entrypoint libraryURI:(nullable NSString*)libraryURI - initialRoute:(nullable NSString*)initialRoute; + initialRoute:(nullable NSString*)initialRoute + entrypointArgs:(nullable NSArray*)entrypointArgs; /** * Dispatches the given key event data to the framework through the engine. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h index 9d251a24b1c6a..c080a1d3cfb98 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h @@ -20,6 +20,7 @@ class ThreadHost; - (void)waitForFirstFrame:(NSTimeInterval)timeout callback:(void (^)(BOOL didTimeout))callback; - (FlutterEngine*)spawnWithEntrypoint:(/*nullable*/ NSString*)entrypoint libraryURI:(/*nullable*/ NSString*)libraryURI - initialRoute:(/*nullable*/ NSString*)initialRoute; + initialRoute:(/*nullable*/ NSString*)initialRoute + entrypointArgs:(/*nullable*/ NSArray*)entrypointArgs; - (const flutter::ThreadHost&)threadHost; @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterIndirectScribbleDelegate.h b/shell/platform/darwin/ios/framework/Source/FlutterIndirectScribbleDelegate.h new file mode 100644 index 0000000000000..f5647e6173387 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterIndirectScribbleDelegate.h @@ -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. + +#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERINDIRECTSCRIBBLEDELEGATE_H_ +#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERINDIRECTSCRIBBLEDELEGATE_H_ + +#import + +NS_ASSUME_NONNULL_BEGIN +@class FlutterTextInputPlugin; + +@protocol FlutterIndirectScribbleDelegate +- (void)flutterTextInputPlugin:(FlutterTextInputPlugin*)textInputPlugin + focusElement:(UIScribbleElementIdentifier)elementIdentifier + atPoint:(CGPoint)referencePoint + result:(FlutterResult)callback; +- (void)flutterTextInputPlugin:(FlutterTextInputPlugin*)textInputPlugin + requestElementsInRect:(CGRect)rect + result:(FlutterResult)callback; +@end +NS_ASSUME_NONNULL_END + +#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERINDIRECTSCRIBBLEDELEGATE_H_ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.mm b/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.mm index f575307b65061..db6aeca077bac 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.mm @@ -4,18 +4,23 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h" #include "flutter/fml/memory/weak_ptr.h" +#include "flutter/fml/platform/darwin/message_loop_darwin.h" + +static constexpr CFTimeInterval kDistantFuture = 1.0e10; @interface FlutterKeyboardManager () /** * The primary responders added by addPrimaryResponder. */ -@property(nonatomic) NSMutableArray>* primaryResponders; +@property(nonatomic, retain, readonly) + NSMutableArray>* primaryResponders; /** * The secondary responders added by addSecondaryResponder. */ -@property(nonatomic) NSMutableArray>* secondaryResponders; +@property(nonatomic, retain, readonly) + NSMutableArray>* secondaryResponders; - (void)dispatchToSecondaryResponders:(nonnull FlutterUIPressProxy*)press complete:(nonnull KeyEventCompleteCallback)callback @@ -46,6 +51,10 @@ - (void)addSecondaryResponder:(nonnull id)responde } - (void)dealloc { + // It will be destroyed and invalidate its weak pointers + // before any other members are destroyed. + _weakFactory.reset(); + [_primaryResponders removeAllObjects]; [_secondaryResponders removeAllObjects]; [_primaryResponders release]; @@ -81,7 +90,7 @@ - (void)handlePress:(nonnull FlutterUIPressProxy*)press NSAssert([_primaryResponders count] >= 0, @"At least one primary responder must be added."); __block auto weakSelf = [self getWeakPtr]; - __block int unreplied = [_primaryResponders count]; + __block NSUInteger unreplied = [self.primaryResponders count]; __block BOOL anyHandled = false; FlutterAsyncKeyCallback replyCallback = ^(BOOL handled) { unreplied--; @@ -102,7 +111,10 @@ - (void)handlePress:(nonnull FlutterUIPressProxy*)press // framework. Once the completeCallback is called, this run loop will exit // and the main one will resume. The completeCallback MUST be called, or // the app will get stuck in this run loop indefinitely. - CFRunLoopRun(); + // + // We need to run in this mode so that UIKit doesn't give us new + // events until we are done processing this one. + CFRunLoopRunInMode(fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, kDistantFuture, NO); break; } case UIPressPhaseChanged: diff --git a/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManagerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManagerTest.mm index 0eafd55b02f37..258684418b34c 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManagerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManagerTest.mm @@ -8,6 +8,7 @@ #import #include <_types/_uint32_t.h> +#include "flutter/fml/platform/darwin/message_loop_darwin.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h" @@ -59,7 +60,8 @@ - (bool)emptyNextResponder; namespace { -typedef void (^KeyCallbackSetter)(FlutterAsyncKeyCallback callback); +typedef void (^KeyCallbackSetter)(FlutterUIPressProxy* press, FlutterAsyncKeyCallback callback) + API_AVAILABLE(ios(13.4)); typedef BOOL (^BoolGetter)(); } // namespace @@ -100,11 +102,14 @@ - (id)checkKeyDownEvent:(UIKeyboardHIDUsage)keyCode API_AVAILABLE(ios(13.4)) { OCMStrictProtocolMock(@protocol(FlutterKeyPrimaryResponder)); OCMStub([mock handlePress:[OCMArg any] callback:[OCMArg any]]) .andDo((^(NSInvocation* invocation) { + FlutterUIPressProxy* press; FlutterAsyncKeyCallback callback; + [invocation getArgument:&press atIndex:2]; [invocation getArgument:&callback atIndex:3]; - CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^() { - callbackSetter(callback); - }); + CFRunLoopPerformBlock(CFRunLoopGetCurrent(), + fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, ^() { + callbackSetter(press, callback); + }); })); return mock; } @@ -149,7 +154,8 @@ - (void)testSinglePrimaryResponder API_AVAILABLE(ios(13.4)) { FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init]; __block BOOL primaryResponse = FALSE; __block int callbackCount = 0; - [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterAsyncKeyCallback callback) { + [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press, + FlutterAsyncKeyCallback callback) { callbackCount++; callback(primaryResponse); }]]; @@ -181,14 +187,16 @@ - (void)testDoublePrimaryResponder API_AVAILABLE(ios(13.4)) { __block BOOL callback1Response = FALSE; __block int callback1Count = 0; - [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterAsyncKeyCallback callback) { + [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press, + FlutterAsyncKeyCallback callback) { callback1Count++; callback(callback1Response); }]]; __block BOOL callback2Response = FALSE; __block int callback2Count = 0; - [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterAsyncKeyCallback callback) { + [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press, + FlutterAsyncKeyCallback callback) { callback2Count++; callback(callback2Response); }]]; @@ -242,7 +250,8 @@ - (void)testSingleSecondaryResponder API_AVAILABLE(ios(13.4)) { __block BOOL primaryResponse = FALSE; __block int callbackCount = 0; - [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterAsyncKeyCallback callback) { + [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press, + FlutterAsyncKeyCallback callback) { callbackCount++; callback(primaryResponse); }]]; @@ -291,4 +300,73 @@ - (void)testSingleSecondaryResponder API_AVAILABLE(ios(13.4)) { XCTAssertFalse(completeHandled); } +- (void)testEventsProcessedSequentially API_AVAILABLE(ios(13.4)) { + constexpr UIKeyboardHIDUsage keyId1 = (UIKeyboardHIDUsage)0x50; + constexpr UIKeyboardHIDUsage keyId2 = (UIKeyboardHIDUsage)0x51; + FlutterUIPressProxy* event1 = keyDownEvent(keyId1); + FlutterUIPressProxy* event2 = keyDownEvent(keyId2); + __block FlutterAsyncKeyCallback key1Callback; + __block FlutterAsyncKeyCallback key2Callback; + __block bool key1Handled = true; + __block bool key2Handled = true; + + FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init]; + [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press, + FlutterAsyncKeyCallback callback) { + if (press == event1) { + key1Callback = callback; + } else if (press == event2) { + key2Callback = callback; + } + }]]; + + // Add both presses into the main CFRunLoop queue + CFRunLoopTimerRef timer0 = CFRunLoopTimerCreateWithHandler( + kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, ^(CFRunLoopTimerRef timerRef) { + [manager handlePress:event1 + nextAction:^() { + key1Handled = false; + }]; + }); + CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer0, kCFRunLoopCommonModes); + CFRunLoopTimerRef timer1 = CFRunLoopTimerCreateWithHandler( + kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 1, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) { + // key1 should be completely finished by now + XCTAssertFalse(key1Handled); + [manager handlePress:event2 + nextAction:^() { + key2Handled = false; + }]; + // End the nested CFRunLoop + CFRunLoopStop(CFRunLoopGetCurrent()); + }); + CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer1, kCFRunLoopCommonModes); + + // Add the callbacks to the CFRunLoop with mode kMessageLoopCFRunLoopMode + // This allows them to interrupt the loop started within handlePress + CFRunLoopTimerRef timer2 = CFRunLoopTimerCreateWithHandler( + kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 2, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) { + // No processing should be done on key2 yet + XCTAssertTrue(key1Callback != nil); + XCTAssertTrue(key2Callback == nil); + key1Callback(false); + }); + CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer2, + fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode); + CFRunLoopTimerRef timer3 = CFRunLoopTimerCreateWithHandler( + kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 3, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) { + // Both keys should be processed by now + XCTAssertTrue(key1Callback != nil); + XCTAssertTrue(key2Callback != nil); + key2Callback(false); + }); + CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer3, + fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode); + + // Start a nested CFRunLoop so we can wait for both presses to complete before exiting the test + CFRunLoopRun(); + XCTAssertFalse(key2Handled); + XCTAssertFalse(key1Handled); +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h b/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h index a00c17ad89245..0c3fe202c6116 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h @@ -14,7 +14,7 @@ - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; -@property(nonatomic, readonly) NSURL* url; +@property(nonatomic, retain, readonly) NSURL* url; @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.mm b/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.mm index c9b124ba4e264..ccf1bbc70de2d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.mm @@ -95,7 +95,7 @@ - (void)publishServiceProtocolPort:(NSURL*)url { int err = DNSServiceRegister(&_dnsServiceRef, flags, interfaceIndex, FlutterObservatoryPublisher.serviceName.UTF8String, registrationType, domain, NULL, htons(port), txtData.length, txtData.bytes, - registrationCallback, NULL); + RegistrationCallback, NULL); if (err != 0) { FML_LOG(ERROR) << "Failed to register observatory port with mDNS with error " << err << "."; @@ -125,7 +125,7 @@ - (void)publishServiceProtocolPort:(NSURL*)url { -65570; #endif // __IPHONE_OS_VERSION_MAX_ALLOWED -static void DNSSD_API registrationCallback(DNSServiceRef sdRef, +static void DNSSD_API RegistrationCallback(DNSServiceRef sdRef, DNSServiceFlags flags, DNSServiceErrorType errorCode, const char* name, @@ -206,8 +206,8 @@ - (instancetype)initWithEnableObservatoryPublication:(BOOL)enableObservatoryPubl // uri comes in as something like 'http://127.0.0.1:XXXXX/' where XXXXX is the port // number. if (weak) { - NSURL* url = - [[NSURL alloc] initWithString:[NSString stringWithUTF8String:uri.c_str()]]; + NSURL* url = [[[NSURL alloc] + initWithString:[NSString stringWithUTF8String:uri.c_str()]] autorelease]; weak.get().url = url; if (weak.get().enableObservatoryPublication) { [[weak.get() delegate] publishServiceProtocolPort:url]; @@ -236,6 +236,10 @@ + (NSData*)createTxtData:(NSURL*)url { } - (void)dealloc { + // It will be destroyed and invalidate its weak pointers + // before any other members are destroyed. + _weakFactory.reset(); + [_delegate stopService]; [_url release]; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm b/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm index 345d3a36798cb..eb61a61ea8538 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm @@ -22,15 +22,13 @@ @implementation FlutterOverlayView - (instancetype)initWithFrame:(CGRect)frame { - @throw([NSException exceptionWithName:@"FlutterOverlayView must init or initWithContentsScale" - reason:nil - userInfo:nil]); + NSAssert(NO, @"FlutterOverlayView must init or initWithContentsScale"); + return nil; } - (instancetype)initWithCoder:(NSCoder*)aDecoder { - @throw([NSException exceptionWithName:@"FlutterOverlayView must init or initWithContentsScale" - reason:nil - userInfo:nil]); + NSAssert(NO, @"FlutterOverlayView must init or initWithContentsScale"); + return nil; } - (instancetype)init { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index 999f81d5bc618..cba52ffdc47aa 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -39,12 +39,6 @@ @implementation FlutterPlatformPlugin { fml::WeakPtr _engine; } -- (instancetype)init { - @throw([NSException exceptionWithName:@"FlutterPlatformPlugin must initWithEngine" - reason:nil - userInfo:nil]); -} - - (instancetype)initWithEngine:(fml::WeakPtr)engine { FML_DCHECK(engine) << "engine must be set"; self = [super init]; @@ -136,19 +130,21 @@ - (void)setSystemChromePreferredOrientations:(NSArray*)orientations { mask |= UIInterfaceOrientationMaskAll; } else { for (NSString* orientation in orientations) { - if ([orientation isEqualToString:@"DeviceOrientation.portraitUp"]) + if ([orientation isEqualToString:@"DeviceOrientation.portraitUp"]) { mask |= UIInterfaceOrientationMaskPortrait; - else if ([orientation isEqualToString:@"DeviceOrientation.portraitDown"]) + } else if ([orientation isEqualToString:@"DeviceOrientation.portraitDown"]) { mask |= UIInterfaceOrientationMaskPortraitUpsideDown; - else if ([orientation isEqualToString:@"DeviceOrientation.landscapeLeft"]) + } else if ([orientation isEqualToString:@"DeviceOrientation.landscapeLeft"]) { mask |= UIInterfaceOrientationMaskLandscapeLeft; - else if ([orientation isEqualToString:@"DeviceOrientation.landscapeRight"]) + } else if ([orientation isEqualToString:@"DeviceOrientation.landscapeRight"]) { mask |= UIInterfaceOrientationMaskLandscapeRight; + } } } - if (!mask) + if (!mask) { return; + } [[NSNotificationCenter defaultCenter] postNotificationName:@(kOrientationUpdateNotificationName) object:nil @@ -205,8 +201,9 @@ - (void)restoreSystemChromeSystemUIOverlays { - (void)setSystemChromeSystemUIOverlayStyle:(NSDictionary*)message { NSString* brightness = message[@"statusBarBrightness"]; - if (brightness == (id)[NSNull null]) + if (brightness == (id)[NSNull null]) { return; + } UIStatusBarStyle statusBarStyle; if ([brightness isEqualToString:@"Brightness.dark"]) { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 333d12f3f92c8..4556798a4ed39 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -611,7 +611,7 @@ UIView* overlay_view = layer->overlay_view.get(); // Set the size of the overlay view. - // This size is equal to the the device screen size. + // This size is equal to the device screen size. overlay_view.frame = flutter_view_.get().bounds; std::unique_ptr frame = layer->surface->AcquireFrame(frame_size_); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 0bcb1b483846b..a7d5a25a469b4 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -95,8 +95,6 @@ void OnPlatformViewSetViewportMetrics(const ViewportMetrics& metrics) override { void OnPlatformViewDispatchPlatformMessage(std::unique_ptr message) override {} void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr packet) override { } - void OnPlatformViewDispatchKeyDataPacket(std::unique_ptr packet, - std::function callback) override {} void OnPlatformViewDispatchSemanticsAction(int32_t id, SemanticsAction action, fml::MallocMapping args) override {} @@ -965,8 +963,9 @@ - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashin flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams_1)); flutterPlatformViewsController->CompositeEmbeddedView(2); + flutter::SurfaceFrame::FramebufferInfo framebuffer_info; auto mock_surface = std::make_unique( - nullptr, true, + nullptr, framebuffer_info, [](const flutter::SurfaceFrame& surface_frame, SkCanvas* canvas) { return false; }); XCTAssertFalse( flutterPlatformViewsController->SubmitFrame(nullptr, nullptr, std::move(mock_surface))); @@ -976,7 +975,7 @@ - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashin flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams_2)); flutterPlatformViewsController->CompositeEmbeddedView(2); auto mock_surface_submit_false = std::make_unique( - nullptr, true, + nullptr, framebuffer_info, [](const flutter::SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); XCTAssertTrue(flutterPlatformViewsController->SubmitFrame(nullptr, nullptr, std::move(mock_surface_submit_false))); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index f886bd0df7df3..8d42bc879488d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -182,67 +182,6 @@ class FlutterPlatformViewsController { 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_; - std::map>> factories_; - std::map>> views_; - std::map> touch_interceptors_; - // Mapping a platform view ID to the top most parent view (root_view) of a platform view. In - // |SubmitFrame|, root_views_ are added to flutter_view_ as child views. - // - // The platform view with the view ID is a child of the root view; If the platform view is not - // clipped, and no clipping view is added, the root view will be the intercepting view. - std::map> root_views_; - // Mapping a platform view ID to its latest composition params. - std::map current_composition_params_; - // 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_; - SkISize frame_size_; - - // The number of frames the rasterizer task runner will continue - // to run on the platform thread after no platform view is rendered. - // - // Note: this is an arbitrary number that attempts to account for cases - // where the platform view might be momentarily off the screen. - static const int kDefaultMergedLeaseDuration = 10; - - // Method channel `OnDispose` calls adds the views to be disposed to this set to be disposed on - // the next frame. - std::unordered_set views_to_dispose_; - - // A vector of embedded view IDs according to their composition order. - // The last ID in this vector belond to the that is composited on top of all others. - std::vector composition_order_; - - // The latest composition order that was presented in Present(). - std::vector active_composition_order_; - - // Only compoiste platform views in this set. - std::unordered_set views_to_recomposite_; - - // The FlutterPlatformViewGestureRecognizersBlockingPolicy for each type of platform view. - std::map - gesture_recognizers_blocking_policies; - - std::unique_ptr> weak_factory_; - - bool catransaction_added_ = false; - void OnCreate(FlutterMethodCall* call, FlutterResult& result); void OnDispose(FlutterMethodCall* call, FlutterResult& result); void OnAcceptGesture(FlutterMethodCall* call, FlutterResult& result); @@ -302,6 +241,67 @@ class FlutterPlatformViewsController { // Resets the state of the frame. void ResetFrameState(); + // 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_; + std::map>> factories_; + std::map>> views_; + std::map> touch_interceptors_; + // Mapping a platform view ID to the top most parent view (root_view) of a platform view. In + // |SubmitFrame|, root_views_ are added to flutter_view_ as child views. + // + // The platform view with the view ID is a child of the root view; If the platform view is not + // clipped, and no clipping view is added, the root view will be the intercepting view. + std::map> root_views_; + // Mapping a platform view ID to its latest composition params. + std::map current_composition_params_; + // 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_; + SkISize frame_size_; + + // The number of frames the rasterizer task runner will continue + // to run on the platform thread after no platform view is rendered. + // + // Note: this is an arbitrary number that attempts to account for cases + // where the platform view might be momentarily off the screen. + static const int kDefaultMergedLeaseDuration = 10; + + // Method channel `OnDispose` calls adds the views to be disposed to this set to be disposed on + // the next frame. + std::unordered_set views_to_dispose_; + + // A vector of embedded view IDs according to their composition order. + // The last ID in this vector belond to the that is composited on top of all others. + std::vector composition_order_; + + // The latest composition order that was presented in Present(). + std::vector active_composition_order_; + + // Only compoiste platform views in this set. + std::unordered_set views_to_recomposite_; + + // The FlutterPlatformViewGestureRecognizersBlockingPolicy for each type of platform view. + std::map + gesture_recognizers_blocking_policies; + + bool catransaction_added_ = false; + + // WeakPtrFactory must be the last member. + std::unique_ptr> weak_factory_; 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 a6d3d03653b17..c662a078f2a16 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -84,7 +84,7 @@ @implementation FlutterClippingMaskView { } - (instancetype)initWithFrame:(CGRect)frame { - if ([super initWithFrame:frame]) { + if (self = [super initWithFrame:frame]) { self.backgroundColor = UIColor.clearColor; } return self; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate.mm b/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate.mm index 424d1eaeff8df..2ea4fe2afe1ae 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate.mm @@ -71,7 +71,7 @@ - (void)dealloc { [super dealloc]; } -static BOOL isPowerOfTwo(NSUInteger x) { +static BOOL IsPowerOfTwo(NSUInteger x) { return x != 0 && (x & (x - 1)) == 0; } @@ -98,7 +98,7 @@ - (BOOL)hasPluginThatRespondsToSelector:(SEL)selector { - (void)addDelegate:(NSObject*)delegate { [_delegates addPointer:(__bridge void*)delegate]; - if (isPowerOfTwo([_delegates count])) { + if (IsPowerOfTwo([_delegates count])) { [_delegates compact]; } } @@ -248,6 +248,18 @@ - (void)application:(UIApplication*)application } } +- (void)application:(UIApplication*)application + didFailToRegisterForRemoteNotificationsWithError:(NSError*)error { + for (NSObject* delegate in _delegates) { + if (!delegate) { + continue; + } + if ([delegate respondsToSelector:_cmd]) { + [delegate application:application didFailToRegisterForRemoteNotificationsWithError:error]; + } + } +} + - (void)application:(UIApplication*)application didReceiveRemoteNotification:(NSDictionary*)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.mm index 0ff207e3b61db..4e11c4d19a708 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.mm @@ -20,13 +20,6 @@ @implementation FlutterRestorationPlugin { BOOL _restorationEnabled; } -- (instancetype)init { - @throw([NSException - exceptionWithName:@"FlutterRestorationPlugin must initWithChannel:restorationEnabled:" - reason:nil - userInfo:nil]); -} - - (instancetype)initWithChannel:(FlutterMethodChannel*)channel restorationEnabled:(BOOL)restorationEnabled { FML_DCHECK(channel) << "channel must be set"; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h b/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h index e5de3e0bab0b3..5a6a90f85bfd8 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h @@ -20,6 +20,8 @@ NS_ASSUME_NONNULL_BEGIN */ @interface FlutterSemanticsScrollView : UIScrollView +@property(nonatomic, assign, nullable) SemanticsObject* semanticsObject; + - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; - (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm b/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm index 2bdde791c62ca..87b9d8b00b5b9 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm @@ -6,10 +6,6 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" -@interface FlutterSemanticsScrollView () -@property(nonatomic, assign) SemanticsObject* semanticsObject; -@end - @implementation FlutterSemanticsScrollView - (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject { @@ -40,7 +36,7 @@ - (BOOL)isAccessibilityElement { self.contentSize.height > self.frame.size.height) { // In SwitchControl or VoiceControl, the isAccessibilityElement must return YES // in order to use scroll actions. - return !_semanticsObject.bridge->isVoiceOverRunning(); + return ![_semanticsObject bridge]->isVoiceOverRunning(); } else { return NO; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h b/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h index b6f2ac5ca975f..7b33539446d63 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h @@ -6,7 +6,9 @@ #define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTDELEGATE_H_ #import -#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterUIPressProxy.h" + +@class FlutterTextInputPlugin; +@class FlutterTextInputView; typedef NS_ENUM(NSInteger, FlutterTextInputAction) { FlutterTextInputActionUnspecified, @@ -29,18 +31,35 @@ typedef NS_ENUM(NSInteger, FlutterFloatingCursorDragState) { }; @protocol FlutterTextInputDelegate -- (void)handlePressEvent:(FlutterUIPressProxy*)press - nextAction:(void (^)())next API_AVAILABLE(ios(13.4)); -- (void)updateEditingClient:(int)client withState:(NSDictionary*)state; -- (void)updateEditingClient:(int)client withState:(NSDictionary*)state withTag:(NSString*)tag; -- (void)updateEditingClient:(int)client withDelta:(NSDictionary*)state; -- (void)performAction:(FlutterTextInputAction)action withClient:(int)client; -- (void)updateFloatingCursor:(FlutterFloatingCursorDragState)state +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + updateEditingClient:(int)client + withState:(NSDictionary*)state; +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + updateEditingClient:(int)client + withState:(NSDictionary*)state + withTag:(NSString*)tag; +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + updateEditingClient:(int)client + withDelta:(NSDictionary*)state; +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + performAction:(FlutterTextInputAction)action + withClient:(int)client; +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + updateFloatingCursor:(FlutterFloatingCursorDragState)state withClient:(int)client withPosition:(NSDictionary*)point; -- (void)showAutocorrectionPromptRectForStart:(NSUInteger)start - end:(NSUInteger)end - withClient:(int)client; +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + showAutocorrectionPromptRectForStart:(NSUInteger)start + end:(NSUInteger)end + withClient:(int)client; +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView showToolbar:(int)client; +- (void)flutterTextInputViewScribbleInteractionBegan:(FlutterTextInputView*)textInputView; +- (void)flutterTextInputViewScribbleInteractionFinished:(FlutterTextInputView*)textInputView; +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView + insertTextPlaceholderWithSize:(CGSize)size + withClient:(int)client; +- (void)flutterTextInputView:(FlutterTextInputView*)textInputView removeTextPlaceholder:(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 c17423f3a82ed..fcc3017ce5748 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h @@ -7,14 +7,33 @@ #import +#include "flutter/shell/platform/common/text_editing_delta.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterIndirectScribbleDelegate.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeySecondaryResponder.h" -#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextEditingDelta.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewResponder.h" -@interface FlutterTextInputPlugin : NSObject +typedef NS_ENUM(NSInteger, FlutterScribbleFocusStatus) { + FlutterScribbleFocusStatusUnfocused, + FlutterScribbleFocusStatusFocusing, + FlutterScribbleFocusStatusFocused, +}; + +typedef NS_ENUM(NSInteger, FlutterScribbleInteractionStatus) { + FlutterScribbleInteractionStatusNone, + FlutterScribbleInteractionStatusStarted, + FlutterScribbleInteractionStatusEnding, +}; + +@interface FlutterTextInputPlugin + : NSObject @property(nonatomic, assign) id textInputDelegate; +@property(nonatomic, assign) UIViewController* viewController; +@property(nonatomic, assign) id indirectScribbleDelegate; +@property(nonatomic, strong) + NSMutableDictionary* scribbleElements; - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; /** @@ -25,6 +44,13 @@ */ - (UIView*)textInputView; +/** + * These are used by the UIIndirectScribbleInteractionDelegate methods to handle focusing on the + * correct element. + */ +- (void)setupIndirectScribbleInteraction:(id)viewResponder; +- (void)resetViewResponder; + @end /** An indexed position in the buffer of a Flutter text editing widget. */ @@ -50,10 +76,41 @@ @interface FlutterTokenizer : UITextInputStringTokenizer @end +@interface FlutterTextSelectionRect : UITextSelectionRect + +@property(nonatomic, assign) CGRect rect; +@property(nonatomic) NSUInteger position; +@property(nonatomic, assign) NSWritingDirection writingDirection; +@property(nonatomic) BOOL containsStart; +@property(nonatomic) BOOL containsEnd; +@property(nonatomic) BOOL isVertical; + ++ (instancetype)selectionRectWithRectAndInfo:(CGRect)rect + position:(NSUInteger)position + writingDirection:(NSWritingDirection)writingDirection + containsStart:(BOOL)containsStart + containsEnd:(BOOL)containsEnd + isVertical:(BOOL)isVertical; + ++ (instancetype)selectionRectWithRect:(CGRect)rect position:(NSUInteger)position; + +- (instancetype)initWithRectAndInfo:(CGRect)rect + position:(NSUInteger)position + writingDirection:(NSWritingDirection)writingDirection + containsStart:(BOOL)containsStart + containsEnd:(BOOL)containsEnd + isVertical:(BOOL)isVertical; + +- (instancetype)init NS_UNAVAILABLE; +@end + +API_AVAILABLE(ios(13.0)) @interface FlutterTextPlaceholder : UITextPlaceholder +@end + #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG FLUTTER_DARWIN_EXPORT #endif -@interface FlutterTextInputView : UIView +@interface FlutterTextInputView : UIView // UITextInput @property(nonatomic, readonly) NSMutableString* text; @@ -80,5 +137,11 @@ FLUTTER_DARWIN_EXPORT @property(nonatomic, assign) id textInputDelegate; @property(nonatomic, assign) UIAccessibilityElement* backingTextInputAccessibilityObject; +// Scribble Support +@property(nonatomic, assign) id viewResponder; +@property(nonatomic) FlutterScribbleFocusStatus scribbleFocusStatus; +@property(nonatomic, strong) NSArray* selectionRects; +- (void)resetScribbleInteractionStatusIfEnding; + @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 cf77aff62b147..74dc1512443c7 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -72,35 +72,45 @@ // used when there's an in-app virtual keyboard. If // "TextInputType.none" is specified, disable the system // keyboard. -static BOOL shouldShowSystemKeyboard(NSDictionary* type) { +static BOOL ShouldShowSystemKeyboard(NSDictionary* type) { NSString* inputType = type[@"name"]; return ![inputType isEqualToString:@"TextInputType.none"]; } static UIKeyboardType ToUIKeyboardType(NSDictionary* type) { NSString* inputType = type[@"name"]; - if ([inputType isEqualToString:@"TextInputType.address"]) + if ([inputType isEqualToString:@"TextInputType.address"]) { return UIKeyboardTypeDefault; - if ([inputType isEqualToString:@"TextInputType.datetime"]) + } + if ([inputType isEqualToString:@"TextInputType.datetime"]) { return UIKeyboardTypeNumbersAndPunctuation; - if ([inputType isEqualToString:@"TextInputType.emailAddress"]) + } + if ([inputType isEqualToString:@"TextInputType.emailAddress"]) { return UIKeyboardTypeEmailAddress; - if ([inputType isEqualToString:@"TextInputType.multiline"]) + } + if ([inputType isEqualToString:@"TextInputType.multiline"]) { return UIKeyboardTypeDefault; - if ([inputType isEqualToString:@"TextInputType.name"]) + } + if ([inputType isEqualToString:@"TextInputType.name"]) { return UIKeyboardTypeNamePhonePad; + } if ([inputType isEqualToString:@"TextInputType.number"]) { - if ([type[@"signed"] boolValue]) + if ([type[@"signed"] boolValue]) { return UIKeyboardTypeNumbersAndPunctuation; - if ([type[@"decimal"] boolValue]) + } + if ([type[@"decimal"] boolValue]) { return UIKeyboardTypeDecimalPad; + } return UIKeyboardTypeNumberPad; } - if ([inputType isEqualToString:@"TextInputType.phone"]) + if ([inputType isEqualToString:@"TextInputType.phone"]) { return UIKeyboardTypePhonePad; - if ([inputType isEqualToString:@"TextInputType.text"]) + } + if ([inputType isEqualToString:@"TextInputType.text"]) { return UIKeyboardTypeDefault; - if ([inputType isEqualToString:@"TextInputType.url"]) + } + if ([inputType isEqualToString:@"TextInputType.url"]) { return UIKeyboardTypeURL; + } return UIKeyboardTypeDefault; } @@ -121,39 +131,51 @@ static UIReturnKeyType ToUIReturnKeyType(NSString* inputType) { // has "unspecified." These 2 terms seem to mean the same thing but we need // to pick just one. "unspecified" was chosen because "default" is often a // reserved word in languages with switch statements (dart, java, etc). - if ([inputType isEqualToString:@"TextInputAction.unspecified"]) + if ([inputType isEqualToString:@"TextInputAction.unspecified"]) { return UIReturnKeyDefault; + } - if ([inputType isEqualToString:@"TextInputAction.done"]) + if ([inputType isEqualToString:@"TextInputAction.done"]) { return UIReturnKeyDone; + } - if ([inputType isEqualToString:@"TextInputAction.go"]) + if ([inputType isEqualToString:@"TextInputAction.go"]) { return UIReturnKeyGo; + } - if ([inputType isEqualToString:@"TextInputAction.send"]) + if ([inputType isEqualToString:@"TextInputAction.send"]) { return UIReturnKeySend; + } - if ([inputType isEqualToString:@"TextInputAction.search"]) + if ([inputType isEqualToString:@"TextInputAction.search"]) { return UIReturnKeySearch; + } - if ([inputType isEqualToString:@"TextInputAction.next"]) + if ([inputType isEqualToString:@"TextInputAction.next"]) { return UIReturnKeyNext; + } - if (@available(iOS 9.0, *)) - if ([inputType isEqualToString:@"TextInputAction.continueAction"]) + if (@available(iOS 9.0, *)) { + if ([inputType isEqualToString:@"TextInputAction.continueAction"]) { return UIReturnKeyContinue; + } + } - if ([inputType isEqualToString:@"TextInputAction.join"]) + if ([inputType isEqualToString:@"TextInputAction.join"]) { return UIReturnKeyJoin; + } - if ([inputType isEqualToString:@"TextInputAction.route"]) + if ([inputType isEqualToString:@"TextInputAction.route"]) { return UIReturnKeyRoute; + } - if ([inputType isEqualToString:@"TextInputAction.emergencyCall"]) + if ([inputType isEqualToString:@"TextInputAction.emergencyCall"]) { return UIReturnKeyEmergencyCall; + } - if ([inputType isEqualToString:@"TextInputAction.newline"]) + if ([inputType isEqualToString:@"TextInputAction.newline"]) { return UIReturnKeyDefault; + } // Present default key if bad input type is given. return UIReturnKeyDefault; @@ -277,7 +299,7 @@ static UITextContentType ToUITextContentType(NSArray* hints) { // Retrieves the autofillId from an input field's configuration. Returns // nil if the field is nil and the input field is not a password field. -static NSString* autofillIdFromDictionary(NSDictionary* dictionary) { +static NSString* AutofillIdFromDictionary(NSDictionary* dictionary) { NSDictionary* autofill = dictionary[kAutofillProperties]; if (autofill) { return autofill[kAutofillId]; @@ -346,16 +368,17 @@ typedef NS_ENUM(NSInteger, FlutterAutofillType) { FlutterAutofillTypePassword, }; -static BOOL isFieldPasswordRelated(NSDictionary* configuration) { +static BOOL IsFieldPasswordRelated(NSDictionary* configuration) { if (@available(iOS 10.0, *)) { // Autofill is explicitly disabled if the id isn't present. - if (!autofillIdFromDictionary(configuration)) { + if (!AutofillIdFromDictionary(configuration)) { return NO; } BOOL isSecureTextEntry = [configuration[kSecureTextEntry] boolValue]; - if (isSecureTextEntry) + if (isSecureTextEntry) { return YES; + } NSDictionary* autofill = configuration[kAutofillProperties]; UITextContentType contentType = ToUITextContentType(autofill[kAutofillHints]); @@ -376,14 +399,14 @@ static BOOL isFieldPasswordRelated(NSDictionary* configuration) { return NO; } -static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) { +static FlutterAutofillType AutofillTypeOf(NSDictionary* configuration) { for (NSDictionary* field in configuration[kAssociatedAutofillFields]) { - if (isFieldPasswordRelated(field)) { + if (IsFieldPasswordRelated(field)) { return FlutterAutofillTypePassword; } } - if (isFieldPasswordRelated(configuration)) { + if (IsFieldPasswordRelated(configuration)) { return FlutterAutofillTypePassword; } @@ -397,6 +420,64 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) { return FlutterAutofillTypeNone; } +static BOOL IsApproximatelyEqual(float x, float y, float delta) { + return fabsf(x - y) <= delta; +} + +// Checks whether point should be considered closer to selectionRect compared to +// otherSelectionRect. +// +// If checkRightBoundary is set, the right-center point on selectionRect and +// otherSelectionRect will be used instead of the left-center point. +// +// This uses special (empirically determined using a 1st gen iPad pro, 9.7" model running +// iOS 14.7.1) logic for determining the closer rect, rather than a simple distance calculation. +// First, the closer vertical distance is determined. Within the closest y distance, if the point is +// above the bottom of the closest rect, the x distance will be minimized; however, if the point is +// below the bottom of the rect, the x value will be maximized. +static BOOL IsSelectionRectCloserToPoint(CGPoint point, + CGRect selectionRect, + CGRect otherSelectionRect, + BOOL checkRightBoundary) { + CGPoint pointForSelectionRect = + CGPointMake(selectionRect.origin.x + (checkRightBoundary ? selectionRect.size.width : 0), + selectionRect.origin.y + selectionRect.size.height * 0.5); + float yDist = fabs(pointForSelectionRect.y - point.y); + float xDist = fabs(pointForSelectionRect.x - point.x); + + CGPoint pointForOtherSelectionRect = + CGPointMake(otherSelectionRect.origin.x + (checkRightBoundary ? selectionRect.size.width : 0), + otherSelectionRect.origin.y + otherSelectionRect.size.height * 0.5); + float yDistOther = fabs(pointForOtherSelectionRect.y - point.y); + float xDistOther = fabs(pointForOtherSelectionRect.x - point.x); + + // This serves a similar purpose to IsApproximatelyEqual, allowing a little buffer before + // declaring something closer vertically to account for the small variations in size and position + // of SelectionRects, especially when dealing with emoji. + BOOL isCloserVertically = yDist < yDistOther - 1; + BOOL isEqualVertically = IsApproximatelyEqual(yDist, yDistOther, 1); + BOOL isAboveBottomOfLine = point.y <= selectionRect.origin.y + selectionRect.size.height; + BOOL isCloserHorizontally = xDist <= xDistOther; + BOOL isBelowBottomOfLine = point.y > selectionRect.origin.y + selectionRect.size.height; + BOOL isFartherToRight = + selectionRect.origin.x + (checkRightBoundary ? selectionRect.size.width : 0) > + otherSelectionRect.origin.x; + return (isCloserVertically || + (isEqualVertically && ((isAboveBottomOfLine && isCloserHorizontally) || + (isBelowBottomOfLine && isFartherToRight)))); +} + +// Checks whether Scribble features are possibly available – meaning this is an iPad running iOS +// 14 or higher. +static BOOL IsScribbleAvailable() { + if (@available(iOS 14.0, *)) { + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + return YES; + } + } + return NO; +} + #pragma mark - FlutterTextPosition @implementation FlutterTextPosition @@ -519,6 +600,71 @@ - (UITextRange*)lineEnclosingPosition:(UITextPosition*)position { @end +#pragma mark - FlutterTextSelectionRect + +@implementation FlutterTextSelectionRect + +@synthesize rect = _rect; +@synthesize writingDirection = _writingDirection; +@synthesize containsStart = _containsStart; +@synthesize containsEnd = _containsEnd; +@synthesize isVertical = _isVertical; + ++ (instancetype)selectionRectWithRectAndInfo:(CGRect)rect + position:(NSUInteger)position + writingDirection:(NSWritingDirection)writingDirection + containsStart:(BOOL)containsStart + containsEnd:(BOOL)containsEnd + isVertical:(BOOL)isVertical { + return [[[FlutterTextSelectionRect alloc] initWithRectAndInfo:rect + position:position + writingDirection:writingDirection + containsStart:containsStart + containsEnd:containsEnd + isVertical:isVertical] autorelease]; +} + ++ (instancetype)selectionRectWithRect:(CGRect)rect position:(NSUInteger)position { + return [[[FlutterTextSelectionRect alloc] initWithRectAndInfo:rect + position:position + writingDirection:UITextWritingDirectionNatural + containsStart:NO + containsEnd:NO + isVertical:NO] autorelease]; +} + +- (instancetype)initWithRectAndInfo:(CGRect)rect + position:(NSUInteger)position + writingDirection:(NSWritingDirection)writingDirection + containsStart:(BOOL)containsStart + containsEnd:(BOOL)containsEnd + isVertical:(BOOL)isVertical { + self = [super init]; + if (self) { + self.rect = rect; + self.position = position; + self.writingDirection = writingDirection; + self.containsStart = containsStart; + self.containsEnd = containsEnd; + self.isVertical = isVertical; + } + return self; +} + +@end + +#pragma mark - FlutterTextPlaceholder + +@implementation FlutterTextPlaceholder + +- (NSArray*)rects { + // Returning anything other than an empty array here seems to cause PencilKit to enter an + // infinite loop of allocating placeholders until the app crashes + return @[]; +} + +@end + // A FlutterTextInputView that masquerades as a UITextField, and forwards // selectors it can't respond to to a shared UITextField instance. // @@ -527,7 +673,7 @@ - (UITextRange*)lineEnclosingPosition:(UITextPosition*)position { // currently only support UITextFields, and password saving only supports // UITextFields and UITextViews, as of iOS 13.5. @interface FlutterSecureTextInputView : FlutterTextInputView -@property(nonatomic, strong, readonly) UITextField* textField; +@property(nonatomic, retain, readonly) UITextField* textField; @end @implementation FlutterSecureTextInputView { @@ -570,6 +716,7 @@ @interface FlutterTextInputView () @property(nonatomic, assign) CGRect markedRect; @property(nonatomic) BOOL isVisibleToAutofill; @property(nonatomic, assign) BOOL accessibilityEnabled; +@property(nonatomic, retain) UITextInteraction* textInteraction API_AVAILABLE(ios(13.0)); - (void)setEditableTransform:(NSArray*)matrix; @end @@ -580,6 +727,8 @@ @implementation FlutterTextInputView { FlutterTextRange* _selectedTextRange; UIInputViewController* _inputViewController; CGRect _cachedFirstRect; + FlutterScribbleInteractionStatus _scribbleInteractionStatus; + BOOL _hasPlaceholder; // Whether to show the system keyboard when this view // becomes the first responder. Typically set to false // when the app shows its own in-flutter keyboard. @@ -604,6 +753,7 @@ - (instancetype)init { _selectedTextRange = [[FlutterTextRange alloc] initWithNSRange:NSMakeRange(0, 0)]; _markedRect = kInvalidFirstRect; _cachedFirstRect = kInvalidFirstRect; + _scribbleInteractionStatus = FlutterScribbleInteractionStatusNone; // Initialize with the zero matrix which is not // an affine transform. _editableTransform = CATransform3D(); @@ -625,14 +775,11 @@ - (instancetype)init { _smartQuotesType = UITextSmartQuotesTypeYes; _smartDashesType = UITextSmartDashesTypeYes; } + _selectionRects = [[NSArray alloc] init]; - // This makes sure UITextSelectionView.interactionAssistant is not nil so - // UITextSelectionView has access to this view (and its bounds). Otherwise - // floating cursor breaks: https://github.com/flutter/flutter/issues/70267. - if (@available(iOS 13.0, *)) { - UITextInteraction* interaction = - [UITextInteraction textInteractionForMode:UITextInteractionModeEditable]; - interaction.textInput = self; + if (@available(iOS 14.0, *)) { + UIScribbleInteraction* interaction = + [[[UIScribbleInteraction alloc] initWithDelegate:self] autorelease]; [self addInteraction:interaction]; } } @@ -649,7 +796,7 @@ - (void)configureWithDictionary:(NSDictionary*)configuration { self.secureTextEntry = [configuration[kSecureTextEntry] boolValue]; self.enableDeltaModel = [configuration[kEnableDeltaModel] boolValue]; - _isSystemKeyboardEnabled = shouldShowSystemKeyboard(inputType); + _isSystemKeyboardEnabled = ShouldShowSystemKeyboard(inputType); self.keyboardType = ToUIKeyboardType(inputType); self.returnKeyType = ToUIReturnKeyType(configuration[kInputAction]); self.autocapitalizationType = ToUITextAutoCapitalizationType(configuration); @@ -678,7 +825,7 @@ - (void)configureWithDictionary:(NSDictionary*)configuration { ? UITextAutocorrectionTypeNo : UITextAutocorrectionTypeDefault; if (@available(iOS 10.0, *)) { - self.autofillId = autofillIdFromDictionary(configuration); + self.autofillId = AutofillIdFromDictionary(configuration); if (autofill == nil) { self.textContentType = @""; } else { @@ -696,13 +843,28 @@ - (UITextContentType)textContentType { return _textContentType; } +// Prevent UIKit from showing selection handles or highlights. This is needed +// because Scribble interactions require the view to have it's actual frame on +// the screen. +- (UIColor*)insertionPointColor { + return [UIColor clearColor]; +} + +- (UIColor*)selectionBarColor { + return [UIColor clearColor]; +} + +- (UIColor*)selectionHighlightColor { + return [UIColor clearColor]; +} + - (UIInputViewController*)inputViewController { if (_isSystemKeyboardEnabled) { return nil; } if (!_inputViewController) { - _inputViewController = [UIInputViewController new]; + _inputViewController = [[UIInputViewController alloc] init]; } return _inputViewController; } @@ -732,11 +894,16 @@ - (void)dealloc { [_tokenizer release]; [_autofillId release]; [_inputViewController release]; + [_selectionRects release]; + [_markedTextStyle release]; + [_textContentType release]; + [_textInteraction release]; [super dealloc]; } - (void)setTextInputClient:(int)client { _textInputClient = client; + _hasPlaceholder = NO; } - (void)setTextInputState:(NSDictionary*)state { @@ -766,8 +933,9 @@ - (void)setTextInputState:(NSDictionary*)state { [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]]; _selectionAffinity = _kTextAffinityDownstream; - if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)]) + if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)]) { _selectionAffinity = _kTextAffinityUpstream; + } [self.inputDelegate selectionDidChange:self]; } @@ -776,79 +944,27 @@ - (void)setTextInputState:(NSDictionary*)state { } } -// The documentation for presses* handlers (implemented below) is entirely -// unclear about how to handle the case where some, but not all, of the presses -// are handled here. I've elected to call super separately for each of the -// presses that aren't handled, but it's not clear if this is correct. It may be -// that iOS intends for us to either handle all or none of the presses, and pass -// the original set to super. I have not yet seen multiple presses in the set in -// the wild, however, so I suspect that the API is built for a tvOS remote or -// something, and perhaps only one ever appears in the set on iOS from a -// keyboard. +// Forward touches to the viewResponder to allow tapping inside the UITextField as normal. +- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { + _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused; + [self resetScribbleInteractionStatusIfEnding]; + [self.viewResponder touchesBegan:touches withEvent:event]; +} -// If you substantially change these presses overrides, consider also changing -// the similar ones in FlutterViewController. They need to be overridden in both -// places to capture keys both inside and outside of a text field, but have -// slightly different implmentations. - -- (void)pressesBegan:(NSSet*)presses - withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) { - if (@available(iOS 13.4, *)) { - for (UIPress* press in presses) { - [_textInputDelegate handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press - withEvent:event] - nextAction:^() { - [super pressesBegan:[NSSet setWithObject:press] withEvent:event]; - }]; - } - } else { - [super pressesBegan:presses withEvent:event]; - } +- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { + [self.viewResponder touchesMoved:touches withEvent:event]; } -- (void)pressesChanged:(NSSet*)presses - withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) { - if (@available(iOS 13.4, *)) { - for (UIPress* press in presses) { - [_textInputDelegate - handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press withEvent:event] - nextAction:^() { - [super pressesChanged:[NSSet setWithObject:press] withEvent:event]; - }]; - } - } else { - [super pressesChanged:presses withEvent:event]; - } +- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { + [self.viewResponder touchesEnded:touches withEvent:event]; } -- (void)pressesEnded:(NSSet*)presses - withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) { - if (@available(iOS 13.4, *)) { - for (UIPress* press in presses) { - [_textInputDelegate handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press - withEvent:event] - nextAction:^() { - [super pressesEnded:[NSSet setWithObject:press] withEvent:event]; - }]; - } - } else { - [super pressesEnded:presses withEvent:event]; - } +- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { + [self.viewResponder touchesCancelled:touches withEvent:event]; } -- (void)pressesCancelled:(NSSet*)presses - withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) { - if (@available(iOS 13.4, *)) { - for (UIPress* press in presses) { - [_textInputDelegate - handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press withEvent:event] - nextAction:^() { - [super pressesCancelled:[NSSet setWithObject:press] withEvent:event]; - }]; - } - } else { - [super pressesCancelled:presses withEvent:event]; - } +- (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches { + [self.viewResponder touchesEstimatedPropertiesUpdated:touches]; } // Extracts the selection information from the editing state dictionary. @@ -868,8 +984,8 @@ - (NSRange)clampSelectionFromBase:(int)selectionBase } - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text { - int start = MIN(MAX(range.location, 0), text.length); - int length = MIN(range.length, text.length - start); + NSUInteger start = MIN(MAX(range.location, 0), text.length); + NSUInteger length = MIN(range.length, text.length - start); return NSMakeRange(start, length); } @@ -883,9 +999,35 @@ - (BOOL)isVisibleToAutofill { // their frames to CGRectZero prevents ios autofill from taking them into // account. - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill { + // This probably needs to change (think it is getting overwritten by the updateSizeAndTransform + // stuff for now). self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero; } +#pragma mark UIScribbleInteractionDelegate + +- (void)scribbleInteractionWillBeginWriting:(UIScribbleInteraction*)interaction + API_AVAILABLE(ios(14.0)) { + _scribbleInteractionStatus = FlutterScribbleInteractionStatusStarted; + [_textInputDelegate flutterTextInputViewScribbleInteractionBegan:self]; +} + +- (void)scribbleInteractionDidFinishWriting:(UIScribbleInteraction*)interaction + API_AVAILABLE(ios(14.0)) { + _scribbleInteractionStatus = FlutterScribbleInteractionStatusEnding; + [_textInputDelegate flutterTextInputViewScribbleInteractionFinished:self]; +} + +- (BOOL)scribbleInteraction:(UIScribbleInteraction*)interaction + shouldBeginAtLocation:(CGPoint)location API_AVAILABLE(ios(14.0)) { + return YES; +} + +- (BOOL)scribbleInteractionShouldDelayFocus:(UIScribbleInteraction*)interaction + API_AVAILABLE(ios(14.0)) { + return NO; +} + #pragma mark - UIResponder Overrides - (BOOL)canBecomeFirstResponder { @@ -896,6 +1038,47 @@ - (BOOL)canBecomeFirstResponder { return _textInputClient != 0; } +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { + // When scribble is available, the FlutterTextInputView will display the native toolbar unless + // these text editing actions are disabled. + if (IsScribbleAvailable()) { + return NO; + } + if (action == @selector(paste:)) { + // Forbid pasting images, memojis, or other non-string content. + return [UIPasteboard generalPasteboard].string != nil; + } + + return [super canPerformAction:action withSender:sender]; +} + +#pragma mark - UIResponderStandardEditActions Overrides + +- (void)cut:(id)sender { + [UIPasteboard generalPasteboard].string = [self textInRange:_selectedTextRange]; + [self replaceRange:_selectedTextRange withText:@""]; +} + +- (void)copy:(id)sender { + [UIPasteboard generalPasteboard].string = [self textInRange:_selectedTextRange]; +} + +- (void)paste:(id)sender { + NSString* pasteboardString = [UIPasteboard generalPasteboard].string; + if (pasteboardString != nil) { + [self insertText:pasteboardString]; + } +} + +- (void)delete:(id)sender { + [self replaceRange:_selectedTextRange withText:@""]; +} + +- (void)selectAll:(id)sender { + [self setSelectedTextRange:[self textRangeFromPosition:[self beginningOfDocument] + toPosition:[self endOfDocument]]]; +} + #pragma mark - UITextInput Overrides - (id)tokenizer { @@ -928,10 +1111,22 @@ - (void)setSelectedTextRange:(UITextRange*)selectedTextRange { [self setSelectedTextRangeLocal:selectedTextRange]; if (_enableDeltaModel) { - [self updateEditingStateWithDelta:[FlutterTextEditingDelta deltaWithNonText:self.text]]; + [self updateEditingStateWithDelta:flutter::TextEditingDelta([self.text UTF8String])]; } else { [self updateEditingState]; } + + if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone || + _scribbleFocusStatus == FlutterScribbleFocusStatusFocused) { + NSAssert([selectedTextRange isKindOfClass:[FlutterTextRange class]], + @"Expected a FlutterTextRange for range (got %@).", [selectedTextRange class]); + FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange; + if (flutterTextRange.range.length > 0) { + [_textInputDelegate flutterTextInputView:self showToolbar:_textInputClient]; + } + } + + [self resetScribbleInteractionStatusIfEnding]; } - (id)insertDictationResultPlaceholder { @@ -950,8 +1145,8 @@ - (NSString*)textInRange:(UITextRange*)range { NSRange textRange = ((FlutterTextRange*)range).range; NSAssert(textRange.location != NSNotFound, @"Expected a valid text range."); // Sanitize the range to prevent going out of bounds. - int location = MIN(textRange.location, self.text.length); - int length = MIN(self.text.length - location, textRange.length); + NSUInteger location = MIN(textRange.location, self.text.length); + NSUInteger length = MIN(self.text.length - location, textRange.length); NSRange safeRange = NSMakeRange(location, length); return [self.text substringWithRange:safeRange]; } @@ -965,8 +1160,9 @@ - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text { // * reduce the length by the intersection length // * adjust the location by newLength - oldLength + intersectionLength NSRange intersectionRange = NSIntersectionRange(range, selectedRange); - if (range.location <= selectedRange.location) + if (range.location <= selectedRange.location) { selectedRange.location += text.length - range.length; + } if (intersectionRange.location != NSNotFound) { selectedRange.location += intersectionRange.length; selectedRange.length -= intersectionRange.length; @@ -984,11 +1180,13 @@ - (void)replaceRange:(UITextRange*)range withText:(NSString*)text { NSRange replaceRange = ((FlutterTextRange*)range).range; [self replaceRangeLocal:replaceRange withText:text]; if (_enableDeltaModel) { - [self updateEditingStateWithDelta:[FlutterTextEditingDelta - textEditingDelta:textBeforeChange - replacedRange:[self clampSelection:replaceRange - forText:textBeforeChange] - updatedText:text]]; + NSRange nextReplaceRange = [self clampSelection:replaceRange forText:textBeforeChange]; + [self updateEditingStateWithDelta:flutter::TextEditingDelta( + [textBeforeChange UTF8String], + flutter::TextRange( + nextReplaceRange.location, + nextReplaceRange.location + nextReplaceRange.length), + [text UTF8String])]; } else { [self updateEditingState]; } @@ -996,8 +1194,9 @@ - (void)replaceRange:(UITextRange*)range withText:(NSString*)text { - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text { if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) { - [self.textInputDelegate performAction:FlutterTextInputActionNewline - withClient:_textInputClient]; + [self.textInputDelegate flutterTextInputView:self + performAction:FlutterTextInputActionNewline + withClient:_textInputClient]; return YES; } @@ -1038,7 +1237,9 @@ - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)t break; } - [self.textInputDelegate performAction:action withClient:_textInputClient]; + [self.textInputDelegate flutterTextInputView:self + performAction:action + withClient:_textInputClient]; return NO; } @@ -1051,8 +1252,14 @@ - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelecte NSRange markedTextRange = ((FlutterTextRange*)self.markedTextRange).range; NSRange actualReplacedRange; - if (markedText == nil) + if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone || + _scribbleFocusStatus != FlutterScribbleFocusStatusUnfocused) { + return; + } + + if (markedText == nil) { markedText = @""; + } if (markedTextRange.length > 0) { // Replace text in the marked range with the new text. @@ -1075,22 +1282,25 @@ - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelecte rangeWithNSRange:[self clampSelection:selectedRange forText:self.text]]]; if (_enableDeltaModel) { - [self updateEditingStateWithDelta:[FlutterTextEditingDelta - textEditingDelta:textBeforeChange - replacedRange:[self clampSelection:actualReplacedRange - forText:textBeforeChange] - updatedText:markedText]]; + NSRange nextReplaceRange = [self clampSelection:actualReplacedRange forText:textBeforeChange]; + [self updateEditingStateWithDelta:flutter::TextEditingDelta( + [textBeforeChange UTF8String], + flutter::TextRange( + nextReplaceRange.location, + nextReplaceRange.location + nextReplaceRange.length), + [markedText UTF8String])]; } else { [self updateEditingState]; } } - (void)unmarkText { - if (!self.markedTextRange) + if (!self.markedTextRange) { return; + } self.markedTextRange = nil; if (_enableDeltaModel) { - [self updateEditingStateWithDelta:[FlutterTextEditingDelta deltaWithNonText:self.text]]; + [self updateEditingStateWithDelta:flutter::TextEditingDelta([self.text UTF8String])]; } else { [self updateEditingState]; } @@ -1130,12 +1340,18 @@ - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInte return nil; } + if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone) { + return [FlutterTextPosition positionWithIndex:newLocation]; + } + if (offset >= 0) { - for (NSInteger i = 0; i < offset && offsetPosition < self.text.length; ++i) + for (NSInteger i = 0; i < offset && offsetPosition < self.text.length; ++i) { offsetPosition = [self incrementOffsetPosition:offsetPosition]; + } } else { - for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) + for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) { offsetPosition = [self decrementOffsetPosition:offsetPosition]; + } } return [FlutterTextPosition positionWithIndex:offsetPosition]; } @@ -1165,10 +1381,12 @@ - (UITextPosition*)endOfDocument { - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other { NSUInteger positionIndex = ((FlutterTextPosition*)position).index; NSUInteger otherIndex = ((FlutterTextPosition*)other).index; - if (positionIndex < otherIndex) + if (positionIndex < otherIndex) { return NSOrderedAscending; - if (positionIndex > otherIndex) + } + if (positionIndex > otherIndex) { return NSOrderedDescending; + } return NSOrderedSame; } @@ -1272,7 +1490,6 @@ - (CGRect)firstRectForRange:(UITextRange*)range { @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]); NSAssert([range.end isKindOfClass:[FlutterTextPosition class]], @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]); - NSUInteger start = ((FlutterTextPosition*)range.start).index; NSUInteger end = ((FlutterTextPosition*)range.end).index; if (_markedTextRange != nil) { @@ -1297,10 +1514,32 @@ - (CGRect)firstRectForRange:(UITextRange*)range { return _cachedFirstRect; } - [self.textInputDelegate showAutocorrectionPromptRectForStart:start - end:end - withClient:_textInputClient]; - // TODO(cbracken) Implement. + if (_scribbleInteractionStatus == FlutterScribbleInteractionStatusNone && + _scribbleFocusStatus == FlutterScribbleFocusStatusUnfocused) { + [_textInputDelegate flutterTextInputView:self + showAutocorrectionPromptRectForStart:start + end:end + withClient:_textInputClient]; + } + + NSUInteger first = start; + if (end < start) { + first = end; + } + FlutterTextRange* textRange = [FlutterTextRange + rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))]; + for (NSUInteger i = 0; i < [_selectionRects count]; i++) { + BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first; + BOOL isLastSelectionRect = i + 1 == [_selectionRects count]; + BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.range.length > first; + BOOL nextSelectionRectIsAfterStartOfRange = + !isLastSelectionRect && _selectionRects[i + 1].position > first; + if (startsOnOrBeforeStartOfRange && + (endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) { + return _selectionRects[i].rect; + } + } + return CGRectZero; } @@ -1324,19 +1563,96 @@ - (CGRect)bounds { } - (UITextPosition*)closestPositionToPoint:(CGPoint)point { - // TODO(cbracken) Implement. - NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index; - return [FlutterTextPosition positionWithIndex:currentIndex]; + if ([_selectionRects count] == 0) { + NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]], + @"Expected a FlutterTextPosition for position (got %@).", + [_selectedTextRange.start class]); + NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index; + return [FlutterTextPosition positionWithIndex:currentIndex]; + } + + FlutterTextRange* range = [FlutterTextRange + rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))]; + return [self closestPositionToPoint:point withinRange:range]; } - (NSArray*)selectionRectsForRange:(UITextRange*)range { - // TODO(cbracken) Implement. - return @[]; + // At least in the simulator, swapping to the Japanese keyboard crashes the app as this method + // is called immediately with a UITextRange with a UITextPosition rather than FlutterTextPosition + // for the start and end. + if (![range.start isKindOfClass:[FlutterTextPosition class]]) { + return @[]; + } + NSAssert([range.start isKindOfClass:[FlutterTextPosition class]], + @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]); + NSAssert([range.end isKindOfClass:[FlutterTextPosition class]], + @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]); + NSUInteger start = ((FlutterTextPosition*)range.start).index; + NSUInteger end = ((FlutterTextPosition*)range.end).index; + NSMutableArray* rects = [[[NSMutableArray alloc] init] autorelease]; + for (NSUInteger i = 0; i < [_selectionRects count]; i++) { + if (_selectionRects[i].position >= start && _selectionRects[i].position <= end) { + float width = _selectionRects[i].rect.size.width; + if (start == end) { + width = 0; + } + CGRect rect = CGRectMake(_selectionRects[i].rect.origin.x, _selectionRects[i].rect.origin.y, + width, _selectionRects[i].rect.size.height); + FlutterTextSelectionRect* selectionRect = [FlutterTextSelectionRect + selectionRectWithRectAndInfo:rect + position:_selectionRects[i].position + writingDirection:UITextWritingDirectionNatural + containsStart:(i == 0) + containsEnd:(i == fml::RangeForCharactersInRange( + self.text, NSMakeRange(0, self.text.length)) + .length) + isVertical:NO]; + [rects addObject:selectionRect]; + } + } + return rects; } - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range { - // TODO(cbracken) Implement. - return range.start; + NSAssert([range.start isKindOfClass:[FlutterTextPosition class]], + @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]); + NSAssert([range.end isKindOfClass:[FlutterTextPosition class]], + @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]); + NSUInteger start = ((FlutterTextPosition*)range.start).index; + NSUInteger end = ((FlutterTextPosition*)range.end).index; + + NSUInteger _closestIndex = 0; + CGRect _closestRect = CGRectZero; + NSUInteger _closestPosition = 0; + for (NSUInteger i = 0; i < [_selectionRects count]; i++) { + NSUInteger position = _selectionRects[i].position; + if (position >= start && position <= end) { + BOOL isFirst = _closestIndex == 0; + if (isFirst || IsSelectionRectCloserToPoint(point, _selectionRects[i].rect, _closestRect, + /*checkRightBoundary=*/NO)) { + _closestIndex = i; + _closestRect = _selectionRects[i].rect; + _closestPosition = position; + } + } + } + + FlutterTextRange* textRange = [FlutterTextRange + rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))]; + + if ([_selectionRects count] > 0 && textRange.range.length == end) { + NSUInteger i = [_selectionRects count] - 1; + NSUInteger position = _selectionRects[i].position + 1; + if (position <= end) { + if (IsSelectionRectCloserToPoint(point, _selectionRects[i].rect, _closestRect, + /*checkRightBoundary=*/YES)) { + _closestIndex = [_selectionRects count]; + _closestPosition = position; + } + } + } + + return [FlutterTextPosition positionWithIndex:_closestPosition]; } - (UITextRange*)characterRangeAtPoint:(CGPoint)point { @@ -1354,36 +1670,53 @@ - (void)beginFloatingCursorAtPoint:(CGPoint)point { // ) // where // point = keyboardPanGestureRecognizer.translationInView(textInputView) + - // caretRectForPosition boundingBox = self.convertRect(bounds, fromView:textInputView) bounds - // = self._selectionClipRect ?? self.bounds + // caretRectForPosition boundingBox = self.convertRect(bounds, fromView:textInputView) + // bounds = self._selectionClipRect ?? self.bounds // - // It's tricky to provide accurate "bounds" and "caretRectForPosition" so it's preferred to bypass - // the clamping and implement the same clamping logic in the framework where we have easy access - // to the bounding box of the input field and the caret location. + // It's tricky to provide accurate "bounds" and "caretRectForPosition" so it's preferred to + // bypass the clamping and implement the same clamping logic in the framework where we have easy + // access to the bounding box of the input field and the caret location. // - // The current implementation returns kSpacePanBounds for "bounds" when "_isFloatingCursorActive" - // is true. kSpacePanBounds centers "caretRectForPosition" so the floating cursor has enough - // clearance in all directions to move around. + // The current implementation returns kSpacePanBounds for "bounds" when + // "_isFloatingCursorActive" is true. kSpacePanBounds centers "caretRectForPosition" so the + // floating cursor has enough clearance in all directions to move around. // // It seems impossible to use a negative "width" or "height", as the "convertRect" // call always turns a CGRect's negative dimensions into non-negative values, e.g., // (1, 2, -3, -4) would become (-2, -2, 3, 4). _isFloatingCursorActive = true; - [self.textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateStart + // This makes sure UITextSelectionView.interactionAssistant is not nil so + // UITextSelectionView has access to this view (and its bounds). Otherwise + // floating cursor breaks: https://github.com/flutter/flutter/issues/70267. + if (@available(iOS 13.0, *)) { + self.textInteraction = [UITextInteraction textInteractionForMode:UITextInteractionModeEditable]; + self.textInteraction.textInput = self; + [self addInteraction:_textInteraction]; + } + [self.textInputDelegate flutterTextInputView:self + updateFloatingCursor:FlutterFloatingCursorDragStateStart withClient:_textInputClient withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}]; } - (void)updateFloatingCursorAtPoint:(CGPoint)point { _isFloatingCursorActive = true; - [self.textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateUpdate + [self.textInputDelegate flutterTextInputView:self + updateFloatingCursor:FlutterFloatingCursorDragStateUpdate withClient:_textInputClient withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}]; } - (void)endFloatingCursor { _isFloatingCursorActive = false; - [self.textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateEnd + if (@available(iOS 13.0, *)) { + if (_textInteraction != NULL) { + [self removeInteraction:_textInteraction]; + self.textInteraction = NULL; + } + } + [self.textInputDelegate flutterTextInputView:self + updateFloatingCursor:FlutterFloatingCursorDragStateEnd withClient:_textInputClient withPosition:@{@"X" : @(0), @"Y" : @(0)}]; } @@ -1413,15 +1746,18 @@ - (void)updateEditingState { }; if (_textInputClient == 0 && _autofillId != nil) { - [self.textInputDelegate updateEditingClient:_textInputClient - withState:state - withTag:_autofillId]; + [self.textInputDelegate flutterTextInputView:self + updateEditingClient:_textInputClient + withState:state + withTag:_autofillId]; } else { - [self.textInputDelegate updateEditingClient:_textInputClient withState:state]; + [self.textInputDelegate flutterTextInputView:self + updateEditingClient:_textInputClient + withState:state]; } } -- (void)updateEditingStateWithDelta:(FlutterTextEditingDelta*)delta { +- (void)updateEditingStateWithDelta:(flutter::TextEditingDelta)delta { NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index; NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index; @@ -1434,10 +1770,10 @@ - (void)updateEditingStateWithDelta:(FlutterTextEditingDelta*)delta { } NSDictionary* deltaToFramework = @{ - @"oldText" : delta.oldText, - @"deltaText" : delta.deltaText, - @"deltaStart" : @(delta.deltaStart), - @"deltaEnd" : @(delta.deltaEnd), + @"oldText" : @(delta.old_text().c_str()), + @"deltaText" : @(delta.delta_text().c_str()), + @"deltaStart" : @(delta.delta_start()), + @"deltaEnd" : @(delta.delta_end()), @"selectionBase" : @(selectionBase), @"selectionExtent" : @(selectionExtent), @"selectionAffinity" : @(_selectionAffinity), @@ -1450,7 +1786,9 @@ - (void)updateEditingStateWithDelta:(FlutterTextEditingDelta*)delta { @"deltas" : @[ deltaToFramework ], }; - [self.textInputDelegate updateEditingClient:_textInputClient withDelta:deltas]; + [self.textInputDelegate flutterTextInputView:self + updateEditingClient:_textInputClient + withDelta:deltas]; } - (BOOL)hasText { @@ -1458,12 +1796,54 @@ - (BOOL)hasText { } - (void)insertText:(NSString*)text { + NSMutableArray* copiedRects = + [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]]; + NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]], + @"Expected a FlutterTextPosition for position (got %@).", + [_selectedTextRange.start class]); + NSUInteger insertPosition = ((FlutterTextPosition*)_selectedTextRange.start).index; + for (NSUInteger i = 0; i < [_selectionRects count]; i++) { + NSUInteger rectPosition = _selectionRects[i].position; + if (rectPosition == insertPosition) { + for (NSUInteger j = 0; j <= text.length; j++) { + [copiedRects + addObject:[FlutterTextSelectionRect selectionRectWithRect:_selectionRects[i].rect + position:rectPosition + j]]; + } + } else { + if (rectPosition > insertPosition) { + rectPosition = rectPosition + text.length; + } + [copiedRects addObject:[FlutterTextSelectionRect selectionRectWithRect:_selectionRects[i].rect + position:rectPosition]]; + } + } + + _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused; + [self resetScribbleInteractionStatusIfEnding]; + self.selectionRects = copiedRects; + [copiedRects release]; _selectionAffinity = _kTextAffinityDownstream; [self replaceRange:_selectedTextRange withText:text]; } +- (UITextPlaceholder*)insertTextPlaceholderWithSize:(CGSize)size API_AVAILABLE(ios(13.0)) { + [_textInputDelegate flutterTextInputView:self + insertTextPlaceholderWithSize:size + withClient:_textInputClient]; + _hasPlaceholder = YES; + return [[[FlutterTextPlaceholder alloc] init] autorelease]; +} + +- (void)removeTextPlaceholder:(UITextPlaceholder*)textPlaceholder API_AVAILABLE(ios(13.0)) { + _hasPlaceholder = NO; + [_textInputDelegate flutterTextInputView:self removeTextPlaceholder:_textInputClient]; +} + - (void)deleteBackward { _selectionAffinity = _kTextAffinityDownstream; + _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused; + [self resetScribbleInteractionStatusIfEnding]; // When deleting Thai vowel, _selectedTextRange has location // but does not have length, so we have to manually set it. @@ -1487,8 +1867,9 @@ - (void)deleteBackward { } } - if (!_selectedTextRange.isEmpty) + if (!_selectedTextRange.isEmpty) { [self replaceRange:_selectedTextRange withText:@""]; + } } - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(id)target { @@ -1510,6 +1891,12 @@ - (BOOL)accessibilityElementsHidden { return !_accessibilityEnabled; } +- (void)resetScribbleInteractionStatusIfEnding { + if (_scribbleInteractionStatus == FlutterScribbleInteractionStatusEnding) { + _scribbleInteractionStatus = FlutterScribbleInteractionStatusNone; + } +} + @end /** @@ -1570,8 +1957,9 @@ @interface FlutterTextInputPlugin () // The current password-autofillable input fields that have yet to be saved. @property(nonatomic, readonly) NSMutableDictionary* autofillContext; -@property(nonatomic, strong) FlutterTextInputView* activeView; -@property(nonatomic, strong) FlutterTextInputViewAccessibilityHider* inputHider; +@property(nonatomic, retain) FlutterTextInputView* activeView; +@property(nonatomic, retain) FlutterTextInputViewAccessibilityHider* inputHider; +@property(nonatomic, readonly) id viewResponder; @end @implementation FlutterTextInputPlugin { @@ -1586,6 +1974,7 @@ - (instancetype)init { if (self) { _autofillContext = [[NSMutableDictionary alloc] init]; _inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init]; + _scribbleElements = [[NSMutableDictionary alloc] init]; // Initialize activeView with a dummy view to keep tests // passing. This dummy view needs to be replace once the // framework initializes an input connection, and thus @@ -1606,6 +1995,7 @@ - (void)dealloc { autofillView.textInputDelegate = nil; } [_autofillContext release]; + [_scribbleElements release]; [super dealloc]; } @@ -1648,6 +2038,9 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else if ([method isEqualToString:kFinishAutofillContextMethod]) { [self triggerAutofillSave:[args boolValue]]; result(nil); + } else if ([method isEqualToString:@"TextInput.setSelectionRects"]) { + [self setSelectionRects:args]; + result(nil); } else { result(FlutterMethodNotImplemented); } @@ -1655,6 +2048,18 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary { [_activeView setEditableTransform:dictionary[@"transform"]]; + if (IsScribbleAvailable()) { + // This is necessary to set up where the scribble interactable element will be. + int leftIndex = 12; + int topIndex = 13; + _inputHider.frame = + CGRectMake([dictionary[@"transform"][leftIndex] intValue], + [dictionary[@"transform"][topIndex] intValue], [dictionary[@"width"] intValue], + [dictionary[@"height"] intValue]); + _activeView.frame = + CGRectMake(0, 0, [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]); + _activeView.tintColor = [UIColor clearColor]; + } } - (void)updateMarkedRect:(NSDictionary*)dictionary { @@ -1666,8 +2071,23 @@ - (void)updateMarkedRect:(NSDictionary*)dictionary { _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ? kInvalidFirstRect : rect; } +- (void)setSelectionRects:(NSArray*)rects { + NSMutableArray* rectsAsRect = + [[[NSMutableArray alloc] initWithCapacity:[rects count]] autorelease]; + for (NSUInteger i = 0; i < [rects count]; i++) { + NSArray* rect = rects[i]; + [rectsAsRect + addObject:[FlutterTextSelectionRect + selectionRectWithRect:CGRectMake([rect[0] floatValue], [rect[1] floatValue], + [rect[2] floatValue], [rect[3] floatValue]) + position:[rect[4] unsignedIntegerValue]]]; + } + _activeView.selectionRects = rectsAsRect; +} + - (void)showTextInput { _activeView.textInputDelegate = _textInputDelegate; + _activeView.viewResponder = _viewResponder; [self addToInputParentViewIfNeeded:_activeView]; // Adds a delay to prevent the text view from receiving accessibility // focus in case it is activated during semantics updates. @@ -1727,7 +2147,7 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur [self changeInputViewsAutofillVisibility:NO]; // Update the current active view. - switch (autofillTypeOf(configuration)) { + switch (AutofillTypeOf(configuration)) { case FlutterAutofillTypeNone: self.activeView = [self createInputViewWith:configuration]; break; @@ -1766,7 +2186,7 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur // views) to decide whether the IME's internal states should be reset. See: // https://github.com/flutter/flutter/issues/79031 . - (FlutterTextInputView*)createInputViewWith:(NSDictionary*)configuration { - NSString* autofillId = autofillIdFromDictionary(configuration); + NSString* autofillId = AutofillIdFromDictionary(configuration); if (autofillId) { [_autofillContext removeObjectForKey:autofillId]; } @@ -1776,8 +2196,8 @@ - (FlutterTextInputView*)createInputViewWith:(NSDictionary*)configuration { newView.textInputDelegate = _textInputDelegate; for (NSDictionary* field in configuration[kAssociatedAutofillFields]) { - NSString* autofillId = autofillIdFromDictionary(field); - if (autofillId && autofillTypeOf(field) == FlutterAutofillTypeNone) { + NSString* autofillId = AutofillIdFromDictionary(field); + if (autofillId && AutofillTypeOf(field) == FlutterAutofillTypeNone) { [_autofillContext removeObjectForKey:autofillId]; } } @@ -1788,7 +2208,7 @@ - (FlutterTextInputView*)updateAndShowAutofillViews:(NSArray*)fields focusedField:(NSDictionary*)focusedField isPasswordRelated:(BOOL)isPassword { FlutterTextInputView* focused = nil; - NSString* focusedId = autofillIdFromDictionary(focusedField); + NSString* focusedId = AutofillIdFromDictionary(focusedField); NSAssert(focusedId, @"autofillId must not be null for the focused field: %@", focusedField); if (!fields) { @@ -1799,10 +2219,10 @@ - (FlutterTextInputView*)updateAndShowAutofillViews:(NSArray*)fields } for (NSDictionary* field in fields) { - NSString* autofillId = autofillIdFromDictionary(field); + NSString* autofillId = AutofillIdFromDictionary(field); NSAssert(autofillId, @"autofillId must not be null for field: %@", field); - BOOL hasHints = autofillTypeOf(field) != FlutterAutofillTypeNone; + BOOL hasHints = AutofillTypeOf(field) != FlutterAutofillTypeNone; BOOL isFocused = [focusedId isEqualToString:autofillId]; if (isFocused) { @@ -1831,7 +2251,7 @@ - (FlutterTextInputView*)updateAndShowAutofillViews:(NSArray*)fields // for autofill purposes so they should not be reused for a different type of views). - (FlutterTextInputView*)getOrCreateAutofillableView:(NSDictionary*)field isPasswordAutofill:(BOOL)needsPasswordAutofill { - NSString* autofillId = autofillIdFromDictionary(field); + NSString* autofillId = AutofillIdFromDictionary(field); FlutterTextInputView* inputView = _autofillContext[autofillId]; if (!inputView) { inputView = @@ -1846,12 +2266,11 @@ - (FlutterTextInputView*)getOrCreateAutofillableView:(NSDictionary*)field } // The UIView to add FlutterTextInputViews to. -- (UIView*)keyWindow { - UIWindow* keyWindow = [UIApplication sharedApplication].keyWindow; - NSAssert(keyWindow != nullptr, - @"The application must have a key window since the keyboard client " +- (UIView*)hostView { + NSAssert(self.viewController.view != nullptr, + @"The application must have a HostView since the keyboard client " @"must be part of the responder chain to function"); - return keyWindow; + return self.viewController.view; } // The UIView to add FlutterTextInputViews to. @@ -1924,7 +2343,7 @@ - (void)addToInputParentViewIfNeeded:(FlutterTextInputView*)inputView { if (![inputView isDescendantOfView:_inputHider]) { [_inputHider addSubview:inputView]; } - UIView* parentView = self.keyWindow; + UIView* parentView = self.hostView; if (_inputHider.superview != parentView) { [parentView addSubview:_inputHider]; } @@ -1936,6 +2355,105 @@ - (void)setTextInputEditingState:(NSDictionary*)state { - (void)clearTextInputClient { [_activeView setTextInputClient:0]; + _activeView.frame = CGRectZero; +} + +#pragma mark UIIndirectScribbleInteractionDelegate + +- (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction + isElementFocused:(UIScribbleElementIdentifier)elementIdentifier + API_AVAILABLE(ios(14.0)) { + return _activeView.scribbleFocusStatus == FlutterScribbleFocusStatusFocused; +} + +- (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction + focusElementIfNeeded:(UIScribbleElementIdentifier)elementIdentifier + referencePoint:(CGPoint)focusReferencePoint + completion:(void (^)(UIResponder* focusedInput))completion + API_AVAILABLE(ios(14.0)) { + _activeView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing; + [_indirectScribbleDelegate flutterTextInputPlugin:self + focusElement:elementIdentifier + atPoint:focusReferencePoint + result:^(id _Nullable result) { + _activeView.scribbleFocusStatus = + FlutterScribbleFocusStatusFocused; + completion(_activeView); + }]; +} + +- (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction + shouldDelayFocusForElement:(UIScribbleElementIdentifier)elementIdentifier + API_AVAILABLE(ios(14.0)) { + return NO; +} + +- (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction + willBeginWritingInElement:(UIScribbleElementIdentifier)elementIdentifier + API_AVAILABLE(ios(14.0)) { +} + +- (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction + didFinishWritingInElement:(UIScribbleElementIdentifier)elementIdentifier + API_AVAILABLE(ios(14.0)) { +} + +- (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction + frameForElement:(UIScribbleElementIdentifier)elementIdentifier + API_AVAILABLE(ios(14.0)) { + NSValue* elementValue = [_scribbleElements objectForKey:elementIdentifier]; + if (elementValue == nil) { + return CGRectZero; + } + return [elementValue CGRectValue]; +} + +- (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction + requestElementsInRect:(CGRect)rect + completion: + (void (^)(NSArray* elements))completion + API_AVAILABLE(ios(14.0)) { + [_indirectScribbleDelegate + flutterTextInputPlugin:self + requestElementsInRect:rect + result:^(id _Nullable result) { + NSMutableArray* elements = + [[[NSMutableArray alloc] init] autorelease]; + if ([result isKindOfClass:[NSArray class]]) { + for (NSArray* elementArray in result) { + [elements addObject:elementArray[0]]; + [_scribbleElements + setObject:[NSValue + valueWithCGRect:CGRectMake( + [elementArray[1] floatValue], + [elementArray[2] floatValue], + [elementArray[3] floatValue], + [elementArray[4] floatValue])] + forKey:elementArray[0]]; + } + } + completion(elements); + }]; +} + +#pragma mark - Methods related to Scribble support + +- (void)setupIndirectScribbleInteraction:(id)viewResponder { + if (_viewResponder != viewResponder) { + if (@available(iOS 14.0, *)) { + UIView* parentView = viewResponder.view; + if (parentView != nil) { + UIIndirectScribbleInteraction* scribbleInteraction = [[[UIIndirectScribbleInteraction alloc] + initWithDelegate:(id)self] autorelease]; + [parentView addInteraction:scribbleInteraction]; + } + } + } + _viewResponder = viewResponder; +} + +- (void)resetViewResponder { + _viewResponder = nil; } #pragma mark - diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm index e8a992ee0a3c0..6289c07d1e996 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -9,9 +9,14 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" +#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" FLUTTER_ASSERT_ARC +@interface FlutterEngine () +- (nonnull FlutterTextInputPlugin*)textInputPlugin; +@end + @interface FlutterTextInputView () @property(nonatomic, copy) NSString* autofillId; - (void)setEditableTransform:(NSArray*)matrix; @@ -58,6 +63,8 @@ - (void)cleanUpViewHierarchy:(BOOL)includeActiveView clearText:(BOOL)clearText delayRemoval:(BOOL)delayRemoval; - (NSArray*)textInputViews; +- (UIView*)hostView; +- (void)addToInputParentViewIfNeeded:(FlutterTextInputView*)inputView; @end @interface FlutterTextInputPluginTest : XCTestCase @@ -68,6 +75,7 @@ @implementation FlutterTextInputPluginTest { NSDictionary* _passwordTemplate; id engine; FlutterTextInputPlugin* textInputPlugin; + FlutterViewController* viewController; } - (void)setUp { @@ -76,6 +84,11 @@ - (void)setUp { engine = OCMClassMock([FlutterEngine class]); textInputPlugin = [[FlutterTextInputPlugin alloc] init]; textInputPlugin.textInputDelegate = engine; + viewController = [FlutterViewController new]; + textInputPlugin.viewController = viewController; + + // Clear pasteboard between tests. + UIPasteboard.generalPasteboard.items = @[]; } - (void)tearDown { @@ -87,6 +100,7 @@ - (void)tearDown { [textInputPlugin cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO]; [[[[textInputPlugin textInputView] superview] subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)]; + viewController = nil; [super tearDown]; } @@ -213,7 +227,62 @@ - (void)testAutocorrectionPromptRectAppears { [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; // Verify behavior. - OCMVerify([engine showAutocorrectionPromptRectForStart:0 end:1 withClient:0]); + OCMVerify([engine flutterTextInputView:inputView + showAutocorrectionPromptRectForStart:0 + end:1 + withClient:0]); +} + +- (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble { + if (@available(iOS 14.0, *)) { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero]; + inputView.textInputDelegate = engine; + + __block int callCount = 0; + OCMStub([engine flutterTextInputView:inputView + showAutocorrectionPromptRectForStart:0 + end:1 + withClient:0]) + .andDo(^(NSInvocation* invocation) { + callCount++; + }); + + [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; + // showAutocorrectionPromptRectForStart fires in response to firstRectForRange + XCTAssertEqual(callCount, 1); + + UIScribbleInteraction* scribbleInteraction = + [[UIScribbleInteraction alloc] initWithDelegate:inputView]; + + [inputView scribbleInteractionWillBeginWriting:scribbleInteraction]; + [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; + // showAutocorrectionPromptRectForStart does not fire in response to setMarkedText during a + // scribble interaction.firstRectForRange + XCTAssertEqual(callCount, 1); + + [inputView scribbleInteractionDidFinishWriting:scribbleInteraction]; + [inputView resetScribbleInteractionStatusIfEnding]; + [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; + // showAutocorrectionPromptRectForStart fires in response to firstRectForRange. + XCTAssertEqual(callCount, 2); + + inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing; + [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; + // showAutocorrectionPromptRectForStart does not fire in response to firstRectForRange during a + // scribble-initiated focus. + XCTAssertEqual(callCount, 2); + + inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused; + [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; + // showAutocorrectionPromptRectForStart does not fire in response to firstRectForRange after a + // scribble-initiated focus. + XCTAssertEqual(callCount, 2); + + inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused; + [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; + // showAutocorrectionPromptRectForStart fires in response to firstRectForRange. + XCTAssertEqual(callCount, 3); + } } - (void)testTextRangeFromPositionMatchesUITextViewBehavior { @@ -247,6 +316,45 @@ - (void)testTextInRange { XCTAssertEqual(substring.length, 0ul); } +- (void)testStandardEditActions { + NSDictionary* config = self.mutableTemplateCopy; + [self setClientId:123 configuration:config]; + NSArray* inputFields = self.installedInputViews; + FlutterTextInputView* inputView = inputFields[0]; + + [inputView insertText:@"aaaa"]; + [inputView selectAll:nil]; + [inputView cut:nil]; + [inputView insertText:@"bbbb"]; + XCTAssertTrue([inputView canPerformAction:@selector(paste:) withSender:nil]); + [inputView paste:nil]; + [inputView selectAll:nil]; + [inputView copy:nil]; + [inputView paste:nil]; + [inputView selectAll:nil]; + [inputView delete:nil]; + [inputView paste:nil]; + [inputView paste:nil]; + + UITextRange* range = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 30)]; + NSString* substring = [inputView textInRange:range]; + XCTAssertEqualObjects(substring, @"bbbbaaaabbbbaaaa"); +} + +- (void)testPastingNonTextDisallowed { + NSDictionary* config = self.mutableTemplateCopy; + [self setClientId:123 configuration:config]; + NSArray* inputFields = self.installedInputViews; + FlutterTextInputView* inputView = inputFields[0]; + + UIPasteboard.generalPasteboard.color = UIColor.redColor; + XCTAssertNil(UIPasteboard.generalPasteboard.string); + XCTAssertFalse([inputView canPerformAction:@selector(paste:) withSender:nil]); + [inputView paste:nil]; + + XCTAssertEqualObjects(inputView.text, @""); +} + - (void)testNoZombies { // Regression test for https://github.com/flutter/flutter/issues/62501. FlutterSecureTextInputView* passwordView = [[FlutterSecureTextInputView alloc] init]; @@ -304,6 +412,21 @@ - (void)ensureOnlyActiveViewCanBecomeFirstResponder { } } +- (void)testPropagatePressEventsToNextResponder { + NSDictionary* config = self.mutableTemplateCopy; + [self setClientId:123 configuration:config]; + + FlutterTextInputView* currentView = textInputPlugin.activeView; + UIView* mockCurrentView = OCMPartialMock(currentView); + + [mockCurrentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil] + withEvent:OCMClassMock([UIPressesEvent class])]; + + // The event should be propagated to the next responder instead + // of the engine. + OCMVerify([mockCurrentView nextResponder]); +} + #pragma mark - TextEditingDelta tests - (void)testTextEditingDeltasAreGeneratedOnTextInput { FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; @@ -311,7 +434,7 @@ - (void)testTextEditingDeltasAreGeneratedOnTextInput { inputView.enableDeltaModel = YES; __block int updateCount = 0; - OCMStub([engine updateEditingClient:0 withDelta:[OCMArg isNotNil]]) + OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]]) .andDo(^(NSInvocation* invocation) { updateCount++; }); @@ -322,82 +445,91 @@ - (void)testTextEditingDeltasAreGeneratedOnTextInput { // Verify correct delta is generated. OCMVerify([engine - updateEditingClient:0 - withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] isEqualToString:@""]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] - isEqualToString:@"text to insert"]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 0) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 0); - }]]); + flutterTextInputView:inputView + updateEditingClient:0 + withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] isEqualToString:@""]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] + isEqualToString:@"text to insert"]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 0) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 0); + }]]); [inputView deleteBackward]; XCTAssertEqual(updateCount, 2); OCMVerify([engine - updateEditingClient:0 - withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] - isEqualToString:@"text to insert"]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] isEqualToString:@""]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 13) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 14); - }]]); + flutterTextInputView:inputView + updateEditingClient:0 + withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] + isEqualToString:@"text to insert"]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] + isEqualToString:@""]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 13) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 14); + }]]); inputView.selectedTextRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]; XCTAssertEqual(updateCount, 3); OCMVerify([engine - updateEditingClient:0 - withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] - isEqualToString:@"text to inser"]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] isEqualToString:@""]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == -1) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == -1); - }]]); + flutterTextInputView:inputView + updateEditingClient:0 + withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] + isEqualToString:@"text to inser"]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] + isEqualToString:@""]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == -1) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == -1); + }]]); [inputView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)] withText:@"replace text"]; XCTAssertEqual(updateCount, 4); OCMVerify([engine - updateEditingClient:0 - withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] - isEqualToString:@"text to inser"]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] - isEqualToString:@"replace text"]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 0) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 1); - }]]); + flutterTextInputView:inputView + updateEditingClient:0 + withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] + isEqualToString:@"text to inser"]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] + isEqualToString:@"replace text"]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 0) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 1); + }]]); [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)]; XCTAssertEqual(updateCount, 5); OCMVerify([engine - updateEditingClient:0 - withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] - isEqualToString:@"replace textext to inser"]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] - isEqualToString:@"marked text"]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 12) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 12); - }]]); + flutterTextInputView:inputView + updateEditingClient:0 + withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] + isEqualToString:@"replace textext to inser"]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] + isEqualToString:@"marked text"]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 12) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 12); + }]]); [inputView unmarkText]; XCTAssertEqual(updateCount, 6); OCMVerify([engine - updateEditingClient:0 - withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] - isEqualToString:@"replace textmarked textext to inser"]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] isEqualToString:@""]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == -1) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == -1); - }]]); + flutterTextInputView:inputView + updateEditingClient:0 + withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] + isEqualToString:@"replace textmarked textext to inser"]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] + isEqualToString:@""]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == -1) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == -1); + }]]); } - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement { @@ -406,7 +538,7 @@ - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement { inputView.enableDeltaModel = YES; __block int updateCount = 0; - OCMStub([engine updateEditingClient:0 withDelta:[OCMArg isNotNil]]) + OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]]) .andDo(^(NSInvocation* invocation) { updateCount++; }); @@ -423,15 +555,16 @@ - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement { XCTAssertEqual(updateCount, 2); OCMVerify([engine - updateEditingClient:0 - withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] - isEqualToString:@"Some initial text"]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] - isEqualToString:@"new marked text."]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 13) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 17); - }]]); + flutterTextInputView:inputView + updateEditingClient:0 + withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] + isEqualToString:@"Some initial text"]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] + isEqualToString:@"new marked text."]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 13) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 17); + }]]); } - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion { @@ -440,7 +573,7 @@ - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion { inputView.enableDeltaModel = YES; __block int updateCount = 0; - OCMStub([engine updateEditingClient:0 withDelta:[OCMArg isNotNil]]) + OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]]) .andDo(^(NSInvocation* invocation) { updateCount++; }); @@ -457,15 +590,16 @@ - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion { XCTAssertEqual(updateCount, 2); OCMVerify([engine - updateEditingClient:0 - withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] - isEqualToString:@"Some initial text"]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] - isEqualToString:@"text."]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 13) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 17); - }]]); + flutterTextInputView:inputView + updateEditingClient:0 + withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] + isEqualToString:@"Some initial text"]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] + isEqualToString:@"text."]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 13) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 17); + }]]); } - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion { @@ -474,7 +608,7 @@ - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion { inputView.enableDeltaModel = YES; __block int updateCount = 0; - OCMStub([engine updateEditingClient:0 withDelta:[OCMArg isNotNil]]) + OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]]) .andDo(^(NSInvocation* invocation) { updateCount++; }); @@ -491,15 +625,16 @@ - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion { XCTAssertEqual(updateCount, 2); OCMVerify([engine - updateEditingClient:0 - withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] - isEqualToString:@"Some initial text"]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] - isEqualToString:@"tex"]) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 13) && - ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 17); - }]]); + flutterTextInputView:inputView + updateEditingClient:0 + withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([[state[@"deltas"] objectAtIndex:0][@"oldText"] + isEqualToString:@"Some initial text"]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaText"] + isEqualToString:@"tex"]) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 13) && + ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 17); + }]]); } #pragma mark - EditingState tests @@ -509,7 +644,7 @@ - (void)testUITextInputCallsUpdateEditingStateOnce { inputView.textInputDelegate = engine; __block int updateCount = 0; - OCMStub([engine updateEditingClient:0 withState:[OCMArg isNotNil]]) + OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]]) .andDo(^(NSInvocation* invocation) { updateCount++; }); @@ -541,7 +676,7 @@ - (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce { inputView.enableDeltaModel = YES; __block int updateCount = 0; - OCMStub([engine updateEditingClient:0 withDelta:[OCMArg isNotNil]]) + OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]]) .andDo(^(NSInvocation* invocation) { updateCount++; }); @@ -572,7 +707,7 @@ - (void)testTextChangesDoNotTriggerUpdateEditingClient { inputView.textInputDelegate = engine; __block int updateCount = 0; - OCMStub([engine updateEditingClient:0 withState:[OCMArg isNotNil]]) + OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]]) .andDo(^(NSInvocation* invocation) { updateCount++; }); @@ -614,7 +749,7 @@ - (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta { inputView.enableDeltaModel = YES; __block int updateCount = 0; - OCMStub([engine updateEditingClient:0 withDelta:[OCMArg isNotNil]]) + OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]]) .andDo(^(NSInvocation* invocation) { updateCount++; }); @@ -655,7 +790,7 @@ - (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls { inputView.textInputDelegate = engine; __block int updateCount = 0; - OCMStub([engine updateEditingClient:0 withState:[OCMArg isNotNil]]) + OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]]) .andDo(^(NSInvocation* invocation) { updateCount++; }); @@ -673,6 +808,56 @@ - (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls { XCTAssertEqual(updateCount, 2); } +- (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient { + if (@available(iOS 14.0, *)) { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + inputView.textInputDelegate = engine; + + __block int updateCount = 0; + OCMStub([engine flutterTextInputView:inputView + updateEditingClient:0 + withState:[OCMArg isNotNil]]) + .andDo(^(NSInvocation* invocation) { + updateCount++; + }); + + [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)]; + // updateEditingClient fires in response to setMarkedText. + XCTAssertEqual(updateCount, 1); + + UIScribbleInteraction* scribbleInteraction = + [[UIScribbleInteraction alloc] initWithDelegate:inputView]; + + [inputView scribbleInteractionWillBeginWriting:scribbleInteraction]; + [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)]; + // updateEditingClient does not fire in response to setMarkedText during a scribble interaction. + XCTAssertEqual(updateCount, 1); + + [inputView scribbleInteractionDidFinishWriting:scribbleInteraction]; + [inputView resetScribbleInteractionStatusIfEnding]; + [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)]; + // updateEditingClient fires in response to setMarkedText. + XCTAssertEqual(updateCount, 2); + + inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing; + [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)]; + // updateEditingClient does not fire in response to setMarkedText during a scribble-initiated + // focus. + XCTAssertEqual(updateCount, 2); + + inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused; + [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)]; + // updateEditingClient does not fire in response to setMarkedText after a scribble-initiated + // focus. + XCTAssertEqual(updateCount, 2); + + inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused; + [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)]; + // updateEditingClient fires in response to setMarkedText. + XCTAssertEqual(updateCount, 3); + } +} + - (void)testUpdateEditingClientNegativeSelection { FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; inputView.textInputDelegate = engine; @@ -687,30 +872,33 @@ - (void)testUpdateEditingClientNegativeSelection { @"selectionExtent" : @-1 }]; [inputView updateEditingState]; - OCMVerify([engine updateEditingClient:0 - withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([state[@"selectionBase"] intValue]) == 0 && - ([state[@"selectionExtent"] intValue] == 0); - }]]); + OCMVerify([engine flutterTextInputView:inputView + updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 0); + }]]); // Returns (0, 0) when either end goes below 0. [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}]; [inputView updateEditingState]; - OCMVerify([engine updateEditingClient:0 - withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([state[@"selectionBase"] intValue]) == 0 && - ([state[@"selectionExtent"] intValue] == 0); - }]]); + OCMVerify([engine flutterTextInputView:inputView + updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 0); + }]]); [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}]; [inputView updateEditingState]; - OCMVerify([engine updateEditingClient:0 - withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([state[@"selectionBase"] intValue]) == 0 && - ([state[@"selectionExtent"] intValue] == 0); - }]]); + OCMVerify([engine flutterTextInputView:inputView + updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 0); + }]]); } - (void)testUpdateEditingClientSelectionClamping { @@ -725,11 +913,12 @@ - (void)testUpdateEditingClientSelectionClamping { [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}]; [inputView updateEditingState]; - OCMVerify([engine updateEditingClient:0 - withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([state[@"selectionBase"] intValue]) == 0 && - ([state[@"selectionExtent"] intValue] == 0); - }]]); + OCMVerify([engine flutterTextInputView:inputView + updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 0); + }]]); // Needs clamping. [inputView setTextInputState:@{ @@ -739,21 +928,23 @@ - (void)testUpdateEditingClientSelectionClamping { }]; [inputView updateEditingState]; - OCMVerify([engine updateEditingClient:0 - withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([state[@"selectionBase"] intValue]) == 0 && - ([state[@"selectionExtent"] intValue] == 9); - }]]); + OCMVerify([engine flutterTextInputView:inputView + updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 9); + }]]); // No clamping needed, but in reverse direction. [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}]; [inputView updateEditingState]; - OCMVerify([engine updateEditingClient:0 - withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([state[@"selectionBase"] intValue]) == 0 && - ([state[@"selectionExtent"] intValue] == 1); - }]]); + OCMVerify([engine flutterTextInputView:inputView + updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 1); + }]]); // Both ends need clamping. [inputView setTextInputState:@{ @@ -762,11 +953,12 @@ - (void)testUpdateEditingClientSelectionClamping { @"selectionExtent" : @9999 }]; [inputView updateEditingState]; - OCMVerify([engine updateEditingClient:0 - withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { - return ([state[@"selectionBase"] intValue]) == 9 && - ([state[@"selectionExtent"] intValue] == 9); - }]]); + OCMVerify([engine flutterTextInputView:inputView + updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 9 && + ([state[@"selectionExtent"] intValue] == 9); + }]]); } #pragma mark - UITextInput methods - Tests @@ -816,6 +1008,130 @@ - (void)testUpdateFirstRectForRange { XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range])); } +- (void)testFirstRectForRangeReturnsCorrectSelectionRect { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + [inputView setTextInputState:@{@"text" : @"COMPOSING"}]; + + FlutterTextRange* range = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)]; + CGRect testRect = CGRectMake(100, 100, 100, 100); + [inputView setSelectionRects:@[ + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U], + [FlutterTextSelectionRect selectionRectWithRect:testRect position:1U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 200, 100, 100) position:2U], + ]]; + XCTAssertTrue(CGRectEqualToRect(testRect, [inputView firstRectForRange:range])); + + [inputView setTextInputState:@{@"text" : @"COM"}]; + FlutterTextRange* rangeOutsideBounds = [FlutterTextRange rangeWithNSRange:NSMakeRange(3, 1)]; + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds])); +} + +- (void)testClosestPositionToPoint { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + [inputView setTextInputState:@{@"text" : @"COMPOSING"}]; + + // Minimize the vertical distance from the center of the rects first + [inputView setSelectionRects:@[ + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:1U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 200, 100, 100) position:2U], + ]]; + CGPoint point = CGPointMake(150, 150); + XCTAssertEqual(1U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).index); + + // Then, if the point is above the bottom of the closest rects vertically, get the closest x + // origin + [inputView setSelectionRects:@[ + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:1U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:2U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:3U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 200, 100, 100) position:4U], + ]]; + point = CGPointMake(125, 150); + XCTAssertEqual(2U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).index); + + // However, if the point is below the bottom of the closest rects vertically, get the position + // farthest to the right + [inputView setSelectionRects:@[ + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:1U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:2U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:3U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 300, 100, 100) position:4U], + ]]; + point = CGPointMake(125, 201); + XCTAssertEqual(3U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).index); + + // Also check a point at the right edge of the last selection rect + [inputView setSelectionRects:@[ + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:1U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:2U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:3U], + ]]; + point = CGPointMake(125, 250); + XCTAssertEqual(4U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).index); +} + +- (void)testSelectionRectsForRange { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + [inputView setTextInputState:@{@"text" : @"COMPOSING"}]; + + CGRect testRect0 = CGRectMake(100, 100, 100, 100); + CGRect testRect1 = CGRectMake(200, 200, 100, 100); + [inputView setSelectionRects:@[ + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U], + [FlutterTextSelectionRect selectionRectWithRect:testRect0 position:1U], + [FlutterTextSelectionRect selectionRectWithRect:testRect1 position:2U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 300, 100, 100) position:3U], + ]]; + + // Returns the matching rects within a range + FlutterTextRange* range = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)]; + XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect)); + XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect)); + XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]); + + // Returns a 0 width rect for a 0-length range + range = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 0)]; + XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]); + XCTAssertTrue(CGRectEqualToRect( + CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height), + [inputView selectionRectsForRange:range][0].rect)); +} + +- (void)testClosestPositionToPointWithinRange { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + [inputView setTextInputState:@{@"text" : @"COMPOSING"}]; + + // Do not return a position before the start of the range + [inputView setSelectionRects:@[ + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:1U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:2U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:3U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 200, 100, 100) position:4U], + ]]; + CGPoint point = CGPointMake(125, 150); + FlutterTextRange* range = [[FlutterTextRange rangeWithNSRange:NSMakeRange(3, 2)] copy]; + XCTAssertEqual( + 3U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index); + + // Do not return a position after the end of the range + [inputView setSelectionRects:@[ + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:1U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:2U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:3U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 200, 100, 100) position:4U], + ]]; + point = CGPointMake(125, 150); + range = [[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)] copy]; + XCTAssertEqual( + 1U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index); +} + #pragma mark - Floating Cursor - Tests - (void)testInputViewsHaveUIInteractions { @@ -873,6 +1189,52 @@ - (void)testBoundsForFloatingCursor { XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds)); } +#pragma mark - UIKeyInput Overrides - Tests + +- (void)testInsertTextAddsPlaceholderSelectionRects { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + [inputView + setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}]; + + FlutterTextSelectionRect* first = + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U]; + FlutterTextSelectionRect* second = + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:1U]; + FlutterTextSelectionRect* third = + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 200, 100, 100) position:2U]; + FlutterTextSelectionRect* fourth = + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 300, 100, 100) position:3U]; + [inputView setSelectionRects:@[ first, second, third, fourth ]]; + + // Inserts additional selection rects at the selection start + [inputView insertText:@"in"]; + NSArray* selectionRects = + [inputView selectionRectsForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 6)]]; + XCTAssertEqual(6U, [selectionRects count]); + + XCTAssertEqual(first.position, ((FlutterTextSelectionRect*)selectionRects[0]).position); + XCTAssertTrue(CGRectEqualToRect(first.rect, ((FlutterTextSelectionRect*)selectionRects[0]).rect)); + + XCTAssertEqual(second.position, ((FlutterTextSelectionRect*)selectionRects[1]).position); + XCTAssertTrue( + CGRectEqualToRect(second.rect, ((FlutterTextSelectionRect*)selectionRects[1]).rect)); + + XCTAssertEqual(second.position + 1, ((FlutterTextSelectionRect*)selectionRects[2]).position); + XCTAssertTrue( + CGRectEqualToRect(second.rect, ((FlutterTextSelectionRect*)selectionRects[2]).rect)); + + XCTAssertEqual(second.position + 2, ((FlutterTextSelectionRect*)selectionRects[3]).position); + XCTAssertTrue( + CGRectEqualToRect(second.rect, ((FlutterTextSelectionRect*)selectionRects[3]).rect)); + + XCTAssertEqual(third.position + 2, ((FlutterTextSelectionRect*)selectionRects[4]).position); + XCTAssertTrue(CGRectEqualToRect(third.rect, ((FlutterTextSelectionRect*)selectionRects[4]).rect)); + + XCTAssertEqual(fourth.position + 2, ((FlutterTextSelectionRect*)selectionRects[5]).position); + XCTAssertTrue( + CGRectEqualToRect(fourth.rect, ((FlutterTextSelectionRect*)selectionRects[5]).rect)); +} + #pragma mark - Autofill - Utilities - (NSMutableDictionary*)mutablePasswordTemplateCopy { @@ -1140,7 +1502,10 @@ - (void)testAutofillInputViews { [self ensureOnlyActiveViewCanBecomeFirstResponder]; // Verify behavior. - OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil] withTag:@"field2"]); + OCMVerify([engine flutterTextInputView:inactiveView + updateEditingClient:0 + withState:[OCMArg isNotNil] + withTag:@"field2"]); } - (void)testPasswordAutofillHack { @@ -1321,8 +1686,11 @@ - (void)testFlutterTokenizerCanParseLines { } - (void)testFlutterTextInputPluginRetainsFlutterTextInputView { + FlutterViewController* flutterViewController = [FlutterViewController new]; FlutterTextInputPlugin* myInputPlugin = [[FlutterTextInputPlugin alloc] init]; myInputPlugin.textInputDelegate = engine; + myInputPlugin.viewController = flutterViewController; + __weak UIView* activeView; @autoreleasepool { FlutterMethodCall* setClientCall = [FlutterMethodCall @@ -1345,4 +1713,19 @@ - (void)testFlutterTextInputPluginRetainsFlutterTextInputView { XCTAssertNotNil(activeView); } +- (void)testFlutterTextInputPluginHostViewNilCrash { + FlutterTextInputPlugin* myInputPlugin = [[FlutterTextInputPlugin alloc] init]; + myInputPlugin.viewController = nil; + XCTAssertThrows([myInputPlugin hostView], @"Throws exception if host view is nil"); +} + +- (void)testFlutterTextInputPluginHostViewNotNil { + FlutterViewController* flutterViewController = [FlutterViewController new]; + FlutterEngine* flutterEngine = [[FlutterEngine alloc] init]; + [flutterEngine runWithEntrypoint:nil]; + flutterEngine.viewController = flutterViewController; + XCTAssertNotNil(flutterEngine.textInputPlugin.viewController); + XCTAssertNotNil([flutterEngine.textInputPlugin hostView]); +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterUmbrellaImport.m b/shell/platform/darwin/ios/framework/Source/FlutterUmbrellaImport.m index ea73b2d1e050f..849a05e5a052a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterUmbrellaImport.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterUmbrellaImport.m @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// FLUTTER_NOLINT: https://github.com/flutter/flutter/issues/93360 + // The only point of this file is to ensure that the Flutter framework umbrella header can be // cleanly imported from an Objective-C translation unit. The target that uses this file copies the // headers to a path that simulates how users would actually import the framework outside of the diff --git a/shell/platform/darwin/ios/framework/Source/FlutterView.mm b/shell/platform/darwin/ios/framework/Source/FlutterView.mm index 36b1bbb346e3e..ff42e3162d289 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterView.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterView.mm @@ -22,21 +22,18 @@ @implementation FlutterView { } - (instancetype)init { - @throw([NSException exceptionWithName:@"FlutterView must initWithDelegate" - reason:nil - userInfo:nil]); + NSAssert(NO, @"FlutterView must initWithDelegate"); + return nil; } - (instancetype)initWithFrame:(CGRect)frame { - @throw([NSException exceptionWithName:@"FlutterView must initWithDelegate" - reason:nil - userInfo:nil]); + NSAssert(NO, @"FlutterView must initWithDelegate"); + return nil; } - (instancetype)initWithCoder:(NSCoder*)aDecoder { - @throw([NSException exceptionWithName:@"FlutterView must initWithDelegate" - reason:nil - userInfo:nil]); + NSAssert(NO, @"FlutterView must initWithDelegate"); + return nil; } - (instancetype)initWithDelegate:(id)delegate opaque:(BOOL)opaque { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 5666f336af88b..1cacf2f54bb6c 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -69,6 +69,12 @@ @interface FlutterViewController () _scrollView; fml::scoped_nsobject _pointerInteraction API_AVAILABLE(ios(13.4)); fml::scoped_nsobject _panGestureRecognizer API_AVAILABLE(ios(13.4)); + fml::scoped_nsobject _keyboardAnimationView; MouseState _mouseState; } @@ -126,28 +133,25 @@ - (instancetype)initWithEngine:(FlutterEngine*)engine NSAssert(engine != nil, @"Engine is required"); 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]); - _weakFactory = std::make_unique>(self); - _ongoingTouches.reset([[NSMutableSet alloc] init]); - - [self performCommonViewControllerInitialization]; - [engine setViewController:self]; + [self setupEngine:engine]; } return self; } +- (instancetype)initWithEngineGroup:(FlutterEngineGroup*)engineGroup + options:(nullable FlutterEngineGroupOptions*)options + nibName:(nullable NSString*)nibName + bundle:(nullable NSBundle*)nibBundle { + NSAssert(engineGroup != nil, @"FlutterEngineGroup is required"); + self = [super initWithNibName:nibName bundle:nibBundle]; + if (self) { + FlutterEngine* engine = [engineGroup makeEngineWithOptions:options]; + [self setupEngine:engine]; + } + return self; +} + - (instancetype)initWithProject:(FlutterDartProject*)project nibName:(NSString*)nibName bundle:(NSBundle*)nibBundle { @@ -191,6 +195,26 @@ - (instancetype)init { return [self initWithProject:nil nibName:nil bundle:nil]; } +- (void)setupEngine:(FlutterEngine*)engine { + _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]); + _weakFactory = std::make_unique>(self); + _ongoingTouches.reset([[NSMutableSet alloc] init]); + + [self performCommonViewControllerInitialization]; + [engine setViewController:self]; +} + - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project initialRoute:(nullable NSString*)initialRoute { // Need the project to get settings for the view. Initializing it here means @@ -235,8 +259,9 @@ - (void)setViewOpaque:(BOOL)value { #pragma mark - Common view controller initialization tasks - (void)performCommonViewControllerInitialization { - if (_initialized) + if (_initialized) { return; + } _initialized = YES; @@ -414,7 +439,7 @@ - (void)loadView { _scrollView.reset(scrollView); } -static void sendFakeTouchEvent(FlutterEngine* engine, +static void SendFakeTouchEvent(FlutterEngine* engine, CGPoint location, flutter::PointerData::Change change) { const CGFloat scale = [UIScreen mainScreen].scale; @@ -435,8 +460,8 @@ - (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView { return NO; } CGPoint statusBarPoint = CGPointZero; - sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kDown); - sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kUp); + SendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kDown); + SendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kUp); return NO; } @@ -542,6 +567,10 @@ - (UIView*)splashScreenView { return _splashScreenView.get(); } +- (UIView*)keyboardAnimationView { + return _keyboardAnimationView.get(); +} + - (BOOL)loadDefaultSplashScreenView { NSString* launchscreenName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"]; @@ -636,7 +665,7 @@ - (void)viewDidLoad { // Register internal plugins before starting the engine. [self addInternalPlugins]; - [_engine.get() launchEngine:nil libraryURI:nil]; + [_engine.get() launchEngine:nil libraryURI:nil entrypointArgs:nil]; [_engine.get() setViewController:self]; _engineNeedsLaunch = NO; } @@ -660,21 +689,24 @@ - (void)viewDidLoad { } - (void)addInternalPlugins { - [self.keyboardManager release]; - self.keyboardManager = [[FlutterKeyboardManager alloc] init]; + self.keyboardManager = [[[FlutterKeyboardManager alloc] init] autorelease]; + fml::WeakPtr weakSelf = [self getWeakPtr]; FlutterSendKeyEvent sendEvent = ^(const FlutterKeyEvent& event, FlutterKeyEventCallback callback, void* userData) { - [_engine.get() sendKeyEvent:event callback:callback userData:userData]; + [weakSelf.get()->_engine.get() sendKeyEvent:event callback:callback userData:userData]; }; [self.keyboardManager addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc] initWithSendEvent:sendEvent]]; - [self.keyboardManager addPrimaryResponder:[[FlutterChannelKeyResponder alloc] - initWithChannel:self.engine.keyEventChannel]]; - [self.keyboardManager addSecondaryResponder:self.engine.textInputPlugin]; + FlutterChannelKeyResponder* responder = [[[FlutterChannelKeyResponder alloc] + initWithChannel:self.engine.keyEventChannel] autorelease]; + [self.keyboardManager addPrimaryResponder:responder]; + FlutterTextInputPlugin* textInputPlugin = self.engine.textInputPlugin; + if (textInputPlugin != nil) { + [self.keyboardManager addSecondaryResponder:textInputPlugin]; + } } - (void)removeInternalPlugins { - [self.keyboardManager release]; self.keyboardManager = nil; } @@ -719,6 +751,8 @@ - (void)viewWillDisappear:(BOOL)animated { - (void)viewDidDisappear:(BOOL)animated { TRACE_EVENT0("flutter", "viewDidDisappear"); if ([_engine.get() viewController] == self) { + [self invalidateDisplayLink]; + [self ensureViewportMetricsIsCorrect]; [self surfaceUpdated:NO]; [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.paused"]; [self flushOngoingTouches]; @@ -771,8 +805,14 @@ - (void)deregisterNotifications { } - (void)dealloc { + // It will be destroyed and invalidate its weak pointers + // before any other members are destroyed. + _weakFactory.reset(); + [self removeInternalPlugins]; [self deregisterNotifications]; + + [_displayLink release]; [super dealloc]; } @@ -780,13 +820,16 @@ - (void)dealloc { - (void)applicationBecameActive:(NSNotification*)notification { TRACE_EVENT0("flutter", "applicationBecameActive"); - if (_viewportMetrics.physical_width) + self.view.accessibilityElementsHidden = NO; + if (_viewportMetrics.physical_width) { [self surfaceUpdated:YES]; + } [self goToApplicationLifecycle:@"AppLifecycleState.resumed"]; } - (void)applicationWillResignActive:(NSNotification*)notification { TRACE_EVENT0("flutter", "applicationWillResignActive"); + self.view.accessibilityElementsHidden = YES; [self goToApplicationLifecycle:@"AppLifecycleState.inactive"]; } @@ -1102,29 +1145,115 @@ - (void)keyboardWillChangeFrame:(NSNotification*)notification { } } + // Ignore keyboard notifications if engine’s viewController is not current viewController. + if ([_engine.get() viewController] != self) { + return; + } + CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGRect screenRect = [[UIScreen mainScreen] bounds]; + // Get the animation duration + NSTimeInterval duration = + [[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + // 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; + self.targetViewInsetBottom = bottom * scale; } else { - _viewportMetrics.physical_view_inset_bottom = 0; + self.targetViewInsetBottom = 0; } - - [self updateViewportMetrics]; + [self startKeyBoardAnimation:duration]; } - (void)keyboardWillBeHidden:(NSNotification*)notification { - _viewportMetrics.physical_view_inset_bottom = 0; - [self updateViewportMetrics]; + // When keyboard hide, the keyboardWillChangeFrame function will be called to update viewport + // metrics. So do not call [self updateViewportMetrics] here again. +} + +- (void)startKeyBoardAnimation:(NSTimeInterval)duration { + // If current physical_view_inset_bottom == targetViewInsetBottom,do nothing. + if (_viewportMetrics.physical_view_inset_bottom == self.targetViewInsetBottom) { + return; + } + + // When call this method first time, + // initialize the keyboardAnimationView to get animation interpolation during animation. + if ([self keyboardAnimationView] == nil) { + UIView* keyboardAnimationView = [[UIView alloc] init]; + [keyboardAnimationView setHidden:YES]; + _keyboardAnimationView.reset(keyboardAnimationView); + } + + if ([self keyboardAnimationView].superview == nil) { + [self.view addSubview:[self keyboardAnimationView]]; + } + + // Remove running animation when start another animation. + // After calling this line,the old display link will invalidate. + [[self keyboardAnimationView].layer removeAllAnimations]; + + // Set animation begin value. + [self keyboardAnimationView].frame = + CGRectMake(0, _viewportMetrics.physical_view_inset_bottom, 0, 0); + + // Invalidate old display link if the old animation is not complete + [self invalidateDisplayLink]; + + self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onDisplayLink)]; + [self.displayLink addToRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes]; + __block CADisplayLink* currentDisplayLink = self.displayLink; + + [UIView animateWithDuration:duration + animations:^{ + // Set end value. + [self keyboardAnimationView].frame = CGRectMake(0, self.targetViewInsetBottom, 0, 0); + } + completion:^(BOOL finished) { + if (self.displayLink == currentDisplayLink) { + [self invalidateDisplayLink]; + } + if (finished) { + [self removeKeyboardAnimationView]; + [self ensureViewportMetricsIsCorrect]; + } + }]; +} + +- (void)invalidateDisplayLink { + [self.displayLink invalidate]; +} + +- (void)removeKeyboardAnimationView { + if ([self keyboardAnimationView].superview != nil) { + [[self keyboardAnimationView] removeFromSuperview]; + } +} + +- (void)ensureViewportMetricsIsCorrect { + if (_viewportMetrics.physical_view_inset_bottom != self.targetViewInsetBottom) { + // Make sure the `physical_view_inset_bottom` is the target value. + _viewportMetrics.physical_view_inset_bottom = self.targetViewInsetBottom; + [self updateViewportMetrics]; + } +} + +- (void)onDisplayLink { + if ([self keyboardAnimationView].superview == nil) { + // Ensure the keyboardAnimationView is in view hierarchy when animation running. + [self.view addSubview:[self keyboardAnimationView]]; + } + if ([self keyboardAnimationView].layer.presentationLayer) { + CGFloat value = [self keyboardAnimationView].layer.presentationLayer.frame.origin.y; + _viewportMetrics.physical_view_inset_bottom = value; + [self updateViewportMetrics]; + } } - (void)handlePressEvent:(FlutterUIPressProxy*)press @@ -1156,7 +1285,8 @@ - (void)pressesBegan:(NSSet*)presses withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) { if (@available(iOS 13.4, *)) { for (UIPress* press in presses) { - [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press withEvent:event] + [self handlePressEvent:[[[FlutterUIPressProxy alloc] initWithPress:press + withEvent:event] autorelease] nextAction:^() { [super pressesBegan:[NSSet setWithObject:press] withEvent:event]; }]; @@ -1170,7 +1300,8 @@ - (void)pressesChanged:(NSSet*)presses withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) { if (@available(iOS 13.4, *)) { for (UIPress* press in presses) { - [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press withEvent:event] + [self handlePressEvent:[[[FlutterUIPressProxy alloc] initWithPress:press + withEvent:event] autorelease] nextAction:^() { [super pressesChanged:[NSSet setWithObject:press] withEvent:event]; }]; @@ -1184,7 +1315,8 @@ - (void)pressesEnded:(NSSet*)presses withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) { if (@available(iOS 13.4, *)) { for (UIPress* press in presses) { - [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press withEvent:event] + [self handlePressEvent:[[[FlutterUIPressProxy alloc] initWithPress:press + withEvent:event] autorelease] nextAction:^() { [super pressesEnded:[NSSet setWithObject:press] withEvent:event]; }]; @@ -1198,7 +1330,8 @@ - (void)pressesCancelled:(NSSet*)presses withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) { if (@available(iOS 13.4, *)) { for (UIPress* press in presses) { - [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press withEvent:event] + [self handlePressEvent:[[[FlutterUIPressProxy alloc] initWithPress:press + withEvent:event] autorelease] nextAction:^() { [super pressesCancelled:[NSSet setWithObject:press] withEvent:event]; }]; @@ -1310,8 +1443,9 @@ - (void)onAccessibilityStatusChanged:(NSNotification*)notification { #else _isVoiceOverRunning = UIAccessibilityIsVoiceOverRunning(); bool enabled = _isVoiceOverRunning || UIAccessibilityIsSwitchControlRunning(); - if (enabled) + if (enabled) { flags |= static_cast(flutter::AccessibilityFeatureFlag::kAccessibleNavigation); + } platformView->SetSemanticsEnabled(enabled || UIAccessibilityIsSpeakScreenEnabled()); platformView->SetAccessibilityFeatures(flags); #endif @@ -1357,32 +1491,33 @@ - (CGFloat)textScaleFactor { // We compute the scale as relative difference from size L (large, the default size), where // L is assumed to have scale 1.0. - if ([category isEqualToString:UIContentSizeCategoryExtraSmall]) + if ([category isEqualToString:UIContentSizeCategoryExtraSmall]) { return xs / l; - else if ([category isEqualToString:UIContentSizeCategorySmall]) + } else if ([category isEqualToString:UIContentSizeCategorySmall]) { return s / l; - else if ([category isEqualToString:UIContentSizeCategoryMedium]) + } else if ([category isEqualToString:UIContentSizeCategoryMedium]) { return m / l; - else if ([category isEqualToString:UIContentSizeCategoryLarge]) + } else if ([category isEqualToString:UIContentSizeCategoryLarge]) { return 1.0; - else if ([category isEqualToString:UIContentSizeCategoryExtraLarge]) + } else if ([category isEqualToString:UIContentSizeCategoryExtraLarge]) { return xl / l; - else if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge]) + } else if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge]) { return xxl / l; - else if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) + } else if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) { return xxxl / l; - else if ([category isEqualToString:UIContentSizeCategoryAccessibilityMedium]) + } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityMedium]) { return ax1 / l; - else if ([category isEqualToString:UIContentSizeCategoryAccessibilityLarge]) + } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityLarge]) { return ax2 / l; - else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) + } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) { return ax3 / l; - else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) + } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) { return ax4 / l; - else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) + } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) { return ax5 / l; - else + } else { return 1.0; + } } - (BOOL)isAlwaysUse24HourFormat { @@ -1494,8 +1629,8 @@ - (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString*)channe binaryMessageHandler:handler]; } -- (void)cleanupConnection:(FlutterBinaryMessengerConnection)connection { - [_engine.get().binaryMessenger cleanupConnection:connection]; +- (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection { + [_engine.get().binaryMessenger cleanUpConnection:connection]; } #pragma mark - FlutterTextureRegistry @@ -1624,6 +1759,7 @@ - (void)scrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) - (void)encodeRestorableStateWithCoder:(NSCoder*)coder { NSData* restorationData = [[_engine.get() restorationPlugin] restorationData]; [coder encodeDataObject:restorationData]; + [super encodeRestorableStateWithCoder:coder]; } - (void)decodeRestorableStateWithCoder:(NSCoder*)coder { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 0ee9487cbad9c..f4772c69811b5 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -5,6 +5,7 @@ #import #import +#include "flutter/fml/platform/darwin/message_loop_darwin.h" #import "flutter/lib/ui/window/viewport_metrics.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" @@ -59,9 +60,10 @@ - (void)sendKeyEvent:(const FlutterKeyEvent&)event // NSAssert(callback != nullptr, @"Invalid callback"); // Response is async, so we have to post it to the run loop instead of calling // it directly. - CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^() { - callback(true, userData); - }); + CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, + ^() { + callback(true, userData); + }); } @end @@ -78,6 +80,11 @@ @interface FlutterEngine (TestLowMemory) - (void)notifyLowMemory; @end +@interface FlutterEngineGroup () +@property(nonatomic, strong) NSMutableArray* engines; +- (FlutterEngine*)makeEngineWithOptions:(nullable FlutterEngineGroupOptions*)options; +@end + extern NSNotificationName const FlutterViewControllerWillDealloc; /// A simple mock class for FlutterEngine. @@ -122,6 +129,10 @@ - (void)handlePressEvent:(FlutterUIPressProxy*)press - (void)scrollEvent:(UIPanGestureRecognizer*)recognizer; - (void)updateViewportMetrics; - (void)onUserSettingsChanged:(NSNotification*)notification; +- (void)keyboardWillChangeFrame:(NSNotification*)notification; +- (void)startKeyBoardAnimation:(NSTimeInterval)duration; +- (void)ensureViewportMetricsIsCorrect; +- (void)invalidateDisplayLink; @end @interface FlutterViewControllerTest : XCTestCase @@ -149,6 +160,54 @@ - (void)tearDown { self.messageSent = nil; } +- (void)testViewControllerInitWithEngineGroupWithEngineGroupOptions { + FlutterEngineGroup* engineGroup = + OCMPartialMock([[FlutterEngineGroup alloc] initWithName:@"io.flutter" project:nil]); + FlutterEngineGroupOptions* options = [[FlutterEngineGroupOptions alloc] init]; + + FlutterViewController* controller = [[FlutterViewController alloc] initWithEngineGroup:engineGroup + options:options + nibName:nil + bundle:nil]; + OCMStub([[engineGroup engines] firstObject]).andReturn(controller.engine); + OCMVerify([engineGroup makeEngineWithOptions:options]); +} + +- (void)testkeyboardWillChangeFrameWillStartKeyboardAnimation { + FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); + [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine + nibName:nil + bundle:nil]; + + CGFloat width = UIScreen.mainScreen.bounds.size.width; + CGRect keyboardFrame = CGRectMake(0, 100, width, 400); + BOOL isLocal = YES; + NSNotification* notification = [NSNotification + notificationWithName:@"" + object:nil + userInfo:@{ + @"UIKeyboardFrameEndUserInfoKey" : [NSValue valueWithCGRect:keyboardFrame], + @"UIKeyboardAnimationDurationUserInfoKey" : [NSNumber numberWithDouble:0.25], + @"UIKeyboardIsLocalUserInfoKey" : [NSNumber numberWithBool:isLocal] + }]; + id viewControllerMock = OCMPartialMock(viewController); + [viewControllerMock keyboardWillChangeFrame:notification]; + OCMVerify([viewControllerMock startKeyBoardAnimation:0.25]); +} + +- (void)testEnsureViewportMetricsWillInvokeAndDisplayLinkWillInvalidateInViewDidDisappear { + FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); + [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine + nibName:nil + bundle:nil]; + id viewControllerMock = OCMPartialMock(viewController); + [viewControllerMock viewDidDisappear:YES]; + OCMVerify([viewControllerMock ensureViewportMetricsIsCorrect]); + OCMVerify([viewControllerMock invalidateDisplayLink]); +} + - (void)testViewDidDisappearDoesntPauseEngineWhenNotTheViewController { id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]); FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init]; @@ -719,6 +778,21 @@ - (void)testWillDeallocNotification { [self waitForExpectations:@[ expectation ] timeout:1.0]; } +- (void)testReleasesKeyboardManagerOnDealloc { + __weak FlutterKeyboardManager* weakKeyboardManager = nil; + @autoreleasepool { + FlutterViewController* viewController = [[FlutterViewController alloc] init]; + + [viewController addInternalPlugins]; + weakKeyboardManager = viewController.keyboardManager; + XCTAssertNotNil(weakKeyboardManager); + [viewController deregisterNotifications]; + viewController = nil; + } + // View controller has released the keyboard manager. + XCTAssertNil(weakKeyboardManager); +} + - (void)testDoesntLoadViewInInit { FlutterDartProject* project = [[FlutterDartProject alloc] init]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; @@ -744,6 +818,25 @@ - (void)testHideOverlay { engine.viewController = nil; } +- (void)testHideA11yElements { + FlutterDartProject* project = [[FlutterDartProject alloc] init]; + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; + [engine createShell:@"" libraryURI:@"" initialRoute:nil]; + FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + XCTAssertFalse(realVC.view.accessibilityElementsHidden); + [[NSNotificationCenter defaultCenter] + postNotificationName:UIApplicationWillResignActiveNotification + object:nil]; + XCTAssertTrue(realVC.view.accessibilityElementsHidden); + [[NSNotificationCenter defaultCenter] + postNotificationName:UIApplicationDidBecomeActiveNotification + object:nil]; + XCTAssertFalse(realVC.view.accessibilityElementsHidden); + engine.viewController = nil; +} + - (void)testNotifyLowMemory { FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init]; FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine @@ -763,9 +856,10 @@ - (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback // Response is async, so we have to post it to the run loop instead of calling // it directly. self.messageSent = message; - CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^() { - callback(replyMessage); - }); + CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, + ^() { + callback(replyMessage); + }); } - (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h index b6b655026b463..9e069b467828a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h @@ -12,6 +12,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterUIPressProxy.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewResponder.h" namespace flutter { class FlutterPlatformViewsController; @@ -26,11 +27,11 @@ extern NSNotificationName const FlutterViewControllerHideHomeIndicator; FLUTTER_DARWIN_EXPORT extern NSNotificationName const FlutterViewControllerShowHomeIndicator; -@interface FlutterViewController () +@interface FlutterViewController () @property(nonatomic, readonly) BOOL isPresentingViewController; @property(nonatomic, readonly) BOOL isVoiceOverRunning; -@property(nonatomic) FlutterKeyboardManager* keyboardManager; +@property(nonatomic, retain) FlutterKeyboardManager* keyboardManager; - (fml::WeakPtr)getWeakPtr; - (std::shared_ptr&)platformViewsController; - (FlutterRestorationPlugin*)restorationPlugin; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewResponder.h b/shell/platform/darwin/ios/framework/Source/FlutterViewResponder.h new file mode 100644 index 0000000000000..a9bf602e86fef --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewResponder.h @@ -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. + +#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERVIEWRESPONDER_H_ +#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERVIEWRESPONDER_H_ + +#import + +NS_ASSUME_NONNULL_BEGIN +@protocol FlutterViewResponder + +@property(nonatomic, strong) UIView* view; + +- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event; +- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event; +- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event; +- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event; +- (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches; + +@end +NS_ASSUME_NONNULL_END + +#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERVIEWRESPONDER_H_ diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.h b/shell/platform/darwin/ios/framework/Source/SemanticsObject.h index b2202f0e619f2..ce9347c7c44e9 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.h +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.h @@ -20,7 +20,11 @@ constexpr float kScrollExtentMaxForInf = 1000; @class FlutterPlatformViewSemanticsContainer; /** - * A node in the iOS semantics tree. + * A node in the iOS semantics tree. This object is a wrapper over a native accessibiliy + * object, which is stored in the property `nativeAccessibility`. In the most case, the + * `nativeAccessibility` directly returns this object. Some subclasses such as the + * `FlutterScrollableSemanticsObject` creates a native `UIScrollView` as its `nativeAccessibility` + * so that it can interact with iOS. */ @interface SemanticsObject : UIAccessibilityElement @@ -61,11 +65,6 @@ constexpr float kScrollExtentMaxForInf = 1000; */ @property(nonatomic, strong) NSArray* children; -/** - * Used if this SemanticsObject is for a platform view. - */ -@property(strong, nonatomic) FlutterPlatformViewSemanticsContainer* platformViewSemanticsContainer; - /** * The UIAccessibility that represents this object. * @@ -159,16 +158,14 @@ constexpr float kScrollExtentMaxForInf = 1000; * * `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; +@interface FlutterPlatformViewSemanticsContainer : SemanticsObject -- (instancetype)init __attribute__((unavailable("Use initWithAccessibilityContainer: instead"))); +- (instancetype)initWithBridge:(fml::WeakPtr)bridge + uid:(int32_t)uid NS_UNAVAILABLE; -- (instancetype)initWithSemanticsObject:(SemanticsObject*)object; +- (instancetype)initWithBridge:(fml::WeakPtr)bridge + uid:(int32_t)uid + platformView:(UIView*)platformView NS_DESIGNATED_INITIALIZER; @end diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm index af44bdd245e76..e0bec49c0440b 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm @@ -146,7 +146,7 @@ - (UIAccessibilityTraits)accessibilityTraits { @end // FlutterSwitchSemanticsObject @interface FlutterScrollableSemanticsObject () -@property(nonatomic, strong) FlutterSemanticsScrollView* scrollView; +@property(nonatomic, retain) FlutterSemanticsScrollView* scrollView; @end @implementation FlutterScrollableSemanticsObject { @@ -167,6 +167,7 @@ - (instancetype)initWithBridge:(fml::WeakPtr)br - (void)dealloc { [_scrollView removeFromSuperview]; + _scrollView.semanticsObject = nil; [_scrollView release]; [super dealloc]; } @@ -299,7 +300,6 @@ - (void)dealloc { [_children release]; _parent = nil; _container.get().semanticsObject = nil; - [_platformViewSemanticsContainer release]; _inDealloc = YES; [super dealloc]; } @@ -318,9 +318,6 @@ - (void)setChildren:(NSArray*)children { } - (BOOL)hasChildren { - if (_node.IsPlatformViewNode()) { - return YES; - } return [self.children count] != 0; } @@ -412,7 +409,7 @@ - (NSAttributedString*)createAttributedStringFromString:(NSString*)string withAttributes: (const flutter::StringAttributes&)attributes { NSMutableAttributedString* attributedString = - [[NSMutableAttributedString alloc] initWithString:string]; + [[[NSMutableAttributedString alloc] initWithString:string] autorelease]; for (const auto& attribute : attributes) { NSRange range = NSMakeRange(attribute->start, attribute->end - attribute->start); switch (attribute->type) { @@ -442,16 +439,18 @@ - (NSAttributedString*)createAttributedStringFromString:(NSString*)string #pragma mark - UIAccessibility overrides - (BOOL)isAccessibilityElement { - if (![self isAccessibilityBridgeAlive]) + 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)) + if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute)) { return false; + } // If the node is scrollable AND hidden OR // The node has a label, value, or hint OR @@ -468,8 +467,9 @@ - (BOOL)isAccessibilityElement { } - (void)collectRoutes:(NSMutableArray*)edges { - if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute)) + if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute)) { [edges addObject:self]; + } if ([self hasChildren]) { for (SemanticsObject* child in self.children) { [child collectRoutes:edges]; @@ -478,8 +478,9 @@ - (void)collectRoutes:(NSMutableArray*)edges { } - (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action { - if (![self node].HasAction(flutter::SemanticsAction::kCustomAction)) + if (![self node].HasAction(flutter::SemanticsAction::kCustomAction)) { return NO; + } int32_t action_id = action.uid; std::vector args; args.push_back(3); // type=int32. @@ -494,8 +495,9 @@ - (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action { } - (NSString*)accessibilityLabel { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return nil; + } NSString* label = nil; if (![self node].label.empty()) { label = @([self node].label.data()); @@ -509,30 +511,35 @@ - (NSString*)accessibilityLabel { - (NSAttributedString*)accessibilityAttributedLabel { NSString* label = [self accessibilityLabel]; - if (label.length == 0) + if (label.length == 0) { return nil; + } return [self createAttributedStringFromString:label withAttributes:[self node].labelAttributes]; } - (NSString*)accessibilityHint { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return nil; + } - if ([self node].hint.empty()) + if ([self node].hint.empty()) { return nil; + } return @([self node].hint.data()); } - (NSAttributedString*)accessibilityAttributedHint { NSString* hint = [self accessibilityHint]; - if (hint.length == 0) + if (hint.length == 0) { return nil; + } return [self createAttributedStringFromString:hint withAttributes:[self node].hintAttributes]; } - (NSString*)accessibilityValue { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return nil; + } if (![self node].value.empty()) { return @([self node].value.data()); @@ -554,14 +561,16 @@ - (NSString*)accessibilityValue { - (NSAttributedString*)accessibilityAttributedValue { NSString* value = [self accessibilityValue]; - if (value.length == 0) + if (value.length == 0) { return nil; + } return [self createAttributedStringFromString:value withAttributes:[self node].valueAttributes]; } - (CGRect)accessibilityFrame { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return CGRectMake(0, 0, 0, 0); + } if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) { return [super accessibilityFrame]; @@ -596,9 +605,10 @@ - (id)accessibilityContainer { } if ([self hasChildren] || [self uid] == kRootNodeId) { - if (_container == nil) + if (_container == nil) { _container.reset([[SemanticsObjectContainer alloc] initWithSemanticsObject:self bridge:[self bridge]]); + } return _container.get(); } if ([self parent] == nil) { @@ -613,17 +623,20 @@ - (id)accessibilityContainer { #pragma mark - UIAccessibilityAction overrides - (BOOL)accessibilityActivate { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return NO; - if (![self node].HasAction(flutter::SemanticsAction::kTap)) + } + if (![self node].HasAction(flutter::SemanticsAction::kTap)) { return NO; + } [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kTap); return YES; } - (void)accessibilityIncrement { - if (![self isAccessibilityBridgeAlive]) + 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); @@ -631,8 +644,9 @@ - (void)accessibilityIncrement { } - (void)accessibilityDecrement { - if (![self isAccessibilityBridgeAlive]) + 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); @@ -640,20 +654,24 @@ - (void)accessibilityDecrement { } - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return NO; + } flutter::SemanticsAction action = GetSemanticsActionForScrollDirection(direction); - if (![self node].HasAction(action)) + if (![self node].HasAction(action)) { return NO; + } [self bridge]->DispatchSemanticsAction([self uid], action); return YES; } - (BOOL)accessibilityPerformEscape { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return NO; - if (![self node].HasAction(flutter::SemanticsAction::kDismiss)) + } + if (![self node].HasAction(flutter::SemanticsAction::kDismiss)) { return NO; + } [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDismiss); return YES; } @@ -661,8 +679,9 @@ - (BOOL)accessibilityPerformEscape { #pragma mark UIAccessibilityFocus overrides - (void)accessibilityElementDidBecomeFocused { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return; + } [self bridge]->AccessibilityObjectDidBecomeFocused([self uid]); if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden) || [self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) { @@ -675,8 +694,9 @@ - (void)accessibilityElementDidBecomeFocused { } - (void)accessibilityElementDidLoseFocus { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return; + } [self bridge]->AccessibilityObjectDidLoseFocus([self uid]); if ([self node].HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) { [self bridge]->DispatchSemanticsAction([self uid], @@ -752,31 +772,16 @@ - (UIAccessibilityTraits)accessibilityTraits { @end @interface FlutterPlatformViewSemanticsContainer () -@property(nonatomic, assign) SemanticsObject* semanticsObject; -@property(nonatomic, strong) UIView* platformView; +@property(nonatomic, retain) UIView* platformView; @end @implementation FlutterPlatformViewSemanticsContainer -// 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; - auto controller = object.bridge->GetPlatformViewsController(); - if (controller) { - _platformView = [controller->GetPlatformViewByID(object.node.platformViewId) retain]; - } +- (instancetype)initWithBridge:(fml::WeakPtr)bridge + uid:(int32_t)uid + platformView:(nonnull UIView*)platformView { + if (self = [super initWithBridge:bridge uid:uid]) { + _platformView = [platformView retain]; } return self; } @@ -789,47 +794,8 @@ - (void)dealloc { #pragma mark - UIAccessibilityContainer overrides -- (NSInteger)accessibilityElementCount { - // This container should only contain 2 elements: - // 1. The semantic object that represents this container. - // 2. The platform view object. - return 2; -} - -- (nullable id)accessibilityElementAtIndex:(NSInteger)index { - FML_DCHECK(index < 2); - if (index == 0) { - return _semanticsObject.nativeAccessibility; - } else { - return _platformView; - } -} - -- (NSInteger)indexOfAccessibilityElement:(id)element { - FML_DCHECK(element == _semanticsObject || element == _platformView); - if (element == _semanticsObject) { - return 0; - } else { - return 1; - } -} - -#pragma mark - UIAccessibilityElement overrides - -- (CGRect)accessibilityFrame { - return _semanticsObject.accessibilityFrame; -} - -- (BOOL)isAccessibilityElement { - return NO; -} - -- (id)accessibilityContainer { - return [_semanticsObject accessibilityContainer]; -} - -- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { - return [_platformView accessibilityScroll:direction]; +- (NSArray*)accessibilityElements { + return @[ _platformView ]; } @end @@ -873,40 +839,33 @@ - (NSInteger)accessibilityElementCount { } - (nullable id)accessibilityElementAtIndex:(NSInteger)index { - if (index < 0 || index >= [self accessibilityElementCount]) + if (index < 0 || index >= [self accessibilityElementCount]) { return nil; + } if (index == 0) { return _semanticsObject.nativeAccessibility; } 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]) + if ([child hasChildren]) { return [child accessibilityContainer]; + } return child.nativeAccessibility; } - (NSInteger)indexOfAccessibilityElement:(id)element { - if (element == _semanticsObject) + if (element == _semanticsObject.nativeAccessibility) { 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)) + if ((![child hasChildren] && child.nativeAccessibility == element) || + ([child hasChildren] && [child.nativeAccessibility accessibilityContainer] == element)) { return i + 1; + } } return NSNotFound; } diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm index 6b8940448c567..019fea6cf5b5e 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm @@ -690,16 +690,25 @@ - (void)testShouldDispatchShowOnScreenActionForHidden { XCTAssertTrue(bridge->observations[0].action == flutter::SemanticsAction::kShowOnScreen); } -- (void)testSemanticsObjectAndPlatformViewSemanticsContainerDontHaveRetainCycle { +- (void)testFlutterPlatformViewSemanticsContainer { fml::WeakPtrFactory factory( new flutter::MockAccessibilityBridge()); fml::WeakPtr bridge = factory.GetWeakPtr(); - SemanticsObject* object = [[SemanticsObject alloc] initWithBridge:bridge uid:1]; - object.platformViewSemanticsContainer = - [[FlutterPlatformViewSemanticsContainer alloc] initWithSemanticsObject:object]; - __weak SemanticsObject* weakObject = object; - object = nil; - XCTAssertNil(weakObject); + __weak UIView* weakPlatformView; + @autoreleasepool { + UIView* platformView = [[UIView alloc] init]; + + FlutterPlatformViewSemanticsContainer* container = + [[FlutterPlatformViewSemanticsContainer alloc] initWithBridge:bridge + uid:1 + platformView:platformView]; + XCTAssertEqualObjects(container.accessibilityElements, @[ platformView ]); + weakPlatformView = platformView; + XCTAssertNotNil(weakPlatformView); + } + // Check if there's no more strong references to `platformView` after container and platformView + // are released. + XCTAssertNil(weakPlatformView); } - (void)testFlutterSwitchSemanticsObjectMatchesUISwitch { diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h index ef451b5923b91..f3ad233389c37 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h @@ -99,12 +99,11 @@ class AccessibilityBridge final : public AccessibilityBridgeIos { int32_t last_focused_semantics_object_id_; fml::scoped_nsobject> objects_; fml::scoped_nsprotocol accessibility_channel_; - fml::WeakPtrFactory weak_factory_; int32_t previous_route_id_; std::unordered_map actions_; std::vector previous_routes_; std::unique_ptr ios_delegate_; - + fml::WeakPtrFactory weak_factory_; // Must be the last member. FML_DISALLOW_COPY_AND_ASSIGN(AccessibilityBridge); }; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index 0e7a4ac01d542..8efbd498e1c92 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -46,11 +46,11 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, platform_views_controller_(platform_views_controller), last_focused_semantics_object_id_(kSemanticObjectIdInvalid), objects_([[NSMutableDictionary alloc] init]), - weak_factory_(this), previous_route_id_(0), previous_routes_({}), ios_delegate_(ios_delegate ? std::move(ios_delegate) - : std::make_unique()) { + : std::make_unique()), + weak_factory_(this) { accessibility_channel_.reset([[FlutterBasicMessageChannel alloc] initWithName:@"flutter/accessibility" binaryMessenger:platform_view->GetOwnerViewController().get().engine.binaryMessenger @@ -126,15 +126,6 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification, object.accessibilityCustomActions = accessibilityCustomActions; } - if (object.node.IsPlatformViewNode()) { - auto controller = GetPlatformViewsController(); - if (controller) { - object.platformViewSemanticsContainer = [[[FlutterPlatformViewSemanticsContainer alloc] - initWithSemanticsObject:object] autorelease]; - } - } else if (object.platformViewSemanticsContainer) { - object.platformViewSemanticsContainer = nil; - } if (needsAnnouncement) { // Try to be more polite - iOS 11+ supports // UIAccessibilitySpeechAttributeQueueAnnouncement which should avoid @@ -268,6 +259,12 @@ static void ReplaceSemanticsObject(SemanticsObject* oldObject, } else if (node.HasFlag(flutter::SemanticsFlags::kHasImplicitScrolling)) { return [[[FlutterScrollableSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id] autorelease]; + } else if (node.IsPlatformViewNode()) { + return [[[FlutterPlatformViewSemanticsContainer alloc] + initWithBridge:weak_ptr + uid:node.id + platformView:weak_ptr->GetPlatformViewsController()->GetPlatformViewByID( + node.platformViewId)] autorelease]; } else { return [[[FlutterSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id] autorelease]; } @@ -329,8 +326,9 @@ static bool DidFlagChange(const flutter::SemanticsNode& oldNode, SemanticsObject* AccessibilityBridge::FindFirstFocusable(SemanticsObject* parent) { SemanticsObject* currentObject = parent ?: objects_.get()[@(kRootNodeId)]; ; - if (!currentObject) + if (!currentObject) { return nil; + } if (currentObject.isAccessibilityElement) { return currentObject; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm index 44035fd23858f..1e281e250e251 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm @@ -79,8 +79,6 @@ void OnPlatformViewSetViewportMetrics(const ViewportMetrics& metrics) override { void OnPlatformViewDispatchPlatformMessage(std::unique_ptr message) override {} void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr packet) override { } - void OnPlatformViewDispatchKeyDataPacket(std::unique_ptr packet, - std::function callback) override {} void OnPlatformViewDispatchSemanticsAction(int32_t id, SemanticsAction action, fml::MallocMapping args) override {} @@ -661,6 +659,61 @@ - (void)testLayoutChangeDoesCallNativeAccessibility { UIAccessibilityLayoutChangedNotification); } +- (void)testScrollableSemanticsContainerReturnsCorrectChildren { + flutter::MockDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/nil, + /*task_runners=*/runners); + id mockFlutterView = OCMClassMock([FlutterView class]); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); + + OCMExpect([mockFlutterView + setAccessibilityElements:[OCMArg checkWithBlock:^BOOL(NSArray* value) { + if ([value count] != 1) { + return NO; + } + SemanticsObjectContainer* container = value[0]; + SemanticsObject* object = container.semanticsObject; + FlutterScrollableSemanticsObject* scrollable = + (FlutterScrollableSemanticsObject*)object.children[0]; + id nativeScrollable = scrollable.nativeAccessibility; + SemanticsObjectContainer* scrollableContainer = [nativeScrollable accessibilityContainer]; + return [scrollableContainer indexOfAccessibilityElement:nativeScrollable] == 1; + }]]); + auto ios_delegate = std::make_unique(); + __block auto bridge = + std::make_unique(/*view_controller=*/mockFlutterViewController, + /*platform_view=*/platform_view.get(), + /*platform_views_controller=*/nil, + /*ios_delegate=*/std::move(ios_delegate)); + + flutter::CustomAccessibilityActionUpdates actions; + flutter::SemanticsNodeUpdates nodes; + + flutter::SemanticsNode node1; + node1.id = 1; + node1.label = "node1"; + node1.flags = static_cast(flutter::SemanticsFlags::kHasImplicitScrolling); + nodes[node1.id] = node1; + flutter::SemanticsNode root_node; + root_node.id = kRootNodeId; + root_node.label = "root"; + root_node.childrenInTraversalOrder = {1}; + root_node.childrenInHitTestOrder = {1}; + nodes[root_node.id] = root_node; + bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions); + OCMVerifyAll(mockFlutterView); +} + - (void)testAnnouncesRouteChangesAndLayoutChangeInOneUpdate { flutter::MockDelegate mock_delegate; auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); @@ -1570,7 +1623,45 @@ - (void)testAccessibilityMessageAfterDeletion { latch.Signal(); }); latch.Wait(); - OCMVerify([messenger cleanupConnection:connection]); + OCMVerify([messenger cleanUpConnection:connection]); [engine stopMocking]; } + +- (void)testFlutterSemanticsScrollViewManagedObjectLifecycleCorrectly { + flutter::MockDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/nil, + /*task_runners=*/runners); + id mockFlutterView = OCMClassMock([FlutterView class]); + id mockFlutterViewController = OCMClassMock([FlutterViewController class]); + OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView); + + auto ios_delegate = std::make_unique(); + __block auto bridge = + std::make_unique(/*view_controller=*/mockFlutterViewController, + /*platform_view=*/platform_view.get(), + /*platform_views_controller=*/nil, + /*ios_delegate=*/std::move(ios_delegate)); + + FlutterSemanticsScrollView* flutterSemanticsScrollView; + @autoreleasepool { + FlutterScrollableSemanticsObject* semanticsObject = + [[[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge->GetWeakPtr() + uid:1234] autorelease]; + + flutterSemanticsScrollView = semanticsObject.nativeAccessibility; + } + XCTAssertTrue(flutterSemanticsScrollView); + // If the _semanticsObject is not a weak pointer this (or any other method on + // flutterSemanticsScrollView) will cause an EXC_BAD_ACCESS. + XCTAssertFalse([flutterSemanticsScrollView isAccessibilityElement]); +} @end diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h b/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h index 163346d7b04dd..9df47186ccf73 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h +++ b/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h @@ -14,10 +14,10 @@ @interface FlutterInactiveTextInput : UIView @property(nonatomic, copy) NSString* text; -@property(nonatomic, readonly) NSMutableString* markedText; -@property(readwrite, copy) UITextRange* selectedTextRange; -@property(nonatomic, strong) UITextRange* markedTextRange; -@property(nonatomic, copy) NSDictionary* markedTextStyle; +@property(nonatomic, copy, readonly) NSMutableString* markedText; +@property(copy) UITextRange* selectedTextRange; +@property(nonatomic, strong, readonly) UITextRange* markedTextRange; +@property(nonatomic, copy) NSDictionary* markedTextStyle; @property(nonatomic, assign) id inputDelegate; @end 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 2b6b52289b43d..edec6680ac7ea 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm @@ -16,8 +16,13 @@ @implementation FlutterInactiveTextInput { @synthesize beginningOfDocument = _beginningOfDocument; @synthesize endOfDocument = _endOfDocument; -- (instancetype)init { - return [super init]; +- (void)dealloc { + [_text release]; + [_markedText release]; + [_markedTextRange release]; + [_selectedTextRange release]; + [_markedTextStyle release]; + [super dealloc]; } - (BOOL)hasText { @@ -232,62 +237,73 @@ - (UIView*)textInputView { } - (void)accessibilityElementDidBecomeFocused { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return; + } [[self textInputSurrogate] accessibilityElementDidBecomeFocused]; [super accessibilityElementDidBecomeFocused]; } - (void)accessibilityElementDidLoseFocus { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return; + } [[self textInputSurrogate] accessibilityElementDidLoseFocus]; [super accessibilityElementDidLoseFocus]; } - (BOOL)accessibilityElementIsFocused { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return false; + } return [self node].HasFlag(flutter::SemanticsFlags::kIsFocused); } - (BOOL)accessibilityActivate { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return false; + } return [[self textInputSurrogate] accessibilityActivate]; } - (NSString*)accessibilityLabel { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return nil; + } NSString* label = [super accessibilityLabel]; - if (label != nil) + if (label != nil) { return label; + } return [self textInputSurrogate].accessibilityLabel; } - (NSString*)accessibilityHint { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return nil; + } NSString* hint = [super accessibilityHint]; - if (hint != nil) + if (hint != nil) { return hint; + } return [self textInputSurrogate].accessibilityHint; } - (NSString*)accessibilityValue { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return nil; + } NSString* value = [super accessibilityValue]; - if (value != nil) + if (value != nil) { return value; + } return [self textInputSurrogate].accessibilityValue; } - (UIAccessibilityTraits)accessibilityTraits { - if (![self isAccessibilityBridgeAlive]) + if (![self isAccessibilityBridgeAlive]) { return 0; + } // 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. diff --git a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm index cb4858d0f8ba4..7877f438b2eea 100644 --- a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm +++ b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm @@ -110,7 +110,7 @@ @implementation DisplayLinkManager + (double)displayRefreshRate { if (@available(iOS 10.3, *)) { fml::scoped_nsobject display_link = fml::scoped_nsobject { - [[CADisplayLink displayLinkWithTarget:[[DisplayLinkManager new] autorelease] + [[CADisplayLink displayLinkWithTarget:[[[DisplayLinkManager alloc] init] autorelease] selector:@selector(onDisplayLink:)] retain] }; display_link.get().paused = YES; diff --git a/shell/platform/darwin/ios/ios_context.h b/shell/platform/darwin/ios/ios_context.h index c08b6b9c729d3..7e0e5e2c23b40 100644 --- a/shell/platform/darwin/ios/ios_context.h +++ b/shell/platform/darwin/ios/ios_context.h @@ -22,7 +22,7 @@ namespace flutter { /// contexts on iOS. On-screen contexts are used by Flutter for /// rendering into the surface. The lifecycle of this context may be /// tied to the lifecycle of the surface. On the other hand, the -/// lifecycle of the the off-screen context it tied to that of the +/// lifecycle of the off-screen context it tied to that of the /// platform view. This one object used to manage both context /// because GPU handles may need to be shared between the two /// context. To achieve this, context may need references to one diff --git a/shell/platform/darwin/ios/ios_surface_gl.h b/shell/platform/darwin/ios/ios_surface_gl.h index a541e28e7c21c..54a0584a7e0af 100644 --- a/shell/platform/darwin/ios/ios_surface_gl.h +++ b/shell/platform/darwin/ios/ios_surface_gl.h @@ -44,7 +44,7 @@ class IOSSurfaceGL final : public IOSSurface, public GPUSurfaceGLDelegate { intptr_t GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| - bool SurfaceSupportsReadback() const override; + SurfaceFrame::FramebufferInfo GLContextFramebufferInfo() const override; // |GPUSurfaceGLDelegate| bool AllowsDrawingWhenGpuDisabled() const override; diff --git a/shell/platform/darwin/ios/ios_surface_gl.mm b/shell/platform/darwin/ios/ios_surface_gl.mm index e1c5bde675499..9b6bb6a99aa1e 100644 --- a/shell/platform/darwin/ios/ios_surface_gl.mm +++ b/shell/platform/darwin/ios/ios_surface_gl.mm @@ -56,13 +56,15 @@ } // |GPUSurfaceGLDelegate| -bool IOSSurfaceGL::SurfaceSupportsReadback() const { +SurfaceFrame::FramebufferInfo IOSSurfaceGL::GLContextFramebufferInfo() const { + SurfaceFrame::FramebufferInfo res; // The onscreen surface wraps a GL renderbuffer, which is extremely slow to read on iOS. // Certain filter effects, in particular BackdropFilter, require making a copy of // the current destination. For performance, the iOS surface will specify that it // does not support readback so that the engine compositor can implement a workaround // such as rendering the scene to an offscreen surface or Skia saveLayer. - return false; + res.supports_readback = false; + return res; } // |GPUSurfaceGLDelegate| diff --git a/shell/platform/darwin/ios/platform_view_ios.h b/shell/platform/darwin/ios/platform_view_ios.h index 4010da254bd1d..3af69b7b2bf8a 100644 --- a/shell/platform/darwin/ios/platform_view_ios.h +++ b/shell/platform/darwin/ios/platform_view_ios.h @@ -90,9 +90,6 @@ class PlatformViewIOS final : public PlatformView { // |PlatformView| PointerDataDispatcherMaker GetDispatcherMaker() override; - void DispatchKeyDataPacket(std::unique_ptr packet, - std::function callback); - // |PlatformView| void SetSemanticsEnabled(bool enabled) override; diff --git a/shell/platform/darwin/ios/rendering_api_selection.mm b/shell/platform/darwin/ios/rendering_api_selection.mm index b2472df89edf7..b713e94ff527b 100644 --- a/shell/platform/darwin/ios/rendering_api_selection.mm +++ b/shell/platform/darwin/ios/rendering_api_selection.mm @@ -22,6 +22,7 @@ bool ShouldUseMetalRenderer() { if (@available(iOS METAL_IOS_VERSION_BASELINE, *)) { auto device = MTLCreateSystemDefaultDevice(); ios_version_supports_metal = [device supportsFeatureSet:MTLFeatureSet_iOS_GPUFamily1_v3]; + [device release]; } return ios_version_supports_metal; } diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h b/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h index c5dbb4ab3571a..12ead38f104f4 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h @@ -24,7 +24,7 @@ FLUTTER_DARWIN_EXPORT @interface FlutterEngine : NSObject /** - * Initializes an engine with the given viewController. + * Initializes an engine with the given project. * * @param labelPrefix Currently unused; in the future, may be used for labelling threads * as with the iOS FlutterEngine. @@ -34,7 +34,7 @@ FLUTTER_DARWIN_EXPORT project:(nullable FlutterDartProject*)project; /** - * Initializes an engine with the given viewController. + * Initializes an engine that can run headlessly with the given project. * * @param labelPrefix Currently unused; in the future, may be used for labelling threads * as with the iOS FlutterEngine. diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h b/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h index be85081563699..2349dad3be4b9 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h @@ -53,4 +53,12 @@ FLUTTER_DARWIN_EXPORT NS_DESIGNATED_INITIALIZER; - (nonnull instancetype)initWithCoder:(nonnull NSCoder*)nibNameOrNil NS_DESIGNATED_INITIALIZER; +/** + * Invoked by the engine right before the engine is restarted. + * + * This should reset states to as if the application has just started. It + * usually indicates a hot restart (Shift-R in Flutter CLI.) + */ +- (void)onPreEngineRestart; + @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm index bcab9b0ecf1c7..dd65f2e97285c 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm @@ -78,8 +78,9 @@ static uint64_t GetPhysicalKeyForKeyCode(unsigned short keyCode) { */ static uint64_t GetLogicalKeyForModifier(unsigned short keyCode, uint64_t hidCode) { NSNumber* fromKeyCode = [keyCodeToLogicalKey objectForKey:@(keyCode)]; - if (fromKeyCode != nil) + if (fromKeyCode != nil) { return fromKeyCode.unsignedLongLongValue; + } return KeyOfPlane(hidCode, kMacosPlane); } @@ -120,8 +121,9 @@ static uint64_t toLower(uint64_t n) { static uint64_t GetLogicalKeyForEvent(NSEvent* event, uint64_t physicalKey) { // Look to see if the keyCode can be mapped from keycode. NSNumber* fromKeyCode = [keyCodeToLogicalKey objectForKey:@(event.keyCode)]; - if (fromKeyCode != nil) + if (fromKeyCode != nil) { return fromKeyCode.unsignedLongLongValue; + } NSString* keyLabel = event.charactersIgnoringModifiers; NSUInteger keyLabelLength = [keyLabel length]; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm index 4a5cde65583b8..7e1012569abb6 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm @@ -51,8 +51,9 @@ - (void)respond:(BOOL)handled { } - (void)dealloc { - if (_data->character != nullptr) + if (_data->character != nullptr) { delete[] _data->character; + } delete _data; } @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index ab196fa2f7aca..c62353b10d64d 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -33,6 +33,33 @@ static FlutterLocale FlutterLocaleFromNSLocale(NSLocale* locale) { return flutterLocale; } +#pragma mark - + +// Records an active handler of the messenger (FlutterEngine) that listens to +// platform messages on a given channel. +@interface FlutterEngineHandlerInfo : NSObject + +- (instancetype)initWithConnection:(NSNumber*)connection + handler:(FlutterBinaryMessageHandler)handler; + +@property(nonatomic, readonly) FlutterBinaryMessageHandler handler; +@property(nonatomic, readonly) NSNumber* connection; + +@end + +@implementation FlutterEngineHandlerInfo +- (instancetype)initWithConnection:(NSNumber*)connection + handler:(FlutterBinaryMessageHandler)handler { + self = [super init]; + NSAssert(self, @"Super init cannot be nil"); + _connection = connection; + _handler = handler; + return self; +} +@end + +#pragma mark - + /** * Private interface declaration for FlutterEngine. */ @@ -48,6 +75,14 @@ - (void)sendUserLocales; */ - (void)engineCallbackOnPlatformMessage:(const FlutterPlatformMessage*)message; +/** + * Invoked right before the engine is restarted. + * + * This should reset states to as if the application has just started. It + * usually indicates a hot restart (Shift-R in Flutter CLI.) + */ +- (void)engineCallbackOnPreEngineRestart; + /** * Requests that the task be posted back the to the Flutter engine at the target time. The target * time is in the clock used by the Flutter engine. @@ -131,8 +166,12 @@ @implementation FlutterEngine { // The project being run by this engine. FlutterDartProject* _project; - // A mapping of channel names to the registered handlers for those channels. - NSMutableDictionary* _messageHandlers; + // A mapping of channel names to the registered information for those channels. + NSMutableDictionary* _messengerHandlers; + + // A self-incremental integer to assign to newly assigned channels as + // identification. + FlutterBinaryMessengerConnection _currentMessengerConnection; // Whether the engine can continue running after the view controller is removed. BOOL _allowHeadlessExecution; @@ -160,7 +199,8 @@ - (instancetype)initWithName:(NSString*)labelPrefix NSAssert(self, @"Super init cannot be nil"); _project = project ?: [[FlutterDartProject alloc] init]; - _messageHandlers = [[NSMutableDictionary alloc] init]; + _messengerHandlers = [[NSMutableDictionary alloc] init]; + _currentMessengerConnection = 1; _allowHeadlessExecution = allowHeadlessExecution; _semanticsEnabled = NO; @@ -264,6 +304,11 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { flutterArguments.compositor = [self createFlutterCompositor]; + flutterArguments.on_pre_engine_restart_callback = [](void* user_data) { + FlutterEngine* engine = (__bridge FlutterEngine*)user_data; + [engine engineCallbackOnPreEngineRestart]; + }; + FlutterRendererConfig rendererConfig = [_renderer createRendererConfig]; FlutterEngineResult result = _embedderAPI.Initialize( FLUTTER_ENGINE_VERSION, &rendererConfig, &flutterArguments, (__bridge void*)(self), &_engine); @@ -544,14 +589,20 @@ - (void)engineCallbackOnPlatformMessage:(const FlutterPlatformMessage*)message { } }; - FlutterBinaryMessageHandler channelHandler = _messageHandlers[channel]; - if (channelHandler) { - channelHandler(messageData, binaryResponseHandler); + FlutterEngineHandlerInfo* handlerInfo = _messengerHandlers[channel]; + if (handlerInfo) { + handlerInfo.handler(messageData, binaryResponseHandler); } else { binaryResponseHandler(nil); } } +- (void)engineCallbackOnPreEngineRestart { + if (_viewController) { + [_viewController onPreEngineRestart]; + } +} + /** * Note: Called from dealloc. Should not use accessors or other methods. */ @@ -640,12 +691,27 @@ - (void)sendOnChannel:(NSString*)channel - (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(nonnull NSString*)channel binaryMessageHandler: (nullable FlutterBinaryMessageHandler)handler { - _messageHandlers[channel] = [handler copy]; - return 0; -} - -- (void)cleanupConnection:(FlutterBinaryMessengerConnection)connection { - // There hasn't been a need to implement this yet for macOS. + _currentMessengerConnection += 1; + _messengerHandlers[channel] = + [[FlutterEngineHandlerInfo alloc] initWithConnection:@(_currentMessengerConnection) + handler:[handler copy]]; + return _currentMessengerConnection; +} + +- (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection { + // Find the _messengerHandlers that has the required connection, and record its + // channel. + NSString* foundChannel = nil; + for (NSString* key in [_messengerHandlers allKeys]) { + FlutterEngineHandlerInfo* handlerInfo = [_messengerHandlers objectForKey:key]; + if ([handlerInfo.connection isEqual:@(connection)]) { + foundChannel = key; + break; + } + } + if (foundChannel) { + [_messengerHandlers removeObjectForKey:foundChannel]; + } } #pragma mark - FlutterPluginRegistry diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index 56d6a62072617..7710c8b4597c5 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -2,14 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" + +#include + #include "flutter/fml/synchronization/waitable_event.h" +#include "flutter/lib/ui/window/platform_message.h" #include "flutter/shell/platform/common/accessibility_bridge.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h" -#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h" #include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/embedder/embedder_engine.h" #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" #include "flutter/testing/test_dart_native_resolver.h" @@ -405,4 +410,67 @@ @interface FlutterEngine (Test) EXPECT_TRUE(called); } +// If a channel overrides a previous channel with the same name, cleaning +// the previous channel should not affect the new channel. +// +// This is important when recreating classes that uses a channel, because the +// new instance would create the channel before the first class is deallocated +// and clears the channel. +TEST_F(FlutterEngineTest, MessengerCleanupConnectionWorks) { + FlutterEngine* engine = GetFlutterEngine(); + EXPECT_TRUE([engine runWithEntrypoint:@"main"]); + + NSString* channel = @"_test_"; + NSData* channel_data = [channel dataUsingEncoding:NSUTF8StringEncoding]; + + // Mock SendPlatformMessage so that if a message is sent to + // "test/send_message", act as if the framework has sent an empty message to + // the channel marked by the `sendOnChannel:message:` call's message. + engine.embedderAPI.SendPlatformMessage = MOCK_ENGINE_PROC( + SendPlatformMessage, ([](auto engine_, auto message_) { + if (strcmp(message_->channel, "test/send_message") == 0) { + // The simplest message that is acceptable to a method channel. + std::string message = R"|({"method": "a"})|"; + std::string channel(reinterpret_cast(message_->message), + message_->message_size); + reinterpret_cast(engine_) + ->GetShell() + .GetPlatformView() + ->HandlePlatformMessage(std::make_unique( + channel.c_str(), fml::MallocMapping::Copy(message.c_str(), message.length()), + fml::RefPtr())); + } + return kSuccess; + })); + + __block int record = 0; + + FlutterMethodChannel* channel1 = + [FlutterMethodChannel methodChannelWithName:channel + binaryMessenger:engine.binaryMessenger + codec:[FlutterJSONMethodCodec sharedInstance]]; + [channel1 setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { + record += 1; + }]; + + [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data]; + EXPECT_EQ(record, 1); + + FlutterMethodChannel* channel2 = + [FlutterMethodChannel methodChannelWithName:channel + binaryMessenger:engine.binaryMessenger + codec:[FlutterJSONMethodCodec sharedInstance]]; + [channel2 setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { + record += 10; + }]; + + [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data]; + EXPECT_EQ(record, 11); + + [channel1 setMethodCallHandler:nil]; + + [engine.binaryMessenger sendOnChannel:@"test/send_message" message:channel_data]; + EXPECT_EQ(record, 21); +} + } // namespace flutter::testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm index e73331db17140..273f4c3533270 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm @@ -51,8 +51,9 @@ }; } NSCursor* result = [systemCursors objectForKey:kind]; - if (result == nil) + if (result == nil) { return [NSCursor arrowCursor]; + } return result; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 57544a770087b..712ea7de9a1ab 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -4,11 +4,14 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h" +#import #import #include #include +#include "flutter/fml/platform/darwin/string_range_sanitization.h" +#include "flutter/shell/platform/common/text_editing_delta.h" #include "flutter/shell/platform/common/text_input_model.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h" @@ -26,6 +29,8 @@ static NSString* const kSetEditableSizeAndTransform = @"TextInput.setEditableSizeAndTransform"; static NSString* const kSetCaretRect = @"TextInput.setCaretRect"; static NSString* const kUpdateEditStateResponseMethod = @"TextInputClient.updateEditingState"; +static NSString* const kUpdateEditStateWithDeltasResponseMethod = + @"TextInputClient.updateEditingStateWithDeltas"; static NSString* const kPerformAction = @"TextInputClient.performAction"; static NSString* const kMultilineInputType = @"TextInputType.multiline"; @@ -33,6 +38,7 @@ static NSString* const kTextAffinityUpstream = @"TextAffinity.upstream"; static NSString* const kTextInputAction = @"inputAction"; +static NSString* const kEnableDeltaModel = @"enableDeltaModel"; static NSString* const kTextInputType = @"inputType"; static NSString* const kTextInputTypeName = @"name"; @@ -124,6 +130,14 @@ @interface FlutterTextInputPlugin () */ @property(nonatomic, nonnull) NSString* inputAction; +/** + * Whether to enable the sending of text input updates from the engine to the + * framework as TextEditingDeltas rather than as one TextEditingValue. + * For more information on the delta model, see: + * https://master-api.flutter.dev/flutter/services/TextInputConfiguration/enableDeltaModel.html + */ +@property(nonatomic) BOOL enableDeltaModel; + /** * Handles a Flutter system message on the text input channel. */ @@ -136,10 +150,17 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; - (void)setEditingState:(NSDictionary*)state; /** - * Informs the Flutter framework of changes to the text input model's state. + * Informs the Flutter framework of changes to the text input model's state by + * sending the entire new state. */ - (void)updateEditState; +/** + * Informs the Flutter framework of changes to the text input model's state by + * sending only the difference. + */ +- (void)updateEditStateWithDelta:(const flutter::TextEditingDelta)delta; + /** * Updates the stringValue and selectedRange that stored in the NSTextView interface * that this plugin inherits from. @@ -149,6 +170,12 @@ - (void)updateEditState; */ - (void)updateTextAndSelection; +/** + * Return the string representation of the current textAffinity as it should be + * sent over the FlutterMethodChannel. + */ +- (NSString*)textAffinityString; + @end @implementation FlutterTextInputPlugin { @@ -186,7 +213,7 @@ - (instancetype)initWithViewController:(FlutterViewController*)viewController { [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { [unsafeSelf handleMethodCall:call result:result]; }]; - _textInputContext = [[NSTextInputContext alloc] initWithClient:self]; + _textInputContext = [[NSTextInputContext alloc] initWithClient:unsafeSelf]; _previouslyPressedFlags = 0; _flutterViewController = viewController; @@ -208,6 +235,11 @@ - (BOOL)isFirstResponder { - (void)dealloc { [_channel setMethodCallHandler:nil]; + if (_textInputContext) { + [_textInputContext deactivate]; + [_textInputContext discardMarkedText]; + _textInputContext = nil; + } } #pragma mark - Private @@ -229,6 +261,7 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { _clientID = clientID; _inputAction = config[kTextInputAction]; + _enableDeltaModel = [config[kEnableDeltaModel] boolValue]; NSDictionary* inputTypeInfo = config[kTextInputType]; _inputType = inputTypeInfo[kTextInputTypeName]; self.textAffinity = FlutterTextAffinityUpstream; @@ -244,6 +277,7 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else if ([method isEqualToString:kClearClientMethod]) { _clientID = nil; _inputAction = nil; + _enableDeltaModel = NO; _inputType = nil; _activeModel = nullptr; } else if ([method isEqualToString:kSetEditingStateMethod]) { @@ -253,7 +287,15 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { // Close the loop, since the framework state could have been updated by the // engine since it sent this update, and needs to now be made to match the // engine's version of the state. - [self updateEditState]; + if (!_enableDeltaModel) { + [self updateEditState]; + } else { + // Send an "empty" delta. The client can compare the old_text with their + // current text and update with that if the race condition described above + // occurs. + [self updateEditStateWithDelta:flutter::TextEditingDelta(_activeModel->GetText().c_str(), + flutter::TextRange(0, 0), "")]; + } } else if ([method isEqualToString:kSetEditableSizeAndTransform]) { NSDictionary* state = call.arguments; [self setEditableTransform:state[kTransformKey]]; @@ -328,9 +370,7 @@ - (void)updateEditState { return; } - NSString* const textAffinity = (self.textAffinity == FlutterTextAffinityUpstream) - ? kTextAffinityUpstream - : kTextAffinityDownstream; + NSString* const textAffinity = [self textAffinityString]; int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1; int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1; @@ -349,6 +389,35 @@ - (void)updateEditState { [self updateTextAndSelection]; } +- (void)updateEditStateWithDelta:(const flutter::TextEditingDelta)delta { + NSUInteger selectionBase = _activeModel->selection().base(); + NSUInteger selectionExtent = _activeModel->selection().extent(); + int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1; + int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1; + + NSString* const textAffinity = [self textAffinityString]; + + NSDictionary* deltaToFramework = @{ + @"oldText" : @(delta.old_text().c_str()), + @"deltaText" : @(delta.delta_text().c_str()), + @"deltaStart" : @(delta.delta_start()), + @"deltaEnd" : @(delta.delta_end()), + @"selectionBase" : @(selectionBase), + @"selectionExtent" : @(selectionExtent), + @"selectionAffinity" : textAffinity, + @"selectionIsDirectional" : @(false), + @"composingBase" : @(composingBase), + @"composingExtent" : @(composingExtent), + }; + + NSDictionary* deltas = @{ + @"deltas" : @[ deltaToFramework ], + }; + + [_channel invokeMethod:kUpdateEditStateWithDeltasResponseMethod + arguments:@[ self.clientID, deltas ]]; +} + - (void)updateTextAndSelection { NSAssert(_activeModel != nullptr, @"Flutter text model must not be null."); NSString* text = @(_activeModel->GetText().data()); @@ -367,6 +436,11 @@ - (void)updateTextAndSelection { } } +- (NSString*)textAffinityString { + return (self.textAffinity == FlutterTextAffinityUpstream) ? kTextAffinityUpstream + : kTextAffinityDownstream; +} + #pragma mark - #pragma mark FlutterKeySecondaryResponder @@ -473,15 +547,29 @@ - (void)insertText:(id)string replacementRange:(NSRange)range { size_t base = std::clamp(location, 0L, textLength); size_t extent = std::clamp(location + signedLength, 0L, textLength); + _activeModel->SetSelection(flutter::TextRange(base, extent)); } - _activeModel->AddText([string UTF8String]); + flutter::TextRange oldSelection = _activeModel->selection(); + + std::string textBeforeChange = _activeModel->GetText().c_str(); + std::string utf8String = [string UTF8String]; + _activeModel->AddText(utf8String); if (_activeModel->composing()) { _activeModel->CommitComposing(); _activeModel->EndComposing(); } - [self updateEditState]; + if (_enableDeltaModel) { + flutter::TextRange replacedRange = + range.location == NSNotFound + ? flutter::TextRange(oldSelection.base(), oldSelection.extent()) + : flutter::TextRange(range.location, range.location + range.length); + [self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange, replacedRange, + utf8String)]; + } else { + [self updateEditState]; + } } - (void)doCommandBySelector:(SEL)selector { @@ -515,16 +603,23 @@ - (void)setMarkedText:(id)string if (_activeModel == nullptr) { return; } + std::string textBeforeChange = _activeModel->GetText().c_str(); if (!_activeModel->composing()) { _activeModel->BeginComposing(); } // Input string may be NSString or NSAttributedString. BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]]; - NSString* marked_text = isAttributedString ? [string string] : string; - _activeModel->UpdateComposingText([marked_text UTF8String]); + std::string marked_text = isAttributedString ? [[string string] UTF8String] : [string UTF8String]; + _activeModel->UpdateComposingText(marked_text); - [self updateEditState]; + if (_enableDeltaModel) { + flutter::TextRange composing = _activeModel->composing_range(); + [self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange, composing, + marked_text)]; + } else { + [self updateEditState]; + } } - (void)unmarkText { @@ -533,7 +628,11 @@ - (void)unmarkText { } _activeModel->CommitComposing(); _activeModel->EndComposing(); - [self updateEditState]; + if (_enableDeltaModel) { + [self updateEditStateWithDelta:flutter::TextEditingDelta(_activeModel->GetText().c_str())]; + } else { + [self updateEditState]; + } } - (NSRange)markedRange { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index b7b8c31fa4c54..4f0c3002f08e2 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -176,6 +176,179 @@ - (bool)testFirstRectForCharacterRange { return NSEqualRects(rect, NSMakeRect(38, 20, 2, 19)); } +- (bool)testSetEditingStateWithTextEditingDelta { + id engineMock = OCMClassMock([FlutterEngine class]); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [engineMock binaryMessenger]) + .andReturn(binaryMessengerMock); + + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock + nibName:@"" + bundle:nil]; + + FlutterTextInputPlugin* plugin = + [[FlutterTextInputPlugin alloc] initWithViewController:viewController]; + + [plugin handleMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ + @(1), @{ + @"inputAction" : @"action", + @"enableDeltaModel" : @"true", + @"inputType" : @{@"name" : @"inputName"}, + } + ]] + result:^(id){ + }]; + + FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState" + arguments:@{ + @"text" : @"Text", + @"selectionBase" : @(0), + @"selectionExtent" : @(0), + @"composingBase" : @(-1), + @"composingExtent" : @(-1), + }]; + + [plugin handleMethodCall:call + result:^(id){ + }]; + + // The setEditingState call is ACKed back to the framework. + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock + sendOnChannel:@"flutter/textinput" + message:[OCMArg checkWithBlock:^BOOL(NSData* callData) { + FlutterMethodCall* call = + [[FlutterJSONMethodCodec sharedInstance] decodeMethodCall:callData]; + return [[call method] + isEqualToString:@"TextInputClient.updateEditingStateWithDeltas"]; + }]]); + } @catch (...) { + return false; + } + return true; +} + +- (bool)testOperationsThatTriggerDelta { + id engineMock = OCMClassMock([FlutterEngine class]); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [engineMock binaryMessenger]) + .andReturn(binaryMessengerMock); + + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock + nibName:@"" + bundle:nil]; + + FlutterTextInputPlugin* plugin = + [[FlutterTextInputPlugin alloc] initWithViewController:viewController]; + + [plugin handleMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ + @(1), @{ + @"inputAction" : @"action", + @"enableDeltaModel" : @"true", + @"inputType" : @{@"name" : @"inputName"}, + } + ]] + result:^(id){ + }]; + [plugin insertText:@"text to insert"]; + + NSDictionary* deltaToFramework = @{ + @"oldText" : @"", + @"deltaText" : @"text to insert", + @"deltaStart" : @(0), + @"deltaEnd" : @(0), + @"selectionBase" : @(14), + @"selectionExtent" : @(14), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(false), + @"composingBase" : @(-1), + @"composingExtent" : @(-1), + }; + NSDictionary* expectedState = @{ + @"deltas" : @[ deltaToFramework ], + }; + + NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas" + arguments:@[ @(1), expectedState ]]]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + + [plugin setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)]; + + deltaToFramework = @{ + @"oldText" : @"text to insert", + @"deltaText" : @"marked text", + @"deltaStart" : @(14), + @"deltaEnd" : @(25), + @"selectionBase" : @(25), + @"selectionExtent" : @(25), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(false), + @"composingBase" : @(14), + @"composingExtent" : @(25), + }; + expectedState = @{ + @"deltas" : @[ deltaToFramework ], + }; + + updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas" + arguments:@[ @(1), expectedState ]]]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + + [plugin unmarkText]; + + deltaToFramework = @{ + @"oldText" : @"text to insertmarked text", + @"deltaText" : @"", + @"deltaStart" : @(-1), + @"deltaEnd" : @(-1), + @"selectionBase" : @(25), + @"selectionExtent" : @(25), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(false), + @"composingBase" : @(-1), + @"composingExtent" : @(-1), + }; + expectedState = @{ + @"deltas" : @[ deltaToFramework ], + }; + + updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas" + arguments:@[ @(1), expectedState ]]]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + return true; +} + @end namespace flutter::testing { @@ -199,6 +372,14 @@ - (bool)testFirstRectForCharacterRange { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testFirstRectForCharacterRange]); } +TEST(FlutterTextInputPluginTest, TestSetEditingStateWithTextEditingDelta) { + ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSetEditingStateWithTextEditingDelta]); +} + +TEST(FlutterTextInputPluginTest, TestOperationsThatTriggerDelta) { + ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testOperationsThatTriggerDelta]); +} + TEST(FlutterTextInputPluginTest, CanWorkWithFlutterTextField) { FlutterEngine* engine = CreateTestEngine(); NSString* fixtures = @(testing::GetFixturesPath()); diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextureRegistrar.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextureRegistrar.mm index a233c4b88fcdb..206eb20b8d1b0 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextureRegistrar.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextureRegistrar.mm @@ -15,12 +15,6 @@ @implementation FlutterTextureRegistrar { NSMutableDictionary>* _textures; } -- (instancetype)init { - @throw([NSException exceptionWithName:@"FlutterTextureRegistrar must initWithDelegate" - reason:nil - userInfo:nil]); -} - - (instancetype)initWithDelegate:(id)delegate engine:(FlutterEngine*)engine { if (self = [super init]) { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index ab9d8833bf43b..2bd004cc373f4 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -138,6 +138,11 @@ - (void)configureTrackingArea; */ - (void)addInternalPlugins; +/** + * Creates and registers keyboard related components. + */ +- (void)initializeKeyboard; + /** * Calls dispatchMouseEvent:phase: with a phase determined by self.mouseState. * @@ -249,6 +254,10 @@ static void CommonInit(FlutterViewController* controller) { selector:@selector(onAccessibilityStatusChanged:) name:EnhancedUserInterfaceNotification object:nil]; + [center addObserver:controller + selector:@selector(applicationWillTerminate:) + name:NSApplicationWillTerminateNotification + object:nil]; } - (instancetype)initWithCoder:(NSCoder*)coder { @@ -358,6 +367,10 @@ - (void)setMouseTrackingMode:(FlutterMouseTrackingMode)mode { [self configureTrackingArea]; } +- (void)onPreEngineRestart { + [self initializeKeyboard]; +} + #pragma mark - Private methods - (BOOL)launchEngine { @@ -391,8 +404,9 @@ - (void)listenForMetaModifiedKeyUpEvents { ([[event window] firstResponder] == weakSelf.flutterView) && ([event modifierFlags] & NSEventModifierFlagCommand) && - ([event type] == NSEventTypeKeyUp)) + ([event type] == NSEventTypeKeyUp)) { [weakSelf keyUp:event]; + } return event; }]; } @@ -431,9 +445,9 @@ - (void)configureTrackingArea { } } -- (void)addInternalPlugins { +- (void)initializeKeyboard { __weak FlutterViewController* weakSelf = self; - [FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"]]; + _textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:weakSelf]; _keyboardManager = [[FlutterKeyboardManager alloc] initWithOwner:weakSelf]; [_keyboardManager addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc] initWithSendEvent:^(const FlutterKeyEvent& event, @@ -451,6 +465,12 @@ - (void)addInternalPlugins { codec:[FlutterJSONMessageCodec sharedInstance]]]]; [_keyboardManager addSecondaryResponder:_textInputPlugin]; +} + +- (void)addInternalPlugins { + __weak FlutterViewController* weakSelf = self; + [FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"]]; + [self initializeKeyboard]; _settingsChannel = [FlutterBasicMessageChannel messageChannelWithName:@"flutter/settings" binaryMessenger:_engine.binaryMessenger @@ -542,6 +562,13 @@ - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase { } } +- (void)applicationWillTerminate:(NSNotification*)notification { + if (!_engine) { + return; + } + [_engine shutDownEngine]; +} + - (void)onAccessibilityStatusChanged:(NSNotification*)notification { if (!_engine) { return; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm index 9ba946617816b..4432da813c094 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm @@ -20,6 +20,7 @@ - (bool)testKeyEventsArePropagatedIfNotHandled; - (bool)testKeyEventsAreNotPropagatedIfHandled; - (bool)testFlagsChangedEventsArePropagatedIfNotHandled; - (bool)testPerformKeyEquivalentSynthesizesKeyUp; +- (bool)testKeyboardIsRestartedOnEngineRestart; + (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event callback:(nullable FlutterKeyEventCallback)callback @@ -171,6 +172,10 @@ + (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testPerformKeyEquivalentSynthesizesKeyUp]); } +TEST(FlutterViewControllerTest, TestKeyboardIsRestartedOnEngineRestart) { + ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testKeyboardIsRestartedOnEngineRestart]); +} + } // namespace flutter::testing @implementation FlutterViewControllerTestObjC @@ -456,11 +461,66 @@ - (bool)testPerformKeyEquivalentSynthesizesKeyUp { return true; } +- (bool)testKeyboardIsRestartedOnEngineRestart { + id engineMock = OCMClassMock([FlutterEngine class]); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [engineMock binaryMessenger]) + .andReturn(binaryMessengerMock); + __block bool called = false; + __block FlutterKeyEvent last_event; + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + callback:nil + userData:nil]) + .andDo((^(NSInvocation* invocation) { + FlutterKeyEvent* event; + [invocation getArgument:&event atIndex:2]; + called = true; + last_event = *event; + })); + + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock + nibName:@"" + bundle:nil]; + [viewController viewWillAppear]; + NSEvent* keyADown = [NSEvent keyEventWithType:NSEventTypeKeyDown + location:NSZeroPoint + modifierFlags:0x100 + timestamp:0 + windowNumber:0 + context:nil + characters:@"a" + charactersIgnoringModifiers:@"a" + isARepeat:FALSE + keyCode:0]; + const uint64_t kPhysicalKeyA = 0x70004; + + // Send KeyA key down event twice. Without restarting the keyboard during + // onPreEngineRestart, the second event received will be an empty event with + // physical key 0x0 because duplicate key down events are ignored. + + called = false; + [viewController keyDown:keyADown]; + EXPECT_TRUE(called); + EXPECT_EQ(last_event.type, kFlutterKeyEventTypeDown); + EXPECT_EQ(last_event.physical, kPhysicalKeyA); + + [viewController onPreEngineRestart]; + + called = false; + [viewController keyDown:keyADown]; + EXPECT_TRUE(called); + EXPECT_EQ(last_event.type, kFlutterKeyEventTypeDown); + EXPECT_EQ(last_event.physical, kPhysicalKeyA); + return true; +} + + (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event callback:(nullable FlutterKeyEventCallback)callback userData:(nullable void*)userData { - if (callback != nullptr) + if (callback != nullptr) { callback(false, userData); + } } @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.mm index 972e3515283ed..4cae427e1ea2d 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.mm @@ -27,4 +27,4 @@ id CreateMockViewController(NSString* pasteboardString) { } } -} +} // namespace flutter::testing diff --git a/shell/platform/embedder/BUILD.gn b/shell/platform/embedder/BUILD.gn index 767d98fff258b..529f48a7ebd41 100644 --- a/shell/platform/embedder/BUILD.gn +++ b/shell/platform/embedder/BUILD.gn @@ -242,6 +242,13 @@ if (enable_unittests) { deps += [ "//flutter/testing:metal" ] } + + if (test_enable_vulkan) { + deps += [ + "//flutter/testing:vulkan", + "//flutter/vulkan", + ] + } } # Tests the build in FLUTTER_ENGINE_NO_PROTOTYPES mode. diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index dfe40017dfb41..5c00230ba60f2 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -69,6 +69,26 @@ extern const intptr_t kPlatformStrongDillSize; const int32_t kFlutterSemanticsNodeIdBatchEnd = -1; const int32_t kFlutterSemanticsCustomActionIdBatchEnd = -1; +// A message channel to send platform-independent FlutterKeyData to the +// framework. +// +// This should be kept in sync with the following variables: +// +// - lib/ui/platform_dispatcher.dart, _kFlutterKeyDataChannel +// - shell/platform/darwin/ios/framework/Source/FlutterEngine.mm, +// FlutterKeyDataChannel +// +// Not to be confused with "flutter/keyevent", which is used to send raw +// key event data in a platform-dependent format. +// +// ## Format +// +// Send: KeyDataPacket.data(). +// +// Expected reply: Whether the event is handled. Exactly 1 byte long, with value +// 1 for handled, and 0 for not. Malformed value is considered false. +const char* kFlutterKeyDataChannel = "flutter/keydata"; + static FlutterEngineResult LogEmbedderError(FlutterEngineResult code, const char* reason, const char* code_name, @@ -1052,10 +1072,10 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, node.rect.fBottom}, flutter_transform, node.childrenInTraversalOrder.size(), - &node.childrenInTraversalOrder[0], - &node.childrenInHitTestOrder[0], + node.childrenInTraversalOrder.data(), + node.childrenInHitTestOrder.data(), node.customAccessibilityActions.size(), - &node.customAccessibilityActions[0], + node.customAccessibilityActions.data(), node.platformViewId, }; ptr(&embedder_node, user_data); @@ -1593,6 +1613,45 @@ static inline flutter::KeyEventType MapKeyEventType( return flutter::KeyEventType::kUp; } +// Send a platform message to the framework. +// +// The `data_callback` will be invoked with `user_data`, and must not be empty. +static FlutterEngineResult InternalSendPlatformMessage( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + const char* channel, + const uint8_t* data, + size_t size, + FlutterDataCallback data_callback, + void* user_data) { + FlutterEngineResult result; + + FlutterPlatformMessageResponseHandle* response_handle; + result = FlutterPlatformMessageCreateResponseHandle( + engine, data_callback, user_data, &response_handle); + if (result != kSuccess) { + return result; + } + + const FlutterPlatformMessage message = { + sizeof(FlutterPlatformMessage), // struct_size + channel, // channel + data, // message + size, // message_size + response_handle, // response_handle + }; + + result = FlutterEngineSendPlatformMessage(engine, &message); + // Whether `SendPlatformMessage` succeeds or not, the response handle must be + // released. + FlutterEngineResult release_result = + FlutterPlatformMessageReleaseResponseHandle(engine, response_handle); + if (result != kSuccess) { + return result; + } + + return release_result; +} + FlutterEngineResult FlutterEngineSendKeyEvent(FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterKeyEvent* event, @@ -1619,18 +1678,27 @@ FlutterEngineResult FlutterEngineSendKeyEvent(FLUTTER_API_SYMBOL(FlutterEngine) auto packet = std::make_unique(key_data, character); - auto response = [callback, user_data](bool handled) { - if (callback != nullptr) { - callback(handled, user_data); - } + struct MessageData { + FlutterKeyEventCallback callback; + void* user_data; }; - return reinterpret_cast(engine) - ->DispatchKeyDataPacket(std::move(packet), response) - ? kSuccess - : LOG_EMBEDDER_ERROR(kInternalInconsistency, - "Could not dispatch the key event to the " - "running Flutter application."); + MessageData* message_data = + new MessageData{.callback = callback, .user_data = user_data}; + + return InternalSendPlatformMessage( + engine, kFlutterKeyDataChannel, packet->data().data(), + packet->data().size(), + [](const uint8_t* data, size_t size, void* user_data) { + auto message_data = std::unique_ptr( + reinterpret_cast(user_data)); + bool handled = false; + if (size == 1) { + handled = *data != 0; + } + message_data->callback(handled, message_data->user_data); + }, + message_data); } FlutterEngineResult FlutterEngineSendPlatformMessage( @@ -2267,18 +2335,19 @@ FlutterEngineResult FlutterEngineNotifyDisplayUpdate( switch (update_type) { case kFlutterEngineDisplaysUpdateTypeStartup: { - std::vector displays; + std::vector> displays; for (size_t i = 0; i < display_count; i++) { - flutter::Display display = - flutter::Display(embedder_displays[i].refresh_rate); - if (!embedder_displays[i].single_display) { - display = flutter::Display(embedder_displays[i].display_id, - embedder_displays[i].refresh_rate); + if (embedder_displays[i].single_display) { + displays.push_back(std::make_unique( + embedder_displays[i].refresh_rate)); + } else { + displays.push_back(std::make_unique( + embedder_displays[i].display_id, + embedder_displays[i].refresh_rate)); } - displays.push_back(display); } engine->GetShell().OnDisplayUpdates(flutter::DisplayUpdateType::kStartup, - displays); + std::move(displays)); return kSuccess; } default: diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index d12d2a5a396fe..e8b769ef4cbef 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -372,7 +372,7 @@ typedef uint32_t (*UIntFrameInfoCallback)( /// /// See: \ref FlutterOpenGLRendererConfig.present_with_info. typedef struct { - /// The size of this struct. Must be sizeof(FlutterFrameInfo). + /// The size of this struct. Must be sizeof(FlutterPresentInfo). size_t struct_size; /// Id of the fbo backing the surface that was presented. uint32_t fbo_id; @@ -1395,9 +1395,10 @@ typedef struct { /// `switches.h` engine source file. const char* const* command_line_argv; /// The callback invoked by the engine in order to give the embedder the - /// chance to respond to platform messages from the Dart application. The - /// callback will be invoked on the thread on which the `FlutterEngineRun` - /// call is made. + /// chance to respond to platform messages from the Dart application. + /// The callback will be invoked on the thread on which the `FlutterEngineRun` + /// call is made. The second parameter, `user_data`, is supplied when + /// `FlutterEngineRun` or `FlutterEngineInitialize` is called. FlutterPlatformMessageCallback platform_message_callback; /// The VM snapshot data buffer used in AOT operation. This buffer must be /// mapped in as read-only. For more information refer to the documentation on @@ -1579,7 +1580,7 @@ typedef struct { // callbacks on `log_message_callback`. Defaults to "flutter" if unspecified. const char* log_tag; - // A callback that is invoked when the engine is restarted. + // A callback that is invoked right before the engine is restarted. // // This optional callback is typically used to reset states to as if the // engine has just been started, and usually indicates the user has requested diff --git a/shell/platform/embedder/embedder_engine.cc b/shell/platform/embedder/embedder_engine.cc index 7d1f74bdb3897..5d8d785d17737 100644 --- a/shell/platform/embedder/embedder_engine.cc +++ b/shell/platform/embedder/embedder_engine.cc @@ -127,22 +127,6 @@ bool EmbedderEngine::DispatchPointerDataPacket( return true; } -bool EmbedderEngine::DispatchKeyDataPacket( - std::unique_ptr packet, - KeyDataResponse callback) { - if (!IsValid() || !packet) { - return false; - } - - auto platform_view = shell_->GetPlatformView(); - if (!platform_view) { - return false; - } - - platform_view->DispatchKeyDataPacket(std::move(packet), std::move(callback)); - return true; -} - bool EmbedderEngine::SendPlatformMessage( std::unique_ptr message) { if (!IsValid() || !message) { @@ -229,8 +213,8 @@ bool EmbedderEngine::OnVsyncEvent(intptr_t baton, return false; } - return VsyncWaiterEmbedder::OnEmbedderVsync(baton, frame_start_time, - frame_target_time); + return VsyncWaiterEmbedder::OnEmbedderVsync( + task_runners_, baton, frame_start_time, frame_target_time); } bool EmbedderEngine::ReloadSystemFonts() { diff --git a/shell/platform/embedder/embedder_engine.h b/shell/platform/embedder/embedder_engine.h index 30b0dd87a4155..d65bc81973a56 100644 --- a/shell/platform/embedder/embedder_engine.h +++ b/shell/platform/embedder/embedder_engine.h @@ -52,22 +52,6 @@ class EmbedderEngine { bool DispatchPointerDataPacket( std::unique_ptr packet); - //---------------------------------------------------------------------------- - /// @brief Notifies the platform view that the embedder has sent it a key - /// data packet. A key data packet contains one key event. This - /// call originates in the platform view and the shell has - /// forwarded the same to the engine on the UI task runner here. - /// The platform view will decide whether to handle this event, - /// and send the result using `callback`, which will be called - /// exactly once. - /// - /// @param[in] packet The key data packet. - /// @param[in] callback Called when the framework has decided whether - /// to handle this key data. - /// - bool DispatchKeyDataPacket(std::unique_ptr packet, - KeyDataResponse callback); - bool SendPlatformMessage(std::unique_ptr message); bool RegisterTexture(int64_t texture); diff --git a/shell/platform/embedder/embedder_external_view.h b/shell/platform/embedder/embedder_external_view.h index 261d4db2b135c..59fcf4260b2cf 100644 --- a/shell/platform/embedder/embedder_external_view.h +++ b/shell/platform/embedder/embedder_external_view.h @@ -26,7 +26,8 @@ class EmbedderExternalView { ViewIdentifier() {} - ViewIdentifier(PlatformViewID view_id) : platform_view_id(view_id) {} + explicit ViewIdentifier(PlatformViewID view_id) + : platform_view_id(view_id) {} struct Hash { constexpr std::size_t operator()(const ViewIdentifier& desc) const { diff --git a/shell/platform/embedder/embedder_external_view_embedder.cc b/shell/platform/embedder/embedder_external_view_embedder.cc index a890260887e26..14927f4833be5 100644 --- a/shell/platform/embedder/embedder_external_view_embedder.cc +++ b/shell/platform/embedder/embedder_external_view_embedder.cc @@ -72,15 +72,16 @@ void EmbedderExternalViewEmbedder::BeginFrame( void EmbedderExternalViewEmbedder::PrerollCompositeEmbeddedView( int view_id, std::unique_ptr params) { - FML_DCHECK(pending_views_.count(view_id) == 0); + auto vid = EmbedderExternalView::ViewIdentifier(view_id); + FML_DCHECK(pending_views_.count(vid) == 0); - pending_views_[view_id] = std::make_unique( + pending_views_[vid] = std::make_unique( pending_frame_size_, // frame size pending_surface_transformation_, // surface xformation - view_id, // view identifier + vid, // view identifier std::move(params) // embedded view params ); - composition_order_.push_back(view_id); + composition_order_.push_back(vid); } // |ExternalViewEmbedder| @@ -111,7 +112,8 @@ std::vector EmbedderExternalViewEmbedder::GetCurrentCanvases() { // |ExternalViewEmbedder| SkCanvas* EmbedderExternalViewEmbedder::CompositeEmbeddedView(int view_id) { - auto found = pending_views_.find(view_id); + auto vid = EmbedderExternalView::ViewIdentifier(view_id); + auto found = pending_views_.find(vid); if (found == pending_views_.end()) { FML_DCHECK(false) << "Attempted to composite a view that was not " "pre-rolled."; diff --git a/shell/platform/embedder/fixtures/main.dart b/shell/platform/embedder/fixtures/main.dart index f1f8ca51a7093..5196f37727597 100644 --- a/shell/platform/embedder/fixtures/main.dart +++ b/shell/platform/embedder/fixtures/main.dart @@ -567,6 +567,27 @@ void key_data_echo() async { signalNativeTest(); } +// After platform channel 'test/starts_echo' receives a message, starts echoing +// the event data with `_echoKeyEvent`, and returns synthesized as handled. +@pragma('vm:entry-point') +void key_data_late_echo() async { + channelBuffers.setListener('test/starts_echo', (ByteData? data, PlatformMessageResponseCallback callback) { + PlatformDispatcher.instance.onKeyData = (KeyData data) { + _echoKeyEvent( + _serializeKeyEventType(data.type), + data.timeStamp.inMicroseconds, + data.physical, + data.logical, + data.character == null ? 0 : data.character!.codeUnitAt(0), + data.synthesized, + ); + return data.synthesized; + }; + callback(null); + }); + signalNativeTest(); +} + @pragma('vm:entry-point') void render_gradient() { PlatformDispatcher.instance.onBeginFrame = (Duration duration) { diff --git a/shell/platform/embedder/tests/embedder_a11y_unittests.cc b/shell/platform/embedder/tests/embedder_a11y_unittests.cc index bf5b8b6d045fa..9f06542b9d578 100644 --- a/shell/platform/embedder/tests/embedder_a11y_unittests.cc +++ b/shell/platform/embedder/tests/embedder_a11y_unittests.cc @@ -21,8 +21,7 @@ namespace testing { using Embedder11yTest = testing::EmbedderTest; -// TODO(52372): De-flake and re-enable. -TEST_F(Embedder11yTest, DISABLED_A11yTreeIsConsistent) { +TEST_F(Embedder11yTest, A11yTreeIsConsistent) { auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); fml::AutoResetWaitableEvent latch; @@ -188,6 +187,7 @@ TEST_F(Embedder11yTest, DISABLED_A11yTreeIsConsistent) { std::vector bytes({2, 1}); result = FlutterEngineDispatchSemanticsAction( engine.get(), 42, kFlutterSemanticsActionTap, &bytes[0], bytes.size()); + ASSERT_EQ(result, FlutterEngineResult::kSuccess); latch.Wait(); // Disable semantics. Wait for NotifySemanticsEnabled(false). diff --git a/shell/platform/embedder/tests/embedder_config_builder.cc b/shell/platform/embedder/tests/embedder_config_builder.cc index ac6c86a7e5089..d04188e0d3070 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.cc +++ b/shell/platform/embedder/tests/embedder_config_builder.cc @@ -261,6 +261,13 @@ void EmbedderConfigBuilder::SetPlatformTaskRunner( project_args_.custom_task_runners = &custom_task_runners_; } +void EmbedderConfigBuilder::SetupVsyncCallback() { + project_args_.vsync_callback = [](void* user_data, intptr_t baton) { + auto context = reinterpret_cast(user_data); + context->RunVsyncCallback(baton); + }; +} + void EmbedderConfigBuilder::SetRenderTaskRunner( const FlutterTaskRunnerDescription* runner) { if (runner == nullptr) { diff --git a/shell/platform/embedder/tests/embedder_config_builder.h b/shell/platform/embedder/tests/embedder_config_builder.h index 40f9bfc85a560..ee1fb514bb611 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.h +++ b/shell/platform/embedder/tests/embedder_config_builder.h @@ -36,9 +36,10 @@ class EmbedderConfigBuilder { kNoInitialize, }; - EmbedderConfigBuilder(EmbedderTestContext& context, - InitializationPreference preference = - InitializationPreference::kSnapshotsInitialize); + explicit EmbedderConfigBuilder( + EmbedderTestContext& context, + InitializationPreference preference = + InitializationPreference::kSnapshotsInitialize); ~EmbedderConfigBuilder(); @@ -104,6 +105,10 @@ class EmbedderConfigBuilder { UniqueEngine InitializeEngine() const; + // Sets up the callback for vsync, the callbacks needs to be specified on the + // text context vis `SetVsyncCallback`. + void SetupVsyncCallback(); + private: EmbedderTestContext& context_; FlutterProjectArgs project_args_ = {}; diff --git a/shell/platform/embedder/tests/embedder_test_compositor_software.h b/shell/platform/embedder/tests/embedder_test_compositor_software.h index 8c38c86079975..ffd007fff0fdb 100644 --- a/shell/platform/embedder/tests/embedder_test_compositor_software.h +++ b/shell/platform/embedder/tests/embedder_test_compositor_software.h @@ -12,7 +12,7 @@ namespace testing { class EmbedderTestCompositorSoftware : public EmbedderTestCompositor { public: - EmbedderTestCompositorSoftware(SkISize surface_size); + explicit EmbedderTestCompositorSoftware(SkISize surface_size); ~EmbedderTestCompositorSoftware() override; diff --git a/shell/platform/embedder/tests/embedder_test_context.cc b/shell/platform/embedder/tests/embedder_test_context.cc index 44d39a3326a96..79eb749e632a9 100644 --- a/shell/platform/embedder/tests/embedder_test_context.cc +++ b/shell/platform/embedder/tests/embedder_test_context.cc @@ -224,5 +224,14 @@ void EmbedderTestContext::FireRootSurfacePresentCallbackIfPresent( callback(image_callback()); } +void EmbedderTestContext::SetVsyncCallback( + std::function callback) { + vsync_callback_ = callback; +} + +void EmbedderTestContext::RunVsyncCallback(intptr_t baton) { + vsync_callback_(baton); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/embedder/tests/embedder_test_context.h b/shell/platform/embedder/tests/embedder_test_context.h index 366b5b0b52cfe..3f4f6d06b9c7d 100644 --- a/shell/platform/embedder/tests/embedder_test_context.h +++ b/shell/platform/embedder/tests/embedder_test_context.h @@ -48,7 +48,7 @@ enum class EmbedderTestContextType { class EmbedderTestContext { public: - EmbedderTestContext(std::string assets_path = ""); + explicit EmbedderTestContext(std::string assets_path = ""); virtual ~EmbedderTestContext(); @@ -89,6 +89,15 @@ class EmbedderTestContext { virtual EmbedderTestContextType GetContextType() const = 0; + // Sets up the callback for vsync. This callback will be invoked + // for every vsync. This should be used in conjunction with SetupVsyncCallback + // on the EmbedderConfigBuilder. Any callback setup here must call + // `FlutterEngineOnVsync` from the platform task runner. + void SetVsyncCallback(std::function callback); + + // Runs the vsync callback. + void RunVsyncCallback(intptr_t baton); + // TODO(gw280): encapsulate these properly for subclasses to use protected: // This allows the builder to access the hooks. @@ -112,6 +121,7 @@ class EmbedderTestContext { std::unique_ptr compositor_; NextSceneCallback next_scene_callback_; SkMatrix root_surface_transformation_; + std::function vsync_callback_ = nullptr; static VoidCallback GetIsolateCreateCallbackHook(); diff --git a/shell/platform/embedder/tests/embedder_test_context_gl.h b/shell/platform/embedder/tests/embedder_test_context_gl.h index 6ab911f07952c..65e2c34c355c2 100644 --- a/shell/platform/embedder/tests/embedder_test_context_gl.h +++ b/shell/platform/embedder/tests/embedder_test_context_gl.h @@ -16,7 +16,7 @@ class EmbedderTestContextGL : public EmbedderTestContext { using GLGetFBOCallback = std::function; using GLPresentCallback = std::function; - EmbedderTestContextGL(std::string assets_path = ""); + explicit EmbedderTestContextGL(std::string assets_path = ""); ~EmbedderTestContextGL() override; diff --git a/shell/platform/embedder/tests/embedder_test_context_software.h b/shell/platform/embedder/tests/embedder_test_context_software.h index 7f5b815c2944a..f8690484d0119 100644 --- a/shell/platform/embedder/tests/embedder_test_context_software.h +++ b/shell/platform/embedder/tests/embedder_test_context_software.h @@ -12,7 +12,7 @@ namespace testing { class EmbedderTestContextSoftware : public EmbedderTestContext { public: - EmbedderTestContextSoftware(std::string assets_path = ""); + explicit EmbedderTestContextSoftware(std::string assets_path = ""); ~EmbedderTestContextSoftware() override; diff --git a/shell/platform/embedder/tests/embedder_unittests.cc b/shell/platform/embedder/tests/embedder_unittests.cc index ea9904d9c0e1f..95d765a6cd254 100644 --- a/shell/platform/embedder/tests/embedder_unittests.cc +++ b/shell/platform/embedder/tests/embedder_unittests.cc @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "fml/task_runner.h" #define FML_USED_ON_EMBEDDER #include @@ -18,7 +17,10 @@ #include "flutter/fml/paths.h" #include "flutter/fml/synchronization/count_down_latch.h" #include "flutter/fml/synchronization/waitable_event.h" +#include "flutter/fml/task_runner.h" #include "flutter/fml/thread.h" +#include "flutter/fml/time/time_delta.h" +#include "flutter/fml/time/time_point.h" #include "flutter/runtime/dart_vm.h" #include "flutter/shell/platform/embedder/tests/embedder_assertions.h" #include "flutter/shell/platform/embedder/tests/embedder_config_builder.h" @@ -29,6 +31,16 @@ #include "third_party/skia/include/core/SkSurface.h" #include "third_party/tonic/converter/dart_converter.h" +namespace { + +static uint64_t NanosFromEpoch(int millis_from_now) { + const auto now = fml::TimePoint::Now(); + const auto delta = fml::TimeDelta::FromMilliseconds(millis_from_now); + return (now + delta).ToEpochDelta().ToNanoseconds(); +} + +} // namespace + namespace flutter { namespace testing { @@ -1093,7 +1105,7 @@ TEST_F(EmbedderTest, CanPostTaskToAllNativeThreads) { size_t ui_threads_count = 0; size_t worker_threads_count = 0; - Captures(size_t count) : latch(count) {} + explicit Captures(size_t count) : latch(count) {} }; Captures captures(engine_threads_count); @@ -1343,7 +1355,7 @@ TEST_F(EmbedderTest, KeyDataIsCorrectlySerialized) { echoed_event.type = UnserializeKeyEventKind(tonic::DartConverter::FromDart( Dart_GetNativeArgument(args, 0))); - echoed_event.timestamp = tonic::DartConverter::FromDart( + echoed_event.timestamp = (double)tonic::DartConverter::FromDart( Dart_GetNativeArgument(args, 1)); echoed_event.physical = tonic::DartConverter::FromDart( Dart_GetNativeArgument(args, 2)); @@ -1428,6 +1440,99 @@ TEST_F(EmbedderTest, KeyDataIsCorrectlySerialized) { EXPECT_EQ(echoed_char, 0llu); } +TEST_F(EmbedderTest, KeyDataAreBuffered) { + auto message_latch = std::make_shared(); + std::vector echoed_events; + + auto native_echo_event = [&](Dart_NativeArguments args) { + echoed_events.push_back(FlutterKeyEvent{ + .timestamp = (double)tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 1)), + .type = + UnserializeKeyEventKind(tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0))), + .physical = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 2)), + .logical = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 3)), + .synthesized = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 5)), + }); + + message_latch->Signal(); + }; + + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("key_data_late_echo"); + fml::AutoResetWaitableEvent ready; + context.AddNativeCallback( + "SignalNativeTest", + CREATE_NATIVE_ENTRY( + [&ready](Dart_NativeArguments args) { ready.Signal(); })); + + context.AddNativeCallback("EchoKeyEvent", + CREATE_NATIVE_ENTRY(native_echo_event)); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + ready.Wait(); + + FlutterKeyEvent sample_event{ + .struct_size = sizeof(FlutterKeyEvent), + .type = kFlutterKeyEventTypeDown, + .physical = 0x00070004, + .logical = 0x00000000061, + .character = "A", + .synthesized = false, + }; + + // Send an event. + sample_event.timestamp = 1.0l; + FlutterEngineSendKeyEvent( + engine.get(), &sample_event, [](bool handled, void* user_data) {}, + nullptr); + + // Should not receive echos because the callback is not set yet. + EXPECT_EQ(echoed_events.size(), 0u); + + // Send an empty message to 'test/starts_echo' to start echoing. + FlutterPlatformMessageResponseHandle* response_handle = nullptr; + FlutterPlatformMessageCreateResponseHandle( + engine.get(), [](const uint8_t* data, size_t size, void* user_data) {}, + nullptr, &response_handle); + + FlutterPlatformMessage message{ + .struct_size = sizeof(FlutterPlatformMessage), + .channel = "test/starts_echo", + .message = nullptr, + .message_size = 0, + .response_handle = response_handle, + }; + + FlutterEngineResult result = + FlutterEngineSendPlatformMessage(engine.get(), &message); + ASSERT_EQ(result, kSuccess); + + FlutterPlatformMessageReleaseResponseHandle(engine.get(), response_handle); + + // message_latch->Wait(); + message_latch->Wait(); + // All previous events should be received now. + EXPECT_EQ(echoed_events.size(), 1u); + + // Send a second event. + sample_event.timestamp = 10.0l; + FlutterEngineSendKeyEvent( + engine.get(), &sample_event, [](bool handled, void* user_data) {}, + nullptr); + message_latch->Wait(); + + // The event should be echoed, too. + EXPECT_EQ(echoed_events.size(), 2u); +} + TEST_F(EmbedderTest, KeyDataResponseIsCorrectlyInvoked) { UniqueEngine engine; fml::AutoResetWaitableEvent sync_latch; @@ -1575,5 +1680,63 @@ TEST_F(EmbedderTest, BackToBackKeyEventResponsesCorrectlyInvoked) { shutdown_latch.Wait(); } +// This test schedules a frame for the future and asserts that vsync waiter +// posts the event at the right frame start time (which is in the future). +TEST_F(EmbedderTest, VsyncCallbackPostedIntoFuture) { + UniqueEngine engine; + fml::AutoResetWaitableEvent present_latch; + fml::AutoResetWaitableEvent vsync_latch; + + // One of the threads that the callback (FlutterEngineOnVsync) will be posted + // to is the platform thread. So we cannot wait for assertions to complete on + // the platform thread. Create a new thread to manage the engine instance and + // wait for assertions on the test thread. + auto platform_task_runner = CreateNewThread("platform_thread"); + + platform_task_runner->PostTask([&]() { + auto& context = + GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + + context.SetVsyncCallback([&](intptr_t baton) { + platform_task_runner->PostTask([baton = baton, &engine, &vsync_latch]() { + FlutterEngineOnVsync(engine.get(), baton, NanosFromEpoch(16), + NanosFromEpoch(32)); + vsync_latch.Signal(); + }); + }); + context.AddNativeCallback( + "SignalNativeTest", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + present_latch.Signal(); + })); + + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetupVsyncCallback(); + builder.SetDartEntrypoint("empty_scene"); + engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = 800; + event.height = 600; + event.pixel_ratio = 1.0; + + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + }); + + vsync_latch.Wait(); + present_latch.Wait(); + + fml::AutoResetWaitableEvent shutdown_latch; + platform_task_runner->PostTask([&]() { + engine.reset(); + shutdown_latch.Signal(); + }); + shutdown_latch.Wait(); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/embedder/tests/embedder_unittests_gl.cc b/shell/platform/embedder/tests/embedder_unittests_gl.cc index c79c6d20562f3..c8086191c8ba3 100644 --- a/shell/platform/embedder/tests/embedder_unittests_gl.cc +++ b/shell/platform/embedder/tests/embedder_unittests_gl.cc @@ -29,6 +29,7 @@ #include "flutter/shell/platform/embedder/tests/embedder_unittests_util.h" #include "flutter/testing/assertions_skia.h" #include "flutter/testing/test_gl_surface.h" +#include "flutter/testing/test_vulkan_context.h" #include "flutter/testing/testing.h" #include "third_party/skia/include/core/SkSurface.h" #include "third_party/skia/src/gpu/gl/GrGLDefines.h" @@ -39,6 +40,16 @@ namespace testing { using EmbedderTest = testing::EmbedderTest; +//------------------------------------------------------------------------------ +/// This is a sanity check to ensure Swiftshader Vulkan is working. Once Vulkan +/// support lands in the embedder API, it'll be tested via a new +/// EmbedderTestContext type/config. +/// +TEST_F(EmbedderTest, CanInitializeTestVulkanContext) { + TestVulkanContext ctx; + ASSERT_TRUE(ctx.IsValid()); +} + TEST_F(EmbedderTest, CanCreateOpenGLRenderingEngine) { EmbedderConfigBuilder builder( GetEmbedderContext(EmbedderTestContextType::kOpenGLContext)); @@ -2449,8 +2460,8 @@ TEST_F(EmbedderTest, ArcEndCapsAreDrawnCorrectly) { FlutterWindowMetricsEvent event = {}; event.struct_size = sizeof(event); - event.width = 1024; - event.height = 800; + event.width = 800; + event.height = 1024; event.pixel_ratio = 1.0; ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), kSuccess); diff --git a/shell/platform/embedder/vsync_waiter_embedder.cc b/shell/platform/embedder/vsync_waiter_embedder.cc index a3d483f51b41f..bf566929e5a6f 100644 --- a/shell/platform/embedder/vsync_waiter_embedder.cc +++ b/shell/platform/embedder/vsync_waiter_embedder.cc @@ -17,26 +17,35 @@ VsyncWaiterEmbedder::~VsyncWaiterEmbedder() = default; // |VsyncWaiter| void VsyncWaiterEmbedder::AwaitVSync() { auto* weak_waiter = new std::weak_ptr(shared_from_this()); - vsync_callback_(reinterpret_cast(weak_waiter)); + intptr_t baton = reinterpret_cast(weak_waiter); + vsync_callback_(baton); } // static -bool VsyncWaiterEmbedder::OnEmbedderVsync(intptr_t baton, - fml::TimePoint frame_start_time, - fml::TimePoint frame_target_time) { +bool VsyncWaiterEmbedder::OnEmbedderVsync( + const flutter::TaskRunners& task_runners, + intptr_t baton, + fml::TimePoint frame_start_time, + fml::TimePoint frame_target_time) { if (baton == 0) { return false; } - auto* weak_waiter = reinterpret_cast*>(baton); - auto strong_waiter = weak_waiter->lock(); - delete weak_waiter; + // If the time here is in the future, the contract for `FlutterEngineOnVsync` + // says that the engine will only process the frame when the time becomes + // current. + task_runners.GetUITaskRunner()->PostTaskForTime( + [frame_start_time, frame_target_time, baton]() { + std::weak_ptr* weak_waiter = + reinterpret_cast*>(baton); + auto vsync_waiter = weak_waiter->lock(); + delete weak_waiter; + if (vsync_waiter) { + vsync_waiter->FireCallback(frame_start_time, frame_target_time); + } + }, + frame_start_time); - if (!strong_waiter) { - return false; - } - - strong_waiter->FireCallback(frame_start_time, frame_target_time); return true; } diff --git a/shell/platform/embedder/vsync_waiter_embedder.h b/shell/platform/embedder/vsync_waiter_embedder.h index c1b06327c8fc3..24e98e433fd70 100644 --- a/shell/platform/embedder/vsync_waiter_embedder.h +++ b/shell/platform/embedder/vsync_waiter_embedder.h @@ -19,7 +19,8 @@ class VsyncWaiterEmbedder final : public VsyncWaiter { ~VsyncWaiterEmbedder() override; - static bool OnEmbedderVsync(intptr_t baton, + static bool OnEmbedderVsync(const flutter::TaskRunners& task_runners, + intptr_t baton, fml::TimePoint frame_start_time, fml::TimePoint frame_target_time); diff --git a/shell/platform/fuchsia/BUILD.gn b/shell/platform/fuchsia/BUILD.gn index cf4493e51797d..eeb62a167b7e1 100644 --- a/shell/platform/fuchsia/BUILD.gn +++ b/shell/platform/fuchsia/BUILD.gn @@ -6,6 +6,7 @@ assert(is_fuchsia) import("//build/fuchsia/sdk.gni") import("//flutter/common/config.gni") +import("//flutter/testing/testing.gni") import("//flutter/tools/fuchsia/dart.gni") import("//flutter/tools/fuchsia/fuchsia_host_bundle.gni") @@ -84,8 +85,10 @@ group("fuchsia") { ] } -group("tests") { - testonly = true +if (enable_unittests) { + group("tests") { + testonly = true - deps = [ "flutter:tests" ] + deps = [ "flutter:tests" ] + } } diff --git a/shell/platform/fuchsia/dart-pkg/zircon/BUILD.gn b/shell/platform/fuchsia/dart-pkg/zircon/BUILD.gn index cf4e8ba160410..39329c34cee50 100644 --- a/shell/platform/fuchsia/dart-pkg/zircon/BUILD.gn +++ b/shell/platform/fuchsia/dart-pkg/zircon/BUILD.gn @@ -3,6 +3,7 @@ # found in the LICENSE file. import("//build/fuchsia/sdk.gni") +import("//flutter/testing/testing.gni") config("zircon_config") { include_dirs = [ "." ] @@ -30,13 +31,14 @@ source_set("zircon") { "$fuchsia_sdk_root/pkg:async-loop-cpp", "$fuchsia_sdk_root/pkg:fdio", "$fuchsia_sdk_root/pkg:zx", - "../zircon_ffi", "//flutter/fml", "//flutter/third_party/tonic", ] } -source_set("test") { - testonly = true - deps = [ "test" ] +if (enable_unittests) { + source_set("test") { + testonly = true + deps = [ "test" ] + } } diff --git a/shell/platform/fuchsia/dart_runner/BUILD.gn b/shell/platform/fuchsia/dart_runner/BUILD.gn index 7f9ce59526f37..3d3fad3828380 100644 --- a/shell/platform/fuchsia/dart_runner/BUILD.gn +++ b/shell/platform/fuchsia/dart_runner/BUILD.gn @@ -10,14 +10,6 @@ import("//flutter/tools/fuchsia/dart.gni") import("//flutter/tools/fuchsia/fuchsia_archive.gni") import("//flutter/tools/fuchsia/fuchsia_libs.gni") -# Loaded via FFI -_common_runner_libs = common_libs + [ - { - name = "libzircon_ffi.so" - path = rebase_path("$root_out_dir") - }, - ] - template("runner") { assert(defined(invoker.product), "The parameter 'product' must be defined") assert(defined(invoker.output_name), @@ -50,9 +42,6 @@ template("runner") { defines = extra_defines - # For `libzircon_ffi` see _common_runner_libs. - public_deps = [ "../dart-pkg/zircon_ffi:zircon_ffi" ] - dart_deps = [] if (!invoker.product) { dart_deps += [ @@ -155,7 +144,7 @@ template("aot_runner_package") { "//flutter/shell/platform/fuchsia/runtime/dart/profiler_symbols:dart_aot_runner", # TODO(kaushikiska): Figure out how to get the profiler symbols for `libdart_precompiled_runtime` - # "//topaz/runtime/dart/profiler_symbols:libdart_precompiled_runtime", + # "//third_party/dart/runtime:libdart_precompiled_runtime", observatory_target, ] } @@ -165,7 +154,7 @@ template("aot_runner_package") { cmx_file = rebase_path("meta/dart_aot${product_suffix}_runner.cmx") cml_file = rebase_path("meta/dart_aot${product_suffix}_runner.cml") - libraries = _common_runner_libs + libraries = common_libs resources = [] if (!invoker.product) { @@ -226,7 +215,7 @@ template("jit_runner_package") { cmx_file = rebase_path("meta/dart_jit${product_suffix}_runner.cmx") cml_file = rebase_path("meta/dart_jit${product_suffix}_runner.cml") - libraries = _common_runner_libs + libraries = common_libs resources = [ { diff --git a/shell/platform/fuchsia/dart_runner/dart_component_controller_v2.cc b/shell/platform/fuchsia/dart_runner/dart_component_controller_v2.cc index 206f7ffa0c0b7..90d0b9af8e79a 100644 --- a/shell/platform/fuchsia/dart_runner/dart_component_controller_v2.cc +++ b/shell/platform/fuchsia/dart_runner/dart_component_controller_v2.cc @@ -354,7 +354,20 @@ void DartComponentControllerV2::Run() { loop_->Run(); if (binding_.is_bound()) { - binding_.Close(return_code_); + // From the documentation for ComponentController, ZX_OK should be sent when + // the ComponentController receives a termination request. However, if the + // component exited with a non-zero return code, we indicate this by sending + // an INTERNAL epitaph instead. + // + // TODO(fxb/86666): Communicate return code from the ComponentController + // once v2 has support. + if (return_code_ == 0) { + binding_.Close(ZX_OK); + } else { + FML_LOG(ERROR) << "Component exited with non-zero return code: " + << return_code_; + binding_.Close(zx_status_t(fuchsia::component::Error::INTERNAL)); + } } } @@ -364,7 +377,7 @@ bool DartComponentControllerV2::RunDartMain() { tonic::DartMicrotaskQueue::StartForCurrentThread(); - // TODO(fxb/79871): Create a file descriptor for each component that is + // TODO(fxb/88384): Create a file descriptor for each component that is // launched and listen for anything that is written to the component. When // something is written to the component, forward that message along to the // Fuchsia logger and decorate it with the tag that it came from the @@ -391,7 +404,7 @@ bool DartComponentControllerV2::RunDartMain() { Dart_EnterIsolate(isolate_); Dart_EnterScope(); - // TODO(fxb/79871): Support argument passing. + // TODO(fxb/88383): Support argument passing. // Note: Even though we do not support argument passing via the cml files // at this time, we still need to create an argument list and pass it off // to the invocation of main below. If we do not do this dart will look for diff --git a/shell/platform/fuchsia/flutter/BUILD.gn b/shell/platform/fuchsia/flutter/BUILD.gn index eb8cf308ba800..c6bcb66cf1514 100644 --- a/shell/platform/fuchsia/flutter/BUILD.gn +++ b/shell/platform/fuchsia/flutter/BUILD.gn @@ -57,8 +57,10 @@ template("runner_sources") { sources = [ "accessibility_bridge.cc", "accessibility_bridge.h", - "component.cc", - "component.h", + "component_v1.cc", + "component_v1.h", + "component_v2.cc", + "component_v2.h", "engine.cc", "engine.h", "file_in_namespace_buffer.cc", @@ -138,6 +140,7 @@ template("runner_sources") { deps = [ "$fuchsia_sdk_root/fidl:fuchsia.accessibility.semantics", + "$fuchsia_sdk_root/fidl:fuchsia.component.runner", "$fuchsia_sdk_root/fidl:fuchsia.fonts", "$fuchsia_sdk_root/fidl:fuchsia.images", "$fuchsia_sdk_root/fidl:fuchsia.intl", @@ -175,14 +178,6 @@ runner_sources("flutter_runner_sources_product") { product = true } -# Loaded via FFI -_common_runner_libs = common_libs + [ - { - name = "libzircon_ffi.so" - path = rebase_path("$root_out_dir") - }, - ] - # Things that explicitly being excluded: # 1. Kernel snapshot framework mode. # 2. Profiler symbols. @@ -227,9 +222,6 @@ template("flutter_runner") { "$fuchsia_sdk_root/pkg:trace-provider-so", ] + extra_deps - # For `libzircon_ffi` see _common_runner_libs. - public_deps = [ "../dart-pkg/zircon_ffi:zircon_ffi" ] - # The flags below are needed so that Dart's CPU profiler can walk the # C++ stack. cflags = [ "-fno-omit-frame-pointer" ] @@ -367,7 +359,7 @@ template("jit_runner") { ] _vulkan_icds = [] - _libs = _common_runner_libs + _libs = common_libs if (enable_vulkan_validation_layers) { _libs += vulkan_validation_libs _vulkan_icds += vulkan_icds @@ -426,7 +418,7 @@ template("aot_runner") { } _vulkan_icds = [] - _libs = _common_runner_libs + _libs = common_libs if (enable_vulkan_validation_layers) { _libs += vulkan_validation_libs _vulkan_icds += vulkan_icds @@ -458,374 +450,374 @@ test_fixtures("flutter_runner_fixtures") { fixtures = [] } -executable("flutter_runner_unittests") { - testonly = true - - output_name = "flutter_runner_tests" - - sources = [ - "accessibility_bridge_unittest.cc", - "component_unittest.cc", - "flutter_runner_fakes.h", - "focus_delegate_unittests.cc", - "fuchsia_intl_unittest.cc", - "keyboard_unittest.cc", - "platform_view_unittest.cc", - "pointer_delegate_unittests.cc", - "runner_unittest.cc", - "tests/engine_unittests.cc", - "tests/fake_flatland_unittests.cc", - "tests/fake_session_unittests.cc", - "tests/flatland_connection_unittests.cc", - "tests/flutter_runner_product_configuration_unittests.cc", - "tests/gfx_external_view_embedder_unittests.cc", - "tests/gfx_session_connection_unittests.cc", - "tests/pointer_event_utility.cc", - "tests/pointer_event_utility.h", - ] +if (enable_unittests) { + executable("flutter_runner_unittests") { + testonly = true - # This is needed for //third_party/googletest for linking zircon symbols. - libs = [ "$fuchsia_sdk_path/arch/$target_cpu/sysroot/lib/libzircon.so" ] - - # The use of these dependencies is temporary and will be moved behind the - # embedder API. - flutter_deps = [ - "//flutter/assets:assets", - "//flutter/common/graphics", - "//flutter/flow", - "//flutter/lib/ui", - "//flutter/shell/common", - "//third_party/dart/runtime:libdart_jit", - "//third_party/dart/runtime/platform:libdart_platform_jit", - ] + output_name = "flutter_runner_tests" - deps = [ - "tests/fakes", - ":flutter_runner_fixtures", - ":flutter_runner_sources", - "//build/fuchsia/pkg:async-testing", - "//build/fuchsia/pkg:sys_cpp_testing", - "//flutter/testing", - ] + flutter_deps -} + sources = [ + "accessibility_bridge_unittest.cc", + "component_v1_unittest.cc", + "flutter_runner_fakes.h", + "focus_delegate_unittests.cc", + "fuchsia_intl_unittest.cc", + "keyboard_unittest.cc", + "platform_view_unittest.cc", + "pointer_delegate_unittests.cc", + "runner_unittest.cc", + "tests/engine_unittests.cc", + "tests/fake_flatland_unittests.cc", + "tests/fake_session_unittests.cc", + "tests/flatland_connection_unittests.cc", + "tests/flatland_external_view_embedder_unittests.cc", + "tests/flutter_runner_product_configuration_unittests.cc", + "tests/gfx_external_view_embedder_unittests.cc", + "tests/gfx_session_connection_unittests.cc", + "tests/pointer_event_utility.cc", + "tests/pointer_event_utility.h", + "vsync_waiter_unittest.cc", + ] -executable("flutter_runner_tzdata_unittests") { - testonly = true + # This is needed for //third_party/googletest for linking zircon symbols. + libs = [ "$fuchsia_sdk_path/arch/$target_cpu/sysroot/lib/libzircon.so" ] - output_name = "flutter_runner_tzdata_tests" + # The use of these dependencies is temporary and will be moved behind the + # embedder API. + flutter_deps = [ + "//flutter/assets:assets", + "//flutter/common/graphics", + "//flutter/flow", + "//flutter/lib/ui", + "//flutter/shell/common", + "//third_party/dart/runtime:libdart_jit", + "//third_party/dart/runtime/platform:libdart_platform_jit", + ] - sources = [ "runner_tzdata_unittest.cc" ] + deps = [ + "tests/fakes", + ":flutter_runner_fixtures", + ":flutter_runner_sources", + "//build/fuchsia/pkg:async-testing", + "//build/fuchsia/pkg:sys_cpp_testing", + "//flutter/testing", + ] + flutter_deps + } - # This is needed for //third_party/googletest for linking zircon symbols. - libs = [ "$fuchsia_sdk_path/arch/$target_cpu/sysroot/lib/libzircon.so" ] + executable("flutter_runner_tzdata_unittests") { + testonly = true - # The use of these dependencies is temporary and will be moved behind the - # embedder API. - flutter_deps = [ - "//flutter/lib/ui", - "//third_party/dart/runtime:libdart_jit", - "//third_party/dart/runtime/platform:libdart_platform_jit", - ] + output_name = "flutter_runner_tzdata_tests" - deps = [ - ":flutter_runner_fixtures", - ":flutter_runner_sources", - "//flutter/testing", - ] + flutter_deps -} + sources = [ "runner_tzdata_unittest.cc" ] -fuchsia_test_archive("flutter_runner_tests") { - deps = [ ":flutter_runner_unittests" ] - - binary = "$target_name" - - resources = [ - { - path = rebase_path("//third_party/icu/common/icudtl.dat") - dest = "icudtl.dat" - }, - { - path = rebase_path( - "//flutter/shell/platform/fuchsia/flutter/tests/tzdata/2019a/44/le/metaZones.res") - dest = "tzdata/metaZones.res" - }, - { - path = rebase_path( - "//flutter/shell/platform/fuchsia/flutter/tests/tzdata/2019a/44/le/timezoneTypes.res") - dest = "tzdata/timezoneTypes.res" - }, - { - path = rebase_path( - "//flutter/shell/platform/fuchsia/flutter/tests/tzdata/2019a/44/le/zoneinfo64.res") - dest = "tzdata/zoneinfo64.res" - }, - ] + # This is needed for //third_party/googletest for linking zircon symbols. + libs = [ "$fuchsia_sdk_path/arch/$target_cpu/sysroot/lib/libzircon.so" ] - cmx_file = rebase_path("meta/$target_name.cmx") -} + # The use of these dependencies is temporary and will be moved behind the + # embedder API. + flutter_deps = [ + "//flutter/lib/ui", + "//third_party/dart/runtime:libdart_jit", + "//third_party/dart/runtime/platform:libdart_platform_jit", + ] -fuchsia_test_archive("flutter_runner_tzdata_tests") { - deps = [ ":flutter_runner_tzdata_unittests" ] + deps = [ + ":flutter_runner_fixtures", + ":flutter_runner_sources", + "//flutter/testing", + ] + flutter_deps + } - binary = "$target_name" + fuchsia_test_archive("flutter_runner_tests") { + deps = [ ":flutter_runner_unittests" ] - resources = [ - { - path = rebase_path("//third_party/icu/common/icudtl.dat") - dest = "icudtl.dat" - }, - ] -} + binary = "$target_name" -fuchsia_test_archive("fml_tests") { - deps = [ "//flutter/fml:fml_unittests" ] + resources = [ + { + path = rebase_path("//third_party/icu/common/icudtl.dat") + dest = "icudtl.dat" + }, + { + path = rebase_path( + "//flutter/shell/platform/fuchsia/flutter/tests/tzdata/2019a/44/le/metaZones.res") + dest = "tzdata/metaZones.res" + }, + { + path = rebase_path( + "//flutter/shell/platform/fuchsia/flutter/tests/tzdata/2019a/44/le/timezoneTypes.res") + dest = "tzdata/timezoneTypes.res" + }, + { + path = rebase_path( + "//flutter/shell/platform/fuchsia/flutter/tests/tzdata/2019a/44/le/zoneinfo64.res") + dest = "tzdata/zoneinfo64.res" + }, + ] - binary = "fml_unittests" -} + cmx_file = rebase_path("meta/$target_name.cmx") + } -fuchsia_test_archive("flow_tests") { - deps = [ "//flutter/flow:flow_unittests" ] - - binary = "flow_unittests" - - resources = [ - { - path = rebase_path( - "//flutter/testing/resources/performance_overlay_gold_60fps.png") - dest = "flutter/testing/resources/performance_overlay_gold_60fps.png" - }, - { - path = rebase_path( - "//flutter/testing/resources/performance_overlay_gold_90fps.png") - dest = "flutter/testing/resources/performance_overlay_gold_90fps.png" - }, - { - path = rebase_path( - "//flutter/testing/resources/performance_overlay_gold_120fps.png") - dest = "flutter/testing/resources/performance_overlay_gold_120fps.png" - }, - ] -} + fuchsia_test_archive("flutter_runner_tzdata_tests") { + deps = [ ":flutter_runner_tzdata_unittests" ] -fuchsia_test_archive("runtime_tests") { - deps = [ - "//flutter/runtime:runtime_fixtures", - "//flutter/runtime:runtime_unittests", - ] + binary = "$target_name" - binary = "runtime_unittests" + resources = [ + { + path = rebase_path("//third_party/icu/common/icudtl.dat") + dest = "icudtl.dat" + }, + ] + } - # TODO(gw280): https://github.com/flutter/flutter/issues/50294 - # Right now we need to manually specify all the fixtures that are - # declared in the test_fixtures() call above. - resources = [ - { - path = "$root_gen_dir/flutter/runtime/assets/kernel_blob.bin" - dest = "assets/kernel_blob.bin" - }, - ] -} + fuchsia_test_archive("fml_tests") { + deps = [ "//flutter/fml:fml_unittests" ] -fuchsia_test_archive("shell_tests") { - deps = [ - "//flutter/shell/common:shell_unittests", - "//flutter/shell/common:shell_unittests_fixtures", - ] + binary = "fml_unittests" + } - binary = "shell_unittests" - - # TODO(gw280): https://github.com/flutter/flutter/issues/50294 - # Right now we need to manually specify all the fixtures that are - # declared in the test_fixtures() call above. - resources = [ - { - path = "$root_gen_dir/flutter/shell/common/assets/kernel_blob.bin" - dest = "assets/kernel_blob.bin" - }, - { - path = - "$root_gen_dir/flutter/shell/common/assets/shelltest_screenshot.png" - dest = "assets/shelltest_screenshot.png" - }, - ] + fuchsia_test_archive("flow_tests") { + deps = [ "//flutter/flow:flow_unittests" ] - libraries = vulkan_validation_libs - resources += vulkan_icds -} + binary = "flow_unittests" -fuchsia_test_archive("testing_tests") { - deps = [ "//flutter/testing:testing_unittests" ] + resources = [ + { + path = rebase_path( + "//flutter/testing/resources/performance_overlay_gold_60fps.png") + dest = "flutter/testing/resources/performance_overlay_gold_60fps.png" + }, + { + path = rebase_path( + "//flutter/testing/resources/performance_overlay_gold_90fps.png") + dest = "flutter/testing/resources/performance_overlay_gold_90fps.png" + }, + { + path = rebase_path( + "//flutter/testing/resources/performance_overlay_gold_120fps.png") + dest = "flutter/testing/resources/performance_overlay_gold_120fps.png" + }, + ] + } - binary = "testing_unittests" -} + fuchsia_test_archive("runtime_tests") { + deps = [ + "//flutter/runtime:runtime_fixtures", + "//flutter/runtime:runtime_unittests", + ] -fuchsia_test_archive("txt_tests") { - deps = [ "//flutter/third_party/txt:txt_unittests" ] + binary = "runtime_unittests" - binary = "txt_unittests" + # TODO(gw280): https://github.com/flutter/flutter/issues/50294 + # Right now we need to manually specify all the fixtures that are + # declared in the test_fixtures() call above. + resources = [ + { + path = "$root_gen_dir/flutter/runtime/assets/kernel_blob.bin" + dest = "assets/kernel_blob.bin" + }, + ] + } - resources = [ - { - path = rebase_path("//third_party/icu/common/icudtl.dat") - dest = "icudtl.dat" - }, - { - path = rebase_path("//third_party/icu/common/icudtl.dat") - dest = "icudtl2.dat" - }, - ] -} + fuchsia_test_archive("shell_tests") { + deps = [ + "//flutter/shell/common:shell_unittests", + "//flutter/shell/common:shell_unittests_fixtures", + ] -fuchsia_test_archive("ui_tests") { - deps = [ - "//flutter/lib/ui:ui_unittests", - "//flutter/lib/ui:ui_unittests_fixtures", - ] + binary = "shell_unittests" - binary = "ui_unittests" - - # TODO(gw280): https://github.com/flutter/flutter/issues/50294 - # Right now we need to manually specify all the fixtures that are - # declared in the test_fixtures() call above. - resources = [ - { - path = "$root_gen_dir/flutter/lib/ui/assets/kernel_blob.bin" - dest = "assets/kernel_blob.bin" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/DashInNooglerHat.jpg" - dest = "assets/DashInNooglerHat.jpg" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/Horizontal.jpg" - dest = "assets/Horizontal.jpg" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/Horizontal.png" - dest = "assets/Horizontal.png" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/hello_loop_2.gif" - dest = "assets/hello_loop_2.gif" - }, - { - path = "$root_gen_dir/flutter/lib/ui/assets/hello_loop_2.webp" - dest = "assets/hello_loop_2.webp" - }, - ] + # TODO(gw280): https://github.com/flutter/flutter/issues/50294 + # Right now we need to manually specify all the fixtures that are + # declared in the test_fixtures() call above. + resources = [ + { + path = "$root_gen_dir/flutter/shell/common/assets/kernel_blob.bin" + dest = "assets/kernel_blob.bin" + }, + { + path = + "$root_gen_dir/flutter/shell/common/assets/shelltest_screenshot.png" + dest = "assets/shelltest_screenshot.png" + }, + ] - libraries = vulkan_validation_libs - resources += vulkan_icds -} + libraries = vulkan_validation_libs + resources += vulkan_icds + } -fuchsia_test_archive("embedder_tests") { - deps = [ - "//flutter/shell/platform/embedder:embedder_unittests", - "//flutter/shell/platform/embedder:fixtures", - ] + fuchsia_test_archive("testing_tests") { + deps = [ "//flutter/testing:testing_unittests" ] - binary = "embedder_unittests" - - # TODO(gw280): https://github.com/flutter/flutter/issues/50294 - # Right now we need to manually specify all the fixtures that are - # declared in the test_fixtures() call above. - resources = [ - { - path = - "$root_gen_dir/flutter/shell/platform/embedder/assets/kernel_blob.bin" - dest = "assets/kernel_blob.bin" - }, - { - path = "$root_gen_dir/flutter/shell/platform/embedder/assets/arc_end_caps.png" - dest = "assets/arc_end_caps.png" - }, - { - path = - "$root_gen_dir/flutter/shell/platform/embedder/assets/compositor.png" - dest = "assets/compositor.png" - }, - { - path = "$root_gen_dir/flutter/shell/platform/embedder/assets/compositor_root_surface_xformation.png" - dest = "assets/compositor_root_surface_xformation.png" - }, - { - path = "$root_gen_dir/flutter/shell/platform/embedder/assets/compositor_software.png" - dest = "assets/compositor_software.png" - }, - { - path = "$root_gen_dir/flutter/shell/platform/embedder/assets/compositor_with_platform_layer_on_bottom.png" - dest = "assets/compositor_with_platform_layer_on_bottom.png" - }, - { - path = "$root_gen_dir/flutter/shell/platform/embedder/assets/compositor_with_root_layer_only.png" - dest = "assets/compositor_with_root_layer_only.png" - }, - { - path = - "$root_gen_dir/flutter/shell/platform/embedder/assets/dpr_noxform.png" - dest = "assets/dpr_noxform.png" - }, - { - path = - "$root_gen_dir/flutter/shell/platform/embedder/assets/dpr_xform.png" - dest = "assets/dpr_xform.png" - }, - { - path = "$root_gen_dir/flutter/shell/platform/embedder/assets/gradient.png" - dest = "assets/gradient.png" - }, - { - path = "$root_gen_dir/flutter/shell/platform/embedder/assets/gradient_xform.png" - dest = "assets/gradient_xform.png" - }, - { - path = "$root_gen_dir/flutter/shell/platform/embedder/assets/scene_without_custom_compositor.png" - dest = "assets/scene_without_custom_compositor.png" - }, - { - path = "$root_gen_dir/flutter/shell/platform/embedder/assets/scene_without_custom_compositor_with_xform.png" - dest = "assets/scene_without_custom_compositor_with_xform.png" - }, - { - path = "$root_gen_dir/flutter/shell/platform/embedder/assets/snapshot_large_scene.png" - dest = "assets/snapshot_large_scene.png" - }, - { - path = "$root_gen_dir/flutter/shell/platform/embedder/assets/verifyb143464703.png" - dest = "assets/verifyb143464703.png" - }, - { - path = "$root_gen_dir/flutter/shell/platform/embedder/assets/verifyb143464703_soft_noxform.png" - dest = "assets/verifyb143464703_soft_noxform.png" - }, - ] -} + binary = "testing_unittests" + } -fuchsia_test_archive("dart_utils_tests") { - deps = [ - "//flutter/shell/platform/fuchsia/runtime/dart/utils:dart_utils_unittests", - ] + fuchsia_test_archive("txt_tests") { + deps = [ "//flutter/third_party/txt:txt_unittests" ] - binary = "dart_utils_unittests" -} + binary = "txt_unittests" -# When adding a new dep here, please also ensure the dep is added to -# testing/fuchsia/run_tests.sh and testing/fuchsia/test_fars -group("tests") { - testonly = true - - deps = [ - ":dart_utils_tests", - ":embedder_tests", - ":flow_tests", - ":flutter_runner_tests", - ":flutter_runner_tzdata_tests", - ":fml_tests", - ":runtime_tests", - ":shell_tests", - ":testing_tests", - ":txt_tests", - ":ui_tests", - "integration_flutter_tests", - ] + resources = [ + { + path = rebase_path("//third_party/icu/common/icudtl.dat") + dest = "icudtl.dat" + }, + { + path = rebase_path("//third_party/icu/common/icudtl.dat") + dest = "icudtl2.dat" + }, + ] + } + + fuchsia_test_archive("ui_tests") { + deps = [ + "//flutter/lib/ui:ui_unittests", + "//flutter/lib/ui:ui_unittests_fixtures", + ] + + binary = "ui_unittests" + + # TODO(gw280): https://github.com/flutter/flutter/issues/50294 + # Right now we need to manually specify all the fixtures that are + # declared in the test_fixtures() call above. + resources = [ + { + path = "$root_gen_dir/flutter/lib/ui/assets/kernel_blob.bin" + dest = "assets/kernel_blob.bin" + }, + { + path = "$root_gen_dir/flutter/lib/ui/assets/DashInNooglerHat.jpg" + dest = "assets/DashInNooglerHat.jpg" + }, + { + path = "$root_gen_dir/flutter/lib/ui/assets/Horizontal.jpg" + dest = "assets/Horizontal.jpg" + }, + { + path = "$root_gen_dir/flutter/lib/ui/assets/Horizontal.png" + dest = "assets/Horizontal.png" + }, + { + path = "$root_gen_dir/flutter/lib/ui/assets/hello_loop_2.gif" + dest = "assets/hello_loop_2.gif" + }, + { + path = "$root_gen_dir/flutter/lib/ui/assets/hello_loop_2.webp" + dest = "assets/hello_loop_2.webp" + }, + ] + + libraries = vulkan_validation_libs + resources += vulkan_icds + } + + fuchsia_test_archive("embedder_tests") { + deps = [ + "//flutter/shell/platform/embedder:embedder_unittests", + "//flutter/shell/platform/embedder:fixtures", + ] + + binary = "embedder_unittests" + + # TODO(gw280): https://github.com/flutter/flutter/issues/50294 + # Right now we need to manually specify all the fixtures that are + # declared in the test_fixtures() call above. + resources = [ + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/kernel_blob.bin" + dest = "assets/kernel_blob.bin" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/arc_end_caps.png" + dest = "assets/arc_end_caps.png" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/compositor.png" + dest = "assets/compositor.png" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/compositor_root_surface_xformation.png" + dest = "assets/compositor_root_surface_xformation.png" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/compositor_software.png" + dest = "assets/compositor_software.png" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/compositor_with_platform_layer_on_bottom.png" + dest = "assets/compositor_with_platform_layer_on_bottom.png" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/compositor_with_root_layer_only.png" + dest = "assets/compositor_with_root_layer_only.png" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/dpr_noxform.png" + dest = "assets/dpr_noxform.png" + }, + { + path = + "$root_gen_dir/flutter/shell/platform/embedder/assets/dpr_xform.png" + dest = "assets/dpr_xform.png" + }, + { + path = + "$root_gen_dir/flutter/shell/platform/embedder/assets/gradient.png" + dest = "assets/gradient.png" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/gradient_xform.png" + dest = "assets/gradient_xform.png" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/scene_without_custom_compositor.png" + dest = "assets/scene_without_custom_compositor.png" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/scene_without_custom_compositor_with_xform.png" + dest = "assets/scene_without_custom_compositor_with_xform.png" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/snapshot_large_scene.png" + dest = "assets/snapshot_large_scene.png" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/verifyb143464703.png" + dest = "assets/verifyb143464703.png" + }, + { + path = "$root_gen_dir/flutter/shell/platform/embedder/assets/verifyb143464703_soft_noxform.png" + dest = "assets/verifyb143464703_soft_noxform.png" + }, + ] + } + + fuchsia_test_archive("dart_utils_tests") { + deps = [ "//flutter/shell/platform/fuchsia/runtime/dart/utils:dart_utils_unittests" ] + + binary = "dart_utils_unittests" + } + + # When adding a new dep here, please also ensure the dep is added to + # testing/fuchsia/run_tests.sh and testing/fuchsia/test_fars + group("tests") { + testonly = true + + deps = [ + ":dart_utils_tests", + ":embedder_tests", + ":flow_tests", + ":flutter_runner_tests", + ":flutter_runner_tzdata_tests", + ":fml_tests", + ":runtime_tests", + ":shell_tests", + ":testing_tests", + ":txt_tests", + ":ui_tests", + "integration_flutter_tests", + ] + } } diff --git a/shell/platform/fuchsia/flutter/accessibility_bridge.cc b/shell/platform/fuchsia/flutter/accessibility_bridge.cc index 8d419ff3d637a..79c5a7bb98007 100644 --- a/shell/platform/fuchsia/flutter/accessibility_bridge.cc +++ b/shell/platform/fuchsia/flutter/accessibility_bridge.cc @@ -218,8 +218,7 @@ std::string NodeChildrenToString(const flutter::SemanticsNode& node) { AccessibilityBridge::AccessibilityBridge( SetSemanticsEnabledCallback set_semantics_enabled_callback, DispatchSemanticsActionCallback dispatch_semantics_action_callback, - fidl::InterfaceHandle - semantics_manager, + fuchsia::accessibility::semantics::SemanticsManagerHandle semantics_manager, fuchsia::ui::views::ViewRef view_ref, inspect::Node inspect_node) : set_semantics_enabled_callback_( diff --git a/shell/platform/fuchsia/flutter/accessibility_bridge.h b/shell/platform/fuchsia/flutter/accessibility_bridge.h index 474ca5b37af54..79068ef10aa41 100644 --- a/shell/platform/fuchsia/flutter/accessibility_bridge.h +++ b/shell/platform/fuchsia/flutter/accessibility_bridge.h @@ -66,7 +66,7 @@ class AccessibilityBridge AccessibilityBridge( SetSemanticsEnabledCallback set_semantics_enabled_callback, DispatchSemanticsActionCallback dispatch_semantics_action_callback, - fidl::InterfaceHandle + fuchsia::accessibility::semantics::SemanticsManagerHandle semantics_manager, fuchsia::ui::views::ViewRef view_ref, inspect::Node inspect_node); diff --git a/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc b/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc index 94a61bf312ba7..0bd6c0acd685a 100644 --- a/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc +++ b/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc @@ -89,8 +89,7 @@ class AccessibilityBridgeTest : public testing::Test { protected: void SetUp() override { // Connect to SemanticsManager service. - fidl::InterfaceHandle - semantics_manager; + fuchsia::accessibility::semantics::SemanticsManagerHandle semantics_manager; zx_status_t semantics_status = services_provider_.service_directory() ->Connect( @@ -807,8 +806,9 @@ TEST_F(AccessibilityBridgeTest, BatchesLargeMessages) { RunLoopUntilIdle(); EXPECT_EQ(0, semantics_manager_.DeleteCount()); + EXPECT_TRUE(6 <= semantics_manager_.UpdateCount() && - semantics_manager_.UpdateCount() <= 10); + semantics_manager_.UpdateCount() <= 12); EXPECT_EQ(1, semantics_manager_.CommitCount()); EXPECT_FALSE(semantics_manager_.DeleteOverflowed()); EXPECT_FALSE(semantics_manager_.UpdateOverflowed()); diff --git a/shell/platform/fuchsia/flutter/component_v1.cc b/shell/platform/fuchsia/flutter/component_v1.cc new file mode 100644 index 0000000000000..2c6b95f4c060a --- /dev/null +++ b/shell/platform/fuchsia/flutter/component_v1.cc @@ -0,0 +1,586 @@ +// 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 "component_v1.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "file_in_namespace_buffer.h" +#include "flutter/fml/mapping.h" +#include "flutter/fml/platform/fuchsia/task_observers.h" +#include "flutter/fml/synchronization/waitable_event.h" +#include "flutter/fml/unique_fd.h" +#include "flutter/runtime/dart_vm_lifecycle.h" +#include "flutter/shell/common/switches.h" +#include "runtime/dart/utils/files.h" +#include "runtime/dart/utils/handle_exception.h" +#include "runtime/dart/utils/mapped_resource.h" +#include "runtime/dart/utils/tempfs.h" +#include "runtime/dart/utils/vmo.h" + +namespace flutter_runner { +namespace { + +constexpr char kDataKey[] = "data"; +constexpr char kAssetsKey[] = "assets"; +constexpr char kTmpPath[] = "/tmp"; +constexpr char kServiceRootPath[] = "/svc"; +constexpr char kRunnerConfigPath[] = "/config/data/flutter_runner_config"; + +std::string DebugLabelForUrl(const std::string& url) { + auto found = url.rfind("/"); + if (found == std::string::npos) { + return url; + } else { + return {url, found + 1}; + } +} + +} // namespace + +void ComponentV1::ParseProgramMetadata( + const fidl::VectorPtr& program_metadata, + std::string* data_path, + std::string* assets_path) { + if (!program_metadata.has_value()) { + return; + } + for (const auto& pg : *program_metadata) { + if (pg.key.compare(kDataKey) == 0) { + *data_path = "pkg/" + pg.value; + } else if (pg.key.compare(kAssetsKey) == 0) { + *assets_path = "pkg/" + pg.value; + } + } + + // assets_path defaults to the same as data_path if omitted. + if (assets_path->empty()) { + *assets_path = *data_path; + } +} + +ActiveComponentV1 ComponentV1::Create( + TerminationCallback termination_callback, + fuchsia::sys::Package package, + fuchsia::sys::StartupInfo startup_info, + std::shared_ptr runner_incoming_services, + fidl::InterfaceRequest controller) { + auto thread = std::make_unique(); + std::unique_ptr component; + + fml::AutoResetWaitableEvent latch; + thread->GetTaskRunner()->PostTask([&]() mutable { + component.reset(new ComponentV1(std::move(termination_callback), + std::move(package), std::move(startup_info), + runner_incoming_services, + std::move(controller))); + latch.Signal(); + }); + + latch.Wait(); + return {.platform_thread = std::move(thread), + .component = std::move(component)}; +} + +ComponentV1::ComponentV1( + TerminationCallback termination_callback, + fuchsia::sys::Package package, + fuchsia::sys::StartupInfo startup_info, + std::shared_ptr runner_incoming_services, + fidl::InterfaceRequest + component_controller_request) + : termination_callback_(std::move(termination_callback)), + debug_label_(DebugLabelForUrl(startup_info.launch_info.url)), + component_controller_(this), + outgoing_dir_(new vfs::PseudoDir()), + runner_incoming_services_(runner_incoming_services), + weak_factory_(this) { + component_controller_.set_error_handler( + [this](zx_status_t status) { Kill(); }); + + FML_DCHECK(fdio_ns_.is_valid()); + // LaunchInfo::url non-optional. + auto& launch_info = startup_info.launch_info; + + // LaunchInfo::arguments optional. + if (auto& arguments = launch_info.arguments) { + settings_.dart_entrypoint_args = arguments.value(); + } + + // Determine where data and assets are stored within /pkg. + std::string data_path; + std::string assets_path; + ParseProgramMetadata(startup_info.program_metadata, &data_path, &assets_path); + + if (data_path.empty()) { + FML_DLOG(ERROR) << "Could not find a /pkg/data directory for " + << package.resolved_url; + return; + } + + // Setup /tmp to be mapped to the process-local memfs. + dart_utils::RunnerTemp::SetupComponent(fdio_ns_.get()); + + // LaunchInfo::flat_namespace optional. + for (size_t i = 0; i < startup_info.flat_namespace.paths.size(); ++i) { + const auto& path = startup_info.flat_namespace.paths.at(i); + if (path == kTmpPath) { + continue; + } + + zx::channel dir; + if (path == kServiceRootPath) { + svc_ = std::make_unique( + std::move(startup_info.flat_namespace.directories.at(i))); + dir = svc_->CloneChannel().TakeChannel(); + } else { + dir = std::move(startup_info.flat_namespace.directories.at(i)); + } + + zx_handle_t dir_handle = dir.release(); + if (fdio_ns_bind(fdio_ns_.get(), path.data(), dir_handle) != ZX_OK) { + FML_DLOG(ERROR) << "Could not bind path to namespace: " << path; + zx_handle_close(dir_handle); + } + } + + { + fml::UniqueFD ns_fd(fdio_ns_opendir(fdio_ns_.get())); + FML_DCHECK(ns_fd.is_valid()); + + constexpr mode_t mode = O_RDONLY | O_DIRECTORY; + + component_assets_directory_.reset( + openat(ns_fd.get(), assets_path.c_str(), mode)); + FML_DCHECK(component_assets_directory_.is_valid()); + + component_data_directory_.reset( + openat(ns_fd.get(), data_path.c_str(), mode)); + FML_DCHECK(component_data_directory_.is_valid()); + } + + // TODO: LaunchInfo::out. + + // TODO: LaunchInfo::err. + + // LaunchInfo::service_request optional. + if (launch_info.directory_request) { + outgoing_dir_->Serve(fuchsia::io::OPEN_RIGHT_READABLE | + fuchsia::io::OPEN_RIGHT_WRITABLE | + fuchsia::io::OPEN_FLAG_DIRECTORY, + std::move(launch_info.directory_request)); + } + + directory_request_ = directory_ptr_.NewRequest(); + + fuchsia::io::DirectoryHandle flutter_public_dir; + // TODO(anmittal): when fixing enumeration using new c++ vfs, make sure that + // flutter_public_dir is only accessed once we receive OnOpen Event. + // That will prevent FL-175 for public directory + auto request = flutter_public_dir.NewRequest().TakeChannel(); + fdio_service_connect_at(directory_ptr_.channel().get(), "svc", + request.release()); + + auto composed_service_dir = std::make_unique(); + composed_service_dir->set_fallback(std::move(flutter_public_dir)); + + // Clone and check if client is servicing the directory. + directory_ptr_->Clone(fuchsia::io::OPEN_FLAG_DESCRIBE | + fuchsia::io::OPEN_RIGHT_READABLE | + fuchsia::io::OPEN_RIGHT_WRITABLE, + cloned_directory_ptr_.NewRequest()); + + cloned_directory_ptr_.events().OnOpen = + [this](zx_status_t status, std::unique_ptr info) { + cloned_directory_ptr_.Unbind(); + if (status != ZX_OK) { + FML_LOG(ERROR) + << "could not bind out directory for flutter component(" + << debug_label_ << "): " << zx_status_get_string(status); + return; + } + const char* other_dirs[] = {"debug", "ctrl", "diagnostics"}; + // add other directories as RemoteDirs. + for (auto& dir_str : other_dirs) { + fuchsia::io::DirectoryHandle dir; + auto request = dir.NewRequest().TakeChannel(); + auto status = fdio_service_connect_at(directory_ptr_.channel().get(), + dir_str, request.release()); + if (status == ZX_OK) { + outgoing_dir_->AddEntry( + dir_str, std::make_unique(dir.TakeChannel())); + } else { + FML_LOG(ERROR) << "could not add out directory entry(" << dir_str + << ") for flutter component(" << debug_label_ + << "): " << zx_status_get_string(status); + } + } + }; + + cloned_directory_ptr_.set_error_handler( + [this](zx_status_t status) { cloned_directory_ptr_.Unbind(); }); + + // TODO: LaunchInfo::additional_services optional. + + // All launch arguments have been read. Perform service binding and + // final settings configuration. The next call will be to create a view + // for this component. + composed_service_dir->AddService( + fuchsia::ui::app::ViewProvider::Name_, + std::make_unique( + [this](zx::channel channel, async_dispatcher_t* dispatcher) { + shells_bindings_.AddBinding( + this, fidl::InterfaceRequest( + std::move(channel))); + })); + + outgoing_dir_->AddEntry("svc", std::move(composed_service_dir)); + + // Setup the component controller binding. + if (component_controller_request) { + component_controller_.Bind(std::move(component_controller_request)); + } + + // Load and use runner-specific configuration, if it exists. + std::string json_string; + if (dart_utils::ReadFileToString(kRunnerConfigPath, &json_string)) { + product_config_ = FlutterRunnerProductConfiguration(json_string); + FML_LOG(INFO) << "Successfully loaded runner configuration: " + << json_string; + } else { + FML_LOG(WARNING) << "Failed to load runner configuration from " + << kRunnerConfigPath << "; using default config values."; + } + + // Load VM and component bytecode. + // For AOT, compare with flutter_aot_app in flutter_app.gni. + // For JIT, compare flutter_jit_runner in BUILD.gn. + if (flutter::DartVM::IsRunningPrecompiledCode()) { + std::shared_ptr snapshot = + std::make_shared(); + if (snapshot->Load(component_data_directory_.get(), + "app_aot_snapshot.so")) { + const uint8_t* isolate_data = snapshot->IsolateData(); + const uint8_t* isolate_instructions = snapshot->IsolateInstrs(); + const uint8_t* vm_data = snapshot->VmData(); + const uint8_t* vm_instructions = snapshot->VmInstrs(); + if (isolate_data == nullptr || isolate_instructions == nullptr || + vm_data == nullptr || vm_instructions == nullptr) { + FML_LOG(FATAL) << "ELF snapshot missing AOT symbols."; + return; + } + auto hold_snapshot = [snapshot](const uint8_t* _, size_t __) {}; + settings_.vm_snapshot_data = [hold_snapshot, vm_data]() { + return std::make_unique(vm_data, 0, hold_snapshot, + true /* dontneed_safe */); + }; + settings_.vm_snapshot_instr = [hold_snapshot, vm_instructions]() { + return std::make_unique( + vm_instructions, 0, hold_snapshot, true /* dontneed_safe */); + }; + settings_.isolate_snapshot_data = [hold_snapshot, isolate_data]() { + return std::make_unique( + isolate_data, 0, hold_snapshot, true /* dontneed_safe */); + }; + settings_.isolate_snapshot_instr = [hold_snapshot, + isolate_instructions]() { + return std::make_unique( + isolate_instructions, 0, hold_snapshot, true /* dontneed_safe */); + }; + isolate_snapshot_ = fml::MakeRefCounted( + std::make_shared(isolate_data, 0, hold_snapshot, + true /* dontneed_safe */), + std::make_shared(isolate_instructions, 0, + hold_snapshot, + true /* dontneed_safe */)); + } else { + const int namespace_fd = component_data_directory_.get(); + settings_.vm_snapshot_data = [namespace_fd]() { + return LoadFile(namespace_fd, "vm_snapshot_data.bin", + false /* executable */); + }; + settings_.vm_snapshot_instr = [namespace_fd]() { + return LoadFile(namespace_fd, "vm_snapshot_instructions.bin", + true /* executable */); + }; + settings_.isolate_snapshot_data = [namespace_fd]() { + return LoadFile(namespace_fd, "isolate_snapshot_data.bin", + false /* executable */); + }; + settings_.isolate_snapshot_instr = [namespace_fd]() { + return LoadFile(namespace_fd, "isolate_snapshot_instructions.bin", + true /* executable */); + }; + } + } else { + settings_.vm_snapshot_data = []() { + return MakeFileMapping("/pkg/data/vm_snapshot_data.bin", + false /* executable */); + }; + settings_.vm_snapshot_instr = []() { + return MakeFileMapping("/pkg/data/vm_snapshot_instructions.bin", + true /* executable */); + }; + + settings_.isolate_snapshot_data = []() { + return MakeFileMapping("/pkg/data/isolate_core_snapshot_data.bin", + false /* executable */); + }; + settings_.isolate_snapshot_instr = [] { + return MakeFileMapping("/pkg/data/isolate_core_snapshot_instructions.bin", + true /* executable */); + }; + } + +#if defined(DART_PRODUCT) + settings_.enable_observatory = false; +#else + settings_.enable_observatory = true; + + // TODO(cbracken): pass this in as a param to allow 0.0.0.0, ::1, etc. + settings_.observatory_host = "127.0.0.1"; +#endif + + // Controls whether category "skia" trace events are enabled. + settings_.trace_skia = true; + + settings_.verbose_logging = true; + + settings_.advisory_script_uri = debug_label_; + + settings_.advisory_script_entrypoint = debug_label_; + + settings_.icu_data_path = ""; + + settings_.assets_dir = component_assets_directory_.get(); + + // Compare flutter_jit_app in flutter_app.gni. + settings_.application_kernel_list_asset = "app.dilplist"; + + settings_.log_tag = debug_label_ + std::string{"(flutter)"}; + + // No asserts in debug or release product. + // No asserts in release with flutter_profile=true (non-product) + // Yes asserts in non-product debug. +#if !defined(DART_PRODUCT) && (!defined(FLUTTER_PROFILE) || !defined(NDEBUG)) + // Debug mode + settings_.disable_dart_asserts = false; +#else + // Release mode + settings_.disable_dart_asserts = true; +#endif + + // Do not leak the VM; allow it to shut down normally when the last shell + // terminates. + settings_.leak_vm = false; + + settings_.task_observer_add = + std::bind(&fml::CurrentMessageLoopAddAfterTaskObserver, + std::placeholders::_1, std::placeholders::_2); + + settings_.task_observer_remove = std::bind( + &fml::CurrentMessageLoopRemoveAfterTaskObserver, std::placeholders::_1); + + settings_.log_message_callback = [](const std::string& tag, + const std::string& message) { + if (tag.size() > 0) { + std::cout << tag << ": "; + } + std::cout << message << std::endl; + }; + + settings_.dart_flags = {"--lazy_async_stacks"}; + + // Don't collect CPU samples from Dart VM C++ code. + settings_.dart_flags.push_back("--no_profile_vm"); + + // Scale back CPU profiler sampling period on ARM64 to avoid overloading + // the tracing engine. +#if defined(__aarch64__) + settings_.dart_flags.push_back("--profile_period=10000"); +#endif // defined(__aarch64__) + + auto platform_task_runner = fml::MessageLoop::GetCurrent().GetTaskRunner(); + const std::string component_url = package.resolved_url; + settings_.unhandled_exception_callback = [weak = weak_factory_.GetWeakPtr(), + platform_task_runner, + runner_incoming_services, + component_url]( + const std::string& error, + const std::string& stack_trace) { + if (weak) { + // TODO(cbracken): unsafe. The above check and the PostTask below are + // happening on the UI thread. If the Component dtor and thread + // termination happen (on the platform thread) between the previous + // line and the next line, a crash will occur since we'll be posting + // to a dead thread. See Runner::OnComponentV1Terminate() in + // runner.cc. + platform_task_runner->PostTask([weak, runner_incoming_services, + component_url, error, stack_trace]() { + if (weak) { + dart_utils::HandleException(runner_incoming_services, component_url, + error, stack_trace); + } else { + FML_LOG(WARNING) + << "Exception was thrown which was not caught in Flutter app: " + << error; + } + }); + } else { + FML_LOG(WARNING) + << "Exception was thrown which was not caught in Flutter app: " + << error; + } + // Ideally we would return whether HandleException returned ZX_OK, but + // short of knowing if the exception was correctly handled, we return + // false to have the error and stack trace printed in the logs. + return false; + }; +} + +ComponentV1::~ComponentV1() = default; + +const std::string& ComponentV1::GetDebugLabel() const { + return debug_label_; +} + +void ComponentV1::Kill() { + component_controller_.events().OnTerminated( + last_return_code_.second, fuchsia::sys::TerminationReason::EXITED); + + termination_callback_(this); + // WARNING: Don't do anything past this point as this instance may have been + // collected. +} + +void ComponentV1::Detach() { + component_controller_.set_error_handler(nullptr); +} + +void ComponentV1::OnEngineTerminate(const Engine* shell_holder) { + auto found = std::find_if(shell_holders_.begin(), shell_holders_.end(), + [shell_holder](const auto& holder) { + return holder.get() == shell_holder; + }); + + if (found == shell_holders_.end()) { + // This indicates a deeper issue with memory management and should never + // happen. + FML_LOG(ERROR) << "Tried to terminate an unregistered shell holder."; + FML_DCHECK(false); + + return; + } + + // We may launch multiple shell in this component. However, we will + // terminate when the last shell goes away. The error code return to the + // component controller will be the last isolate that had an error. + auto return_code = shell_holder->GetEngineReturnCode(); + if (return_code.has_value()) { + last_return_code_ = {true, return_code.value()}; + } else { + FML_LOG(ERROR) << "Failed to get return code from terminated shell holder."; + } + + shell_holders_.erase(found); + + if (shell_holders_.size() == 0) { + FML_VLOG(-1) << "Killing component because all shell holders have been " + "terminated."; + Kill(); + // WARNING: Don't do anything past this point because the delegate may have + // collected this instance via the termination callback. + } +} + +void ComponentV1::CreateView( + zx::eventpair token, + fidl::InterfaceRequest /*incoming_services*/, + fidl::InterfaceHandle< + fuchsia::sys::ServiceProvider> /*outgoing_services*/) { + auto view_ref_pair = scenic::ViewRefPair::New(); + CreateViewWithViewRef(std::move(token), std::move(view_ref_pair.control_ref), + std::move(view_ref_pair.view_ref)); +} + +void ComponentV1::CreateViewWithViewRef( + zx::eventpair view_token, + fuchsia::ui::views::ViewRefControl control_ref, + fuchsia::ui::views::ViewRef view_ref) { + if (!svc_) { + FML_DLOG(ERROR) + << "Component incoming services was invalid when attempting to " + "create a shell for a view provider request."; + return; + } + + shell_holders_.emplace(std::make_unique( + *this, // delegate + debug_label_, // thread label + svc_, // Component incoming services + runner_incoming_services_, // Runner incoming services + settings_, // settings + scenic::ToViewToken(std::move(view_token)), // view token + scenic::ViewRefPair{ + .control_ref = std::move(control_ref), + .view_ref = std::move(view_ref), + }, + std::move(fdio_ns_), // FDIO namespace + std::move(directory_request_), // outgoing request + product_config_ // product configuration + )); +} + +void ComponentV1::CreateView2(fuchsia::ui::app::CreateView2Args view_args) { + if (!svc_) { + FML_DLOG(ERROR) + << "Component incoming services was invalid when attempting to " + "create a shell for a view provider request."; + return; + } + + shell_holders_.emplace(std::make_unique( + *this, // delegate + debug_label_, // thread label + svc_, // Component incoming services + runner_incoming_services_, // Runner incoming services + settings_, // settings + std::move( + *view_args.mutable_view_creation_token()), // view creation token + scenic::ViewRefPair::New(), // view ref pair + std::move(fdio_ns_), // FDIO namespace + std::move(directory_request_), // outgoing request + product_config_ // product configuration + )); +} + +#if !defined(DART_PRODUCT) +void ComponentV1::WriteProfileToTrace() const { + for (const auto& engine : shell_holders_) { + engine->WriteProfileToTrace(); + } +} +#endif // !defined(DART_PRODUCT) + +} // namespace flutter_runner diff --git a/shell/platform/fuchsia/flutter/component_v1.h b/shell/platform/fuchsia/flutter/component_v1.h new file mode 100644 index 0000000000000..4fa6cbaf4091e --- /dev/null +++ b/shell/platform/fuchsia/flutter/component_v1.h @@ -0,0 +1,140 @@ +// 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_FUCHSIA_COMPONENT_V1_H_ +#define FLUTTER_SHELL_PLATFORM_FUCHSIA_COMPONENT_V1_H_ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "flutter/common/settings.h" +#include "flutter/fml/macros.h" + +#include "engine.h" +#include "flutter_runner_product_configuration.h" +#include "unique_fdio_ns.h" + +namespace flutter_runner { + +class ComponentV1; + +struct ActiveComponentV1 { + std::unique_ptr platform_thread; + std::unique_ptr component; + + ActiveComponentV1& operator=(ActiveComponentV1&& other) noexcept { + if (this != &other) { + this->platform_thread.reset(other.platform_thread.release()); + this->component.reset(other.component.release()); + } + return *this; + } + + ~ActiveComponentV1() = default; +}; + +// Represents an instance of a CF v1 Flutter component that contains one or more +// Flutter engine instances. +class ComponentV1 final : public Engine::Delegate, + public fuchsia::sys::ComponentController, + public fuchsia::ui::app::ViewProvider { + public: + using TerminationCallback = fit::function; + + // Creates a dedicated thread to run the component and creates the + // component on it. The component can be accessed only on this thread. + // This is a synchronous operation. + static ActiveComponentV1 Create( + TerminationCallback termination_callback, + fuchsia::sys::Package package, + fuchsia::sys::StartupInfo startup_info, + std::shared_ptr runner_incoming_services, + fidl::InterfaceRequest controller); + + // Must be called on the same thread returned from the create call. The thread + // may be collected after. + ~ComponentV1(); + + static void ParseProgramMetadata( + const fidl::VectorPtr& program_metadata, + std::string* data_path, + std::string* assets_path); + + const std::string& GetDebugLabel() const; + +#if !defined(DART_PRODUCT) + void WriteProfileToTrace() const; +#endif // !defined(DART_PRODUCT) + + private: + ComponentV1( + TerminationCallback termination_callback, + fuchsia::sys::Package package, + fuchsia::sys::StartupInfo startup_info, + std::shared_ptr runner_incoming_services, + fidl::InterfaceRequest controller); + + // |fuchsia::sys::ComponentController| + void Kill() override; + + // |fuchsia::sys::ComponentController| + void Detach() override; + + // |fuchsia::ui::app::ViewProvider| + void CreateView( + zx::eventpair token, + fidl::InterfaceRequest incoming_services, + fuchsia::sys::ServiceProviderHandle outgoing_services) override; + + // |fuchsia::ui::app::ViewProvider| + void CreateViewWithViewRef(zx::eventpair view_token, + fuchsia::ui::views::ViewRefControl control_ref, + fuchsia::ui::views::ViewRef view_ref) override; + + // |fuchsia::ui::app::ViewProvider| + void CreateView2(fuchsia::ui::app::CreateView2Args view_args) override; + + // |flutter::Engine::Delegate| + void OnEngineTerminate(const Engine* holder) override; + + flutter::Settings settings_; + FlutterRunnerProductConfiguration product_config_; + TerminationCallback termination_callback_; + const std::string debug_label_; + UniqueFDIONS fdio_ns_ = UniqueFDIONSCreate(); + fml::UniqueFD component_data_directory_; + fml::UniqueFD component_assets_directory_; + + fidl::Binding component_controller_; + fuchsia::io::DirectoryPtr directory_ptr_; + fuchsia::io::NodePtr cloned_directory_ptr_; + fidl::InterfaceRequest directory_request_; + std::unique_ptr outgoing_dir_; + std::shared_ptr svc_; + std::shared_ptr runner_incoming_services_; + fidl::BindingSet shells_bindings_; + + fml::RefPtr isolate_snapshot_; + std::set> shell_holders_; + std::pair last_return_code_; + fml::WeakPtrFactory weak_factory_; // Must be the last member. + FML_DISALLOW_COPY_AND_ASSIGN(ComponentV1); +}; + +} // namespace flutter_runner + +#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_COMPONENT_V1_H_ diff --git a/shell/platform/fuchsia/flutter/component_v1_unittest.cc b/shell/platform/fuchsia/flutter/component_v1_unittest.cc new file mode 100644 index 0000000000000..26bcbd4c44a69 --- /dev/null +++ b/shell/platform/fuchsia/flutter/component_v1_unittest.cc @@ -0,0 +1,51 @@ +// 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 "component_v1.h" + +#include + +namespace flutter_runner { +namespace { + +TEST(ComponentV1, ParseProgramMetadata) { + std::string data_path; + std::string assets_path; + + // The ProgramMetadata field may be null. We should parse this as if no + // fields were specified. + ComponentV1::ParseProgramMetadata(nullptr, &data_path, &assets_path); + + EXPECT_EQ(data_path, ""); + EXPECT_EQ(assets_path, ""); + + // The ProgramMetadata field may be empty. Treat this the same as null. + fidl::VectorPtr program_metadata(size_t{0}); + + ComponentV1::ParseProgramMetadata(program_metadata, &data_path, &assets_path); + + EXPECT_EQ(data_path, ""); + EXPECT_EQ(assets_path, ""); + + // The assets_path defaults to the "data" value if unspecified + program_metadata = {{"data", "foobar"}}; + + ComponentV1::ParseProgramMetadata(program_metadata, &data_path, &assets_path); + + EXPECT_EQ(data_path, "pkg/foobar"); + EXPECT_EQ(assets_path, "pkg/foobar"); + + data_path = ""; + assets_path = ""; + + program_metadata = {{"not_data", "foo"}, {"data", "bar"}, {"assets", "baz"}}; + + ComponentV1::ParseProgramMetadata(program_metadata, &data_path, &assets_path); + + EXPECT_EQ(data_path, "pkg/bar"); + EXPECT_EQ(assets_path, "pkg/baz"); +} + +} // anonymous namespace +} // namespace flutter_runner diff --git a/shell/platform/fuchsia/flutter/component_v2.cc b/shell/platform/fuchsia/flutter/component_v2.cc new file mode 100644 index 0000000000000..f5f83c9ccdc4b --- /dev/null +++ b/shell/platform/fuchsia/flutter/component_v2.cc @@ -0,0 +1,636 @@ +// 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 "component_v2.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "file_in_namespace_buffer.h" +#include "flutter/fml/mapping.h" +#include "flutter/fml/platform/fuchsia/task_observers.h" +#include "flutter/fml/synchronization/waitable_event.h" +#include "flutter/fml/unique_fd.h" +#include "flutter/runtime/dart_vm_lifecycle.h" +#include "flutter/shell/common/switches.h" +#include "runtime/dart/utils/files.h" +#include "runtime/dart/utils/handle_exception.h" +#include "runtime/dart/utils/mapped_resource.h" +#include "runtime/dart/utils/tempfs.h" +#include "runtime/dart/utils/vmo.h" + +namespace flutter_runner { +namespace { + +constexpr char kDataKey[] = "data"; +constexpr char kAssetsKey[] = "assets"; +constexpr char kTmpPath[] = "/tmp"; +constexpr char kServiceRootPath[] = "/svc"; +constexpr char kRunnerConfigPath[] = "/config/data/flutter_runner_config"; + +std::string DebugLabelForUrl(const std::string& url) { + auto found = url.rfind("/"); + if (found == std::string::npos) { + return url; + } else { + return {url, found + 1}; + } +} + +} // namespace + +void ComponentV2::ParseProgramMetadata( + const fuchsia::data::Dictionary& program_metadata, + std::string* data_path, + std::string* assets_path) { + for (const auto& entry : program_metadata.entries()) { + if (entry.key.compare(kDataKey) == 0 && entry.value != nullptr) { + *data_path = "pkg/" + entry.value->str(); + } else if (entry.key.compare(kAssetsKey) == 0 && entry.value != nullptr) { + *assets_path = "pkg/" + entry.value->str(); + } + } + + // assets_path defaults to the same as data_path if omitted. + if (assets_path->empty()) { + *assets_path = *data_path; + } +} + +ActiveComponentV2 ComponentV2::Create( + TerminationCallback termination_callback, + fuchsia::component::runner::ComponentStartInfo start_info, + std::shared_ptr runner_incoming_services, + fidl::InterfaceRequest + controller) { + auto thread = std::make_unique(); + std::unique_ptr component; + + fml::AutoResetWaitableEvent latch; + thread->GetTaskRunner()->PostTask([&]() mutable { + component.reset( + new ComponentV2(std::move(termination_callback), std::move(start_info), + runner_incoming_services, std::move(controller))); + latch.Signal(); + }); + + latch.Wait(); + return {.platform_thread = std::move(thread), + .component = std::move(component)}; +} + +ComponentV2::ComponentV2( + TerminationCallback termination_callback, + fuchsia::component::runner::ComponentStartInfo start_info, + std::shared_ptr runner_incoming_services, + fidl::InterfaceRequest + component_controller_request) + : termination_callback_(std::move(termination_callback)), + debug_label_(DebugLabelForUrl(start_info.resolved_url())), + component_controller_(this), + outgoing_dir_(new vfs::PseudoDir()), + runtime_dir_(new vfs::PseudoDir()), + runner_incoming_services_(runner_incoming_services), + weak_factory_(this) { + component_controller_.set_error_handler([this](zx_status_t status) { + FML_LOG(ERROR) << "ComponentController binding error for component(" + << debug_label_ << "): " << zx_status_get_string(status); + KillWithEpitaph( + zx_status_t(fuchsia::component::Error::INSTANCE_CANNOT_START)); + }); + + FML_DCHECK(fdio_ns_.is_valid()); + + // TODO(fxb/50694): Dart launch arguments. + FML_LOG(WARNING) << "program() arguments are currently ignored (fxb/50694)."; + + // Determine where data and assets are stored within /pkg. + std::string data_path; + std::string assets_path; + ParseProgramMetadata(start_info.program(), &data_path, &assets_path); + + if (data_path.empty()) { + FML_DLOG(ERROR) << "Could not find a /pkg/data directory for " + << start_info.resolved_url(); + return; + } + + // Setup /tmp to be mapped to the process-local memfs. + dart_utils::RunnerTemp::SetupComponent(fdio_ns_.get()); + + // ComponentStartInfo::ns (optional) + if (start_info.has_ns()) { + for (auto& entry : *start_info.mutable_ns()) { + // /tmp/ is mapped separately to the process-level memfs, so we ignore it + // here. + const auto& path = entry.path(); + if (path == kTmpPath) { + continue; + } + + // We should never receive namespace entries without a directory, but we + // check it anyways to avoid crashing if we do. + if (!entry.has_directory()) { + FML_DLOG(ERROR) << "Namespace entry at path (" << path + << ") has no directory."; + continue; + } + + zx::channel dir; + if (path == kServiceRootPath) { + svc_ = std::make_unique( + std::move(*entry.mutable_directory())); + dir = svc_->CloneChannel().TakeChannel(); + } else { + dir = entry.mutable_directory()->TakeChannel(); + } + + zx_handle_t dir_handle = dir.release(); + if (fdio_ns_bind(fdio_ns_.get(), path.data(), dir_handle) != ZX_OK) { + FML_DLOG(ERROR) << "Could not bind path to namespace: " << path; + zx_handle_close(dir_handle); + } + } + } + + // Open the data and assets directories inside our namespace. + { + fml::UniqueFD ns_fd(fdio_ns_opendir(fdio_ns_.get())); + FML_DCHECK(ns_fd.is_valid()); + + constexpr mode_t mode = O_RDONLY | O_DIRECTORY; + + component_assets_directory_.reset( + openat(ns_fd.get(), assets_path.c_str(), mode)); + FML_DCHECK(component_assets_directory_.is_valid()); + + component_data_directory_.reset( + openat(ns_fd.get(), data_path.c_str(), mode)); + FML_DCHECK(component_data_directory_.is_valid()); + } + + // ComponentStartInfo::runtime_dir (optional). + if (start_info.has_runtime_dir()) { + runtime_dir_->Serve(fuchsia::io::OPEN_RIGHT_READABLE | + fuchsia::io::OPEN_RIGHT_WRITABLE | + fuchsia::io::OPEN_FLAG_DIRECTORY, + start_info.mutable_runtime_dir()->TakeChannel()); + } + + // ComponentStartInfo::outgoing_dir (optional). + if (start_info.has_outgoing_dir()) { + outgoing_dir_->Serve(fuchsia::io::OPEN_RIGHT_READABLE | + fuchsia::io::OPEN_RIGHT_WRITABLE | + fuchsia::io::OPEN_FLAG_DIRECTORY, + start_info.mutable_outgoing_dir()->TakeChannel()); + } + + directory_request_ = directory_ptr_.NewRequest(); + + fuchsia::io::DirectoryHandle flutter_public_dir; + // TODO(anmittal): when fixing enumeration using new c++ vfs, make sure that + // flutter_public_dir is only accessed once we receive OnOpen Event. + // That will prevent FL-175 for public directory + auto request = flutter_public_dir.NewRequest().TakeChannel(); + fdio_service_connect_at(directory_ptr_.channel().get(), "svc", + request.release()); + + auto composed_service_dir = std::make_unique(); + composed_service_dir->set_fallback(std::move(flutter_public_dir)); + + // Clone and check if client is servicing the directory. + directory_ptr_->Clone(fuchsia::io::OPEN_FLAG_DESCRIBE | + fuchsia::io::OPEN_RIGHT_READABLE | + fuchsia::io::OPEN_RIGHT_WRITABLE, + cloned_directory_ptr_.NewRequest()); + + cloned_directory_ptr_.events().OnOpen = + [this](zx_status_t status, std::unique_ptr info) { + cloned_directory_ptr_.Unbind(); + if (status != ZX_OK) { + FML_LOG(ERROR) + << "could not bind out directory for flutter component(" + << debug_label_ << "): " << zx_status_get_string(status); + return; + } + const char* other_dirs[] = {"debug", "ctrl", "diagnostics"}; + // add other directories as RemoteDirs. + for (auto& dir_str : other_dirs) { + fuchsia::io::DirectoryHandle dir; + auto request = dir.NewRequest().TakeChannel(); + auto status = fdio_service_connect_at(directory_ptr_.channel().get(), + dir_str, request.release()); + if (status == ZX_OK) { + outgoing_dir_->AddEntry( + dir_str, std::make_unique(dir.TakeChannel())); + } else { + FML_LOG(ERROR) << "could not add out directory entry(" << dir_str + << ") for flutter component(" << debug_label_ + << "): " << zx_status_get_string(status); + } + } + }; + + cloned_directory_ptr_.set_error_handler( + [this](zx_status_t status) { cloned_directory_ptr_.Unbind(); }); + + // TODO(fxb/50694): Close handles from ComponentStartInfo::numbered_handles + // since we're not using them. See documentation from ComponentController: + // https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/fidl/fuchsia.component.runner/component_runner.fidl;l=97;drc=e3b39f2b57e720770773b857feca4f770ee0619e + + // TODO(fxb/50694): There's an OnPublishDiagnostics event we may want to + // fire for diagnostics. See documentation from ComponentController: + // https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/fidl/fuchsia.component.runner/component_runner.fidl;l=181;drc=e3b39f2b57e720770773b857feca4f770ee0619e + + // All launch arguments have been read. Perform service binding and + // final settings configuration. The next call will be to create a view + // for this component. + composed_service_dir->AddService( + fuchsia::ui::app::ViewProvider::Name_, + std::make_unique( + [this](zx::channel channel, async_dispatcher_t* dispatcher) { + shells_bindings_.AddBinding( + this, fidl::InterfaceRequest( + std::move(channel))); + })); + outgoing_dir_->AddEntry("svc", std::move(composed_service_dir)); + + // Setup the component controller binding. + if (component_controller_request) { + component_controller_.Bind(std::move(component_controller_request)); + } + + // Load and use runner-specific configuration, if it exists. + std::string json_string; + if (dart_utils::ReadFileToString(kRunnerConfigPath, &json_string)) { + product_config_ = FlutterRunnerProductConfiguration(json_string); + FML_LOG(INFO) << "Successfully loaded runner configuration: " + << json_string; + } else { + FML_LOG(WARNING) << "Failed to load runner configuration from " + << kRunnerConfigPath << "; using default config values."; + } + + // Load VM and component bytecode. + // For AOT, compare with flutter_aot_app in flutter_app.gni. + // For JIT, compare flutter_jit_runner in BUILD.gn. + if (flutter::DartVM::IsRunningPrecompiledCode()) { + std::shared_ptr snapshot = + std::make_shared(); + if (snapshot->Load(component_data_directory_.get(), + "app_aot_snapshot.so")) { + const uint8_t* isolate_data = snapshot->IsolateData(); + const uint8_t* isolate_instructions = snapshot->IsolateInstrs(); + const uint8_t* vm_data = snapshot->VmData(); + const uint8_t* vm_instructions = snapshot->VmInstrs(); + if (isolate_data == nullptr || isolate_instructions == nullptr || + vm_data == nullptr || vm_instructions == nullptr) { + FML_LOG(FATAL) << "ELF snapshot missing AOT symbols."; + return; + } + auto hold_snapshot = [snapshot](const uint8_t* _, size_t __) {}; + settings_.vm_snapshot_data = [hold_snapshot, vm_data]() { + return std::make_unique(vm_data, 0, hold_snapshot, + true /* dontneed_safe */); + }; + settings_.vm_snapshot_instr = [hold_snapshot, vm_instructions]() { + return std::make_unique( + vm_instructions, 0, hold_snapshot, true /* dontneed_safe */); + }; + settings_.isolate_snapshot_data = [hold_snapshot, isolate_data]() { + return std::make_unique( + isolate_data, 0, hold_snapshot, true /* dontneed_safe */); + }; + settings_.isolate_snapshot_instr = [hold_snapshot, + isolate_instructions]() { + return std::make_unique( + isolate_instructions, 0, hold_snapshot, true /* dontneed_safe */); + }; + isolate_snapshot_ = fml::MakeRefCounted( + std::make_shared(isolate_data, 0, hold_snapshot, + true /* dontneed_safe */), + std::make_shared(isolate_instructions, 0, + hold_snapshot, + true /* dontneed_safe */)); + } else { + const int namespace_fd = component_data_directory_.get(); + settings_.vm_snapshot_data = [namespace_fd]() { + return LoadFile(namespace_fd, "vm_snapshot_data.bin", + false /* executable */); + }; + settings_.vm_snapshot_instr = [namespace_fd]() { + return LoadFile(namespace_fd, "vm_snapshot_instructions.bin", + true /* executable */); + }; + settings_.isolate_snapshot_data = [namespace_fd]() { + return LoadFile(namespace_fd, "isolate_snapshot_data.bin", + false /* executable */); + }; + settings_.isolate_snapshot_instr = [namespace_fd]() { + return LoadFile(namespace_fd, "isolate_snapshot_instructions.bin", + true /* executable */); + }; + } + } else { + settings_.vm_snapshot_data = []() { + return MakeFileMapping("/pkg/data/vm_snapshot_data.bin", + false /* executable */); + }; + settings_.vm_snapshot_instr = []() { + return MakeFileMapping("/pkg/data/vm_snapshot_instructions.bin", + true /* executable */); + }; + + settings_.isolate_snapshot_data = []() { + return MakeFileMapping("/pkg/data/isolate_core_snapshot_data.bin", + false /* executable */); + }; + settings_.isolate_snapshot_instr = [] { + return MakeFileMapping("/pkg/data/isolate_core_snapshot_instructions.bin", + true /* executable */); + }; + } + +#if defined(DART_PRODUCT) + settings_.enable_observatory = false; +#else + settings_.enable_observatory = true; + + // TODO(cbracken): pass this in as a param to allow 0.0.0.0, ::1, etc. + settings_.observatory_host = "127.0.0.1"; +#endif + + // Controls whether category "skia" trace events are enabled. + settings_.trace_skia = true; + + settings_.verbose_logging = true; + + settings_.advisory_script_uri = debug_label_; + + settings_.advisory_script_entrypoint = debug_label_; + + settings_.icu_data_path = ""; + + settings_.assets_dir = component_assets_directory_.get(); + + // Compare flutter_jit_app in flutter_app.gni. + settings_.application_kernel_list_asset = "app.dilplist"; + + settings_.log_tag = debug_label_ + std::string{"(flutter)"}; + + // No asserts in debug or release product. + // No asserts in release with flutter_profile=true (non-product) + // Yes asserts in non-product debug. +#if !defined(DART_PRODUCT) && (!defined(FLUTTER_PROFILE) || !defined(NDEBUG)) + // Debug mode + settings_.disable_dart_asserts = false; +#else + // Release mode + settings_.disable_dart_asserts = true; +#endif + + // Do not leak the VM; allow it to shut down normally when the last shell + // terminates. + settings_.leak_vm = false; + + settings_.task_observer_add = + std::bind(&fml::CurrentMessageLoopAddAfterTaskObserver, + std::placeholders::_1, std::placeholders::_2); + + settings_.task_observer_remove = std::bind( + &fml::CurrentMessageLoopRemoveAfterTaskObserver, std::placeholders::_1); + + settings_.log_message_callback = [](const std::string& tag, + const std::string& message) { + if (tag.size() > 0) { + std::cout << tag << ": "; + } + std::cout << message << std::endl; + }; + + settings_.dart_flags = {"--lazy_async_stacks"}; + + // Don't collect CPU samples from Dart VM C++ code. + settings_.dart_flags.push_back("--no_profile_vm"); + + // Scale back CPU profiler sampling period on ARM64 to avoid overloading + // the tracing engine. +#if defined(__aarch64__) + settings_.dart_flags.push_back("--profile_period=10000"); +#endif // defined(__aarch64__) + + auto platform_task_runner = fml::MessageLoop::GetCurrent().GetTaskRunner(); + const std::string component_url = start_info.resolved_url(); + settings_.unhandled_exception_callback = [weak = weak_factory_.GetWeakPtr(), + platform_task_runner, + runner_incoming_services, + component_url]( + const std::string& error, + const std::string& stack_trace) { + if (weak) { + // TODO(cbracken): unsafe. The above check and the PostTask below are + // happening on the UI thread. If the Component dtor and thread + // termination happen (on the platform thread) between the previous + // line and the next line, a crash will occur since we'll be posting + // to a dead thread. See Runner::OnComponentV2Terminate() in + // runner.cc. + platform_task_runner->PostTask([weak, runner_incoming_services, + component_url, error, stack_trace]() { + if (weak) { + dart_utils::HandleException(runner_incoming_services, component_url, + error, stack_trace); + } else { + FML_LOG(WARNING) + << "Exception was thrown which was not caught in Flutter app: " + << error; + } + }); + } else { + FML_LOG(WARNING) + << "Exception was thrown which was not caught in Flutter app: " + << error; + } + // Ideally we would return whether HandleException returned ZX_OK, but + // short of knowing if the exception was correctly handled, we return + // false to have the error and stack trace printed in the logs. + return false; + }; +} + +ComponentV2::~ComponentV2() = default; + +const std::string& ComponentV2::GetDebugLabel() const { + return debug_label_; +} + +void ComponentV2::Kill() { + FML_VLOG(-1) << "received Kill event"; + + // From the documentation for ComponentController, ZX_OK should be sent when + // the ComponentController receives a termination request. However, if the + // component exited with a non-zero return code, we indicate this by sending + // an INTERNAL epitaph instead. + // + // TODO(fxb/86666): Communicate return code from the ComponentController once + // v2 has support. + auto [got_return_code, return_code] = last_return_code_; + if (got_return_code && return_code == 0) { + KillWithEpitaph(ZX_OK); + } else { + if (got_return_code) { + FML_LOG(ERROR) << "Component exited with non-zero return code: " + << return_code; + } else { + FML_LOG(ERROR) << "Failed to get return code for component"; + } + + KillWithEpitaph(zx_status_t(fuchsia::component::Error::INTERNAL)); + } + + // WARNING: Don't do anything past this point as this instance may have been + // collected. +} + +void ComponentV2::KillWithEpitaph(zx_status_t epitaph_status) { + component_controller_.set_error_handler(nullptr); + component_controller_.Close(epitaph_status); + + termination_callback_(this); + // WARNING: Don't do anything past this point as this instance may have been + // collected. +} + +void ComponentV2::Stop() { + FML_VLOG(-1) << "received Stop event"; + + // TODO(fxb/50694): Any other cleanup logic we should do that's appropriate + // for Stop but not for Kill? + KillWithEpitaph(ZX_OK); +} + +void ComponentV2::OnEngineTerminate(const Engine* shell_holder) { + auto found = std::find_if(shell_holders_.begin(), shell_holders_.end(), + [shell_holder](const auto& holder) { + return holder.get() == shell_holder; + }); + + if (found == shell_holders_.end()) { + // This indicates a deeper issue with memory management and should never + // happen. + FML_LOG(ERROR) << "Tried to terminate an unregistered shell holder."; + FML_DCHECK(false); + + return; + } + + // We may launch multiple shells in this component. However, we will + // terminate when the last shell goes away. The error code returned to the + // component controller will be the last isolate that had an error. + auto return_code = shell_holder->GetEngineReturnCode(); + if (return_code.has_value()) { + last_return_code_ = {true, return_code.value()}; + } else { + FML_LOG(ERROR) << "Failed to get return code from terminated shell holder."; + } + + shell_holders_.erase(found); + + if (shell_holders_.size() == 0) { + FML_VLOG(-1) << "Killing component because all shell holders have been " + "terminated."; + Kill(); + // WARNING: Don't do anything past this point because the delegate may have + // collected this instance via the termination callback. + } +} + +void ComponentV2::CreateView( + zx::eventpair token, + fidl::InterfaceRequest /*incoming_services*/, + fidl::InterfaceHandle< + fuchsia::sys::ServiceProvider> /*outgoing_services*/) { + auto view_ref_pair = scenic::ViewRefPair::New(); + CreateViewWithViewRef(std::move(token), std::move(view_ref_pair.control_ref), + std::move(view_ref_pair.view_ref)); +} + +void ComponentV2::CreateViewWithViewRef( + zx::eventpair view_token, + fuchsia::ui::views::ViewRefControl control_ref, + fuchsia::ui::views::ViewRef view_ref) { + if (!svc_) { + FML_DLOG(ERROR) + << "Component incoming services was invalid when attempting to " + "create a shell for a view provider request."; + return; + } + + shell_holders_.emplace(std::make_unique( + *this, // delegate + debug_label_, // thread label + svc_, // Component incoming services + runner_incoming_services_, // Runner incoming services + settings_, // settings + scenic::ToViewToken(std::move(view_token)), // view token + scenic::ViewRefPair{ + .control_ref = std::move(control_ref), + .view_ref = std::move(view_ref), + }, + std::move(fdio_ns_), // FDIO namespace + std::move(directory_request_), // outgoing request + product_config_ // product configuration + )); +} + +void ComponentV2::CreateView2(fuchsia::ui::app::CreateView2Args view_args) { + if (!svc_) { + FML_DLOG(ERROR) + << "Component incoming services was invalid when attempting to " + "create a shell for a view provider request."; + return; + } + + shell_holders_.emplace(std::make_unique( + *this, // delegate + debug_label_, // thread label + svc_, // Component incoming services + runner_incoming_services_, // Runner incoming services + settings_, // settings + std::move( + *view_args.mutable_view_creation_token()), // view creation token + scenic::ViewRefPair::New(), // view ref pair + std::move(fdio_ns_), // FDIO namespace + std::move(directory_request_), // outgoing request + product_config_ // product configuration + )); +} + +#if !defined(DART_PRODUCT) +void ComponentV2::WriteProfileToTrace() const { + for (const auto& engine : shell_holders_) { + engine->WriteProfileToTrace(); + } +} +#endif // !defined(DART_PRODUCT) + +} // namespace flutter_runner diff --git a/shell/platform/fuchsia/flutter/component_v2.h b/shell/platform/fuchsia/flutter/component_v2.h new file mode 100644 index 0000000000000..33acbf64e98e7 --- /dev/null +++ b/shell/platform/fuchsia/flutter/component_v2.h @@ -0,0 +1,156 @@ +// 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_FUCHSIA_COMPONENT_V2_H_ +#define FLUTTER_SHELL_PLATFORM_FUCHSIA_COMPONENT_V2_H_ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "flutter/common/settings.h" +#include "flutter/fml/macros.h" + +#include "engine.h" +#include "flutter_runner_product_configuration.h" +#include "unique_fdio_ns.h" + +namespace flutter_runner { + +class ComponentV2; + +struct ActiveComponentV2 { + std::unique_ptr platform_thread; + std::unique_ptr component; + + ActiveComponentV2& operator=(ActiveComponentV2&& other) noexcept { + if (this != &other) { + this->platform_thread.reset(other.platform_thread.release()); + this->component.reset(other.component.release()); + } + return *this; + } + + ~ActiveComponentV2() = default; +}; + +// Represents an instance of a CF v2 Flutter component that contains one or more +// Flutter engine instances. +// +// TODO(fxb/50694): Add unit tests once we've verified that the current behavior +// is working correctly. +class ComponentV2 final + : public Engine::Delegate, + public fuchsia::component::runner::ComponentController, + public fuchsia::ui::app::ViewProvider { + public: + using TerminationCallback = fit::function; + + // Creates a dedicated thread to run the component and creates the + // component on it. The component can be accessed only on this thread. + // This is a synchronous operation. + static ActiveComponentV2 Create( + TerminationCallback termination_callback, + fuchsia::component::runner::ComponentStartInfo start_info, + std::shared_ptr runner_incoming_services, + fidl::InterfaceRequest + controller); + + // Must be called on the same thread returned from the create call. The thread + // may be collected after. + ~ComponentV2(); + + static void ParseProgramMetadata( + const fuchsia::data::Dictionary& program_metadata, + std::string* data_path, + std::string* assets_path); + + const std::string& GetDebugLabel() const; + +#if !defined(DART_PRODUCT) + void WriteProfileToTrace() const; +#endif // !defined(DART_PRODUCT) + + private: + ComponentV2( + TerminationCallback termination_callback, + fuchsia::component::runner::ComponentStartInfo start_info, + std::shared_ptr runner_incoming_services, + fidl::InterfaceRequest + controller); + + // |fuchsia::component::runner::ComponentController| + void Kill() override; + + /// Helper to actually |Kill| the component, closing the connection via an + /// epitaph with the given |epitaph_status|. Call this instead of + /// Kill() in the implementation of this class, as |Kill| is only intended for + /// clients of the ComponentController protocol to call. + /// + /// To determine what |epitaph_status| is appropriate for your situation, + /// see the documentation for |fuchsia.component.runner.ComponentController|. + void KillWithEpitaph(zx_status_t epitaph_status); + + // |fuchsia::component::runner::ComponentController| + void Stop() override; + + // |fuchsia::ui::app::ViewProvider| + void CreateView( + zx::eventpair token, + fidl::InterfaceRequest incoming_services, + fuchsia::sys::ServiceProviderHandle outgoing_services) override; + + // |fuchsia::ui::app::ViewProvider| + void CreateViewWithViewRef(zx::eventpair view_token, + fuchsia::ui::views::ViewRefControl control_ref, + fuchsia::ui::views::ViewRef view_ref) override; + + // |fuchsia::ui::app::ViewProvider| + void CreateView2(fuchsia::ui::app::CreateView2Args view_args) override; + + // |flutter::Engine::Delegate| + void OnEngineTerminate(const Engine* holder) override; + + flutter::Settings settings_; + FlutterRunnerProductConfiguration product_config_; + TerminationCallback termination_callback_; + const std::string debug_label_; + UniqueFDIONS fdio_ns_ = UniqueFDIONSCreate(); + fml::UniqueFD component_data_directory_; + fml::UniqueFD component_assets_directory_; + + fidl::Binding + component_controller_; + fuchsia::io::DirectoryPtr directory_ptr_; + fuchsia::io::NodePtr cloned_directory_ptr_; + fidl::InterfaceRequest directory_request_; + std::unique_ptr outgoing_dir_; + std::unique_ptr runtime_dir_; + std::shared_ptr svc_; + std::shared_ptr runner_incoming_services_; + fidl::BindingSet shells_bindings_; + + fml::RefPtr isolate_snapshot_; + std::set> shell_holders_; + std::pair last_return_code_; + fml::WeakPtrFactory weak_factory_; // Must be the last member. + FML_DISALLOW_COPY_AND_ASSIGN(ComponentV2); +}; + +} // namespace flutter_runner + +#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_COMPONENT_V2_H_ diff --git a/shell/platform/fuchsia/flutter/engine.cc b/shell/platform/fuchsia/flutter/engine.cc index 06a0da74f925f..859208b10c938 100644 --- a/shell/platform/fuchsia/flutter/engine.cc +++ b/shell/platform/fuchsia/flutter/engine.cc @@ -138,20 +138,22 @@ void Engine::Initialize( // Connect to Scenic. auto scenic = runner_services->Connect(); fuchsia::ui::scenic::SessionEndpoints gfx_protocols; - fidl::InterfaceHandle session; + fuchsia::ui::scenic::SessionHandle session; gfx_protocols.set_session(session.NewRequest()); - fidl::InterfaceHandle session_listener; + fuchsia::ui::scenic::SessionListenerHandle session_listener; auto session_listener_request = session_listener.NewRequest(); gfx_protocols.set_session_listener(session_listener.Bind()); - fidl::InterfaceHandle focuser; - fidl::InterfaceHandle view_ref_focused; - fidl::InterfaceHandle touch_source; + fuchsia::ui::views::FocuserHandle focuser; + fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused; + fuchsia::ui::pointer::TouchSourceHandle touch_source; + fuchsia::ui::pointer::MouseSourceHandle mouse_source; fuchsia::ui::composition::ViewBoundProtocols flatland_view_protocols; if (use_flatland) { flatland_view_protocols.set_view_focuser(focuser.NewRequest()); flatland_view_protocols.set_view_ref_focused(view_ref_focused.NewRequest()); flatland_view_protocols.set_touch_source(touch_source.NewRequest()); + flatland_view_protocols.set_mouse_source(mouse_source.NewRequest()); } else { gfx_protocols.set_view_focuser(focuser.NewRequest()); gfx_protocols.set_view_ref_focused(view_ref_focused.NewRequest()); @@ -161,7 +163,7 @@ void Engine::Initialize( scenic->CreateSessionT(std::move(gfx_protocols), [] {}); // Connect to Flatland. - fidl::InterfaceHandle flatland; + fuchsia::ui::composition::FlatlandHandle flatland; zx_status_t flatland_status = runner_services->Connect( flatland.NewRequest()); @@ -171,8 +173,7 @@ void Engine::Initialize( } // Connect to SemanticsManager service. - fidl::InterfaceHandle - semantics_manager; + fuchsia::accessibility::semantics::SemanticsManagerHandle semantics_manager; zx_status_t semantics_status = runner_services ->Connect( @@ -185,7 +186,7 @@ void Engine::Initialize( } // Connect to ImeService service. - fidl::InterfaceHandle ime_service; + fuchsia::ui::input::ImeServiceHandle ime_service; zx_status_t ime_status = runner_services->Connect( ime_service.NewRequest()); @@ -195,7 +196,7 @@ void Engine::Initialize( } // Connect to Keyboard service. - fidl::InterfaceHandle keyboard; + fuchsia::ui::input3::KeyboardHandle keyboard; zx_status_t keyboard_status = runner_services->Connect( keyboard.NewRequest()); @@ -221,6 +222,7 @@ void Engine::Initialize( weak = weak_factory_.GetWeakPtr()]() { task_runner->PostTask([weak]() { if (weak) { + FML_LOG(ERROR) << "Terminating from session_error_callback"; weak->Terminate(); } }); @@ -256,10 +258,10 @@ void Engine::Initialize( .view_ref_control = std::move(view_ref_pair.control_ref)}; flatland_view_embedder_ = std::make_shared( - thread_label_, std::move(view_creation_token), - std::move(view_identity), std::move(flatland_view_protocols), - std::move(request), *flatland_connection_.get(), - surface_producer_.value(), intercept_all_input_); + std::move(view_creation_token), std::move(view_identity), + std::move(flatland_view_protocols), std::move(request), + *flatland_connection_.get(), surface_producer_.value(), + intercept_all_input_); } else { session_connection_ = std::make_shared( thread_label_, std::move(session_inspect_node), @@ -339,6 +341,8 @@ void Engine::Initialize( weak = weak_factory_.GetWeakPtr()]() { task_runner->PostTask([weak]() { if (weak) { + FML_LOG(ERROR) << "Terminating from " + "on_session_listener_error_callback"; weak->Terminate(); } }); @@ -370,6 +374,7 @@ void Engine::Initialize( focuser = std::move(focuser), view_ref_focused = std::move(view_ref_focused), touch_source = std::move(touch_source), + mouse_source = std::move(mouse_source), on_session_listener_error_callback = std::move(on_session_listener_error_callback), on_enable_wireframe_callback = @@ -445,7 +450,8 @@ void Engine::Initialize( shell, shell.GetTaskRunners(), std::move(view_ref), std::move(external_view_embedder), std::move(ime_service), std::move(keyboard), std::move(touch_source), - std::move(focuser), std::move(view_ref_focused), + std::move(mouse_source), std::move(focuser), + std::move(view_ref_focused), std::move(parent_viewport_watcher), std::move(on_enable_wireframe_callback), std::move(on_create_flatland_view_callback), @@ -462,7 +468,8 @@ void Engine::Initialize( shell, shell.GetTaskRunners(), std::move(view_ref), std::move(external_view_embedder), std::move(ime_service), std::move(keyboard), std::move(touch_source), - std::move(focuser), std::move(view_ref_focused), + std::move(mouse_source), std::move(focuser), + std::move(view_ref_focused), std::move(session_listener_request), std::move(on_session_listener_error_callback), std::move(on_enable_wireframe_callback), @@ -615,6 +622,7 @@ void Engine::Initialize( // The engine could have been killed by the caller right after the // constructor was called but before it could run on the UI thread. if (weak) { + FML_LOG(ERROR) << "Terminating from on_run_failure"; weak->Terminate(); } }; diff --git a/shell/platform/fuchsia/flutter/engine.h b/shell/platform/fuchsia/flutter/engine.h index 1c2dab34c1f05..6163f2fb044e1 100644 --- a/shell/platform/fuchsia/flutter/engine.h +++ b/shell/platform/fuchsia/flutter/engine.h @@ -171,7 +171,6 @@ class Engine final : public fuchsia::memorypressure::Watcher { bool intercept_all_input_ = false; fml::WeakPtrFactory weak_factory_; - friend class testing::EngineTest; FML_DISALLOW_COPY_AND_ASSIGN(Engine); diff --git a/shell/platform/fuchsia/flutter/file_in_namespace_buffer.cc b/shell/platform/fuchsia/flutter/file_in_namespace_buffer.cc index 159499aaae44a..972b303c450e6 100644 --- a/shell/platform/fuchsia/flutter/file_in_namespace_buffer.cc +++ b/shell/platform/fuchsia/flutter/file_in_namespace_buffer.cc @@ -70,6 +70,10 @@ size_t FileInNamespaceBuffer::GetSize() const { return size_; } +bool FileInNamespaceBuffer::IsDontNeedSafe() const { + return true; +} + std::unique_ptr LoadFile(int namespace_fd, const char* path, bool executable) { diff --git a/shell/platform/fuchsia/flutter/file_in_namespace_buffer.h b/shell/platform/fuchsia/flutter/file_in_namespace_buffer.h index a299cadfecfb3..2a075edbd0f94 100644 --- a/shell/platform/fuchsia/flutter/file_in_namespace_buffer.h +++ b/shell/platform/fuchsia/flutter/file_in_namespace_buffer.h @@ -25,6 +25,9 @@ class FileInNamespaceBuffer final : public fml::Mapping { // |fml::Mapping| size_t GetSize() const override; + // |fml::Mapping| + bool IsDontNeedSafe() const override; + private: /// The address that was mapped to the buffer. void* address_; diff --git a/shell/platform/fuchsia/flutter/flatland_connection.cc b/shell/platform/fuchsia/flutter/flatland_connection.cc index 15023ead546d1..895b374da85b0 100644 --- a/shell/platform/fuchsia/flutter/flatland_connection.cc +++ b/shell/platform/fuchsia/flutter/flatland_connection.cc @@ -12,7 +12,7 @@ namespace flutter_runner { FlatlandConnection::FlatlandConnection( std::string debug_label, - fidl::InterfaceHandle flatland, + fuchsia::ui::composition::FlatlandHandle flatland, fml::closure error_callback, on_frame_presented_event on_frame_presented_callback, uint64_t max_frames_in_flight, @@ -21,7 +21,7 @@ FlatlandConnection::FlatlandConnection( error_callback_(error_callback), on_frame_presented_callback_(std::move(on_frame_presented_callback)) { flatland_.set_error_handler([callback = error_callback_](zx_status_t status) { - FML_LOG(ERROR) << "Flatland disconnected" << zx_status_get_string(status); + FML_LOG(ERROR) << "Flatland disconnected: " << zx_status_get_string(status); callback(); }); flatland_->SetDebugName(debug_label); diff --git a/shell/platform/fuchsia/flutter/flatland_connection.h b/shell/platform/fuchsia/flutter/flatland_connection.h index 2a1e2707be141..89f3a012060e9 100644 --- a/shell/platform/fuchsia/flutter/flatland_connection.h +++ b/shell/platform/fuchsia/flutter/flatland_connection.h @@ -30,13 +30,12 @@ static constexpr fml::TimeDelta kDefaultFlatlandPresentationInterval = // maintaining the Flatland instance connection and presenting updates. class FlatlandConnection final { public: - FlatlandConnection( - std::string debug_label, - fidl::InterfaceHandle flatland, - fml::closure error_callback, - on_frame_presented_event on_frame_presented_callback, - uint64_t max_frames_in_flight, - fml::TimeDelta vsync_offset); + FlatlandConnection(std::string debug_label, + fuchsia::ui::composition::FlatlandHandle flatland, + fml::closure error_callback, + on_frame_presented_event on_frame_presented_callback, + uint64_t max_frames_in_flight, + fml::TimeDelta vsync_offset); ~FlatlandConnection(); diff --git a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc index 088ac0d18c632..91e7d7a816a28 100644 --- a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc +++ b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc @@ -11,10 +11,7 @@ namespace flutter_runner { -constexpr uint32_t kFlatlandDefaultViewportSize = 32; - FlatlandExternalViewEmbedder::FlatlandExternalViewEmbedder( - std::string debug_label, fuchsia::ui::views::ViewCreationToken view_creation_token, fuchsia::ui::views::ViewIdentityOnCreation view_identity, fuchsia::ui::composition::ViewBoundProtocols view_protocols, @@ -221,6 +218,7 @@ void FlatlandExternalViewEmbedder::SubmitFrame( // Attach the FlatlandView to the main scene graph. flatland_.flatland()->AddChild(root_transform_id_, viewport.transform_id); + child_transforms_.emplace_back(viewport.transform_id); } // Acquire the surface associated with the layer. @@ -264,6 +262,8 @@ void FlatlandExternalViewEmbedder::SubmitFrame( flatland_.flatland()->AddChild( root_transform_id_, flatland_layers_[flatland_layer_index].transform_id); + child_transforms_.emplace_back( + flatland_layers_[flatland_layer_index].transform_id); } // Reset for the next pass: @@ -352,7 +352,20 @@ void FlatlandExternalViewEmbedder::DestroyView( FlatlandViewIdCallback on_view_unbound) { auto flatland_view = flatland_views_.find(view_id); FML_CHECK(flatland_view != flatland_views_.end()); + auto viewport_id = flatland_view->second.viewport_id; + auto transform_id = flatland_view->second.transform_id; + flatland_.flatland()->ReleaseViewport(viewport_id, [](auto) {}); + auto itr = + std::find_if(child_transforms_.begin(), child_transforms_.end(), + [transform_id](fuchsia::ui::composition::TransformId id) { + return id.value == transform_id.value; + }); + if (itr != child_transforms_.end()) { + flatland_.flatland()->RemoveChild(root_transform_id_, transform_id); + child_transforms_.erase(itr); + } + flatland_.flatland()->ReleaseTransform(transform_id); flatland_views_.erase(flatland_view); on_view_unbound(viewport_id); @@ -374,9 +387,15 @@ void FlatlandExternalViewEmbedder::Reset() { frame_composition_order_.clear(); frame_size_ = SkISize::Make(0, 0); + // Clear all children from root. + for (const auto& transform : child_transforms_) { + flatland_.flatland()->RemoveChild(root_transform_id_, transform); + } + child_transforms_.clear(); + // Clear images on all layers so they aren't cached unnecessarily. - for (auto& layer : flatland_layers_) { - flatland_.flatland()->RemoveChild(root_transform_id_, layer.transform_id); + for (const auto& layer : flatland_layers_) { + flatland_.flatland()->SetContent(layer.transform_id, {0}); } } diff --git a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h index f4c4a303b408d..c8017a1d24aa3 100644 --- a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h +++ b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h @@ -44,8 +44,9 @@ using FlatlandViewIdCallback = class FlatlandExternalViewEmbedder final : public flutter::ExternalViewEmbedder { public: + constexpr static uint32_t kFlatlandDefaultViewportSize = 32; + FlatlandExternalViewEmbedder( - std::string debug_label, fuchsia::ui::views::ViewCreationToken view_creation_token, fuchsia::ui::views::ViewIdentityOnCreation view_identity, fuchsia::ui::composition::ViewBoundProtocols endpoints, @@ -163,6 +164,7 @@ class FlatlandExternalViewEmbedder final }; struct FlatlandLayer { + // Transform on which Images are set. fuchsia::ui::composition::TransformId transform_id; }; @@ -177,6 +179,7 @@ class FlatlandExternalViewEmbedder final std::unordered_map frame_layers_; std::vector frame_composition_order_; + std::vector child_transforms_; SkISize frame_size_ = SkISize::Make(0, 0); FML_DISALLOW_COPY_AND_ASSIGN(FlatlandExternalViewEmbedder); diff --git a/shell/platform/fuchsia/flutter/flatland_platform_view.cc b/shell/platform/fuchsia/flutter/flatland_platform_view.cc index d585cd24f47f0..864cff3c88873 100644 --- a/shell/platform/fuchsia/flutter/flatland_platform_view.cc +++ b/shell/platform/fuchsia/flutter/flatland_platform_view.cc @@ -13,12 +13,13 @@ FlatlandPlatformView::FlatlandPlatformView( flutter::TaskRunners task_runners, fuchsia::ui::views::ViewRef view_ref, std::shared_ptr external_view_embedder, - fidl::InterfaceHandle ime_service, - fidl::InterfaceHandle keyboard, - fidl::InterfaceHandle touch_source, - fidl::InterfaceHandle focuser, - fidl::InterfaceHandle view_ref_focused, - fidl::InterfaceHandle + fuchsia::ui::input::ImeServiceHandle ime_service, + fuchsia::ui::input3::KeyboardHandle keyboard, + fuchsia::ui::pointer::TouchSourceHandle touch_source, + fuchsia::ui::pointer::MouseSourceHandle mouse_source, + fuchsia::ui::views::FocuserHandle focuser, + fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused, + fuchsia::ui::composition::ParentViewportWatcherHandle parent_viewport_watcher, OnEnableWireframe wireframe_enabled_callback, OnCreateFlatlandView on_create_view_callback, @@ -38,6 +39,7 @@ FlatlandPlatformView::FlatlandPlatformView( std::move(ime_service), std::move(keyboard), std::move(touch_source), + std::move(mouse_source), std::move(focuser), std::move(view_ref_focused), std::move(wireframe_enabled_callback), @@ -134,6 +136,21 @@ void FlatlandPlatformView::OnChildViewStatus( }); } +void FlatlandPlatformView::OnChildViewViewRef( + uint64_t content_id, + uint64_t view_id, + fuchsia::ui::views::ViewRef view_ref) { + FML_CHECK(child_view_info_.count(content_id) == 1); + + focus_delegate_->OnChildViewViewRef(view_id, std::move(view_ref)); + + child_view_info_.at(content_id) + .child_view_watcher->GetViewRef( + [this, content_id, view_id](fuchsia::ui::views::ViewRef view_ref) { + this->OnChildViewViewRef(content_id, view_id, std::move(view_ref)); + }); +} + void FlatlandPlatformView::OnCreateView(ViewCallback on_view_created, int64_t view_id_raw, bool hit_testable, @@ -145,10 +162,15 @@ void FlatlandPlatformView::OnCreateView(ViewCallback on_view_created, fuchsia::ui::composition::ContentId content_id, fuchsia::ui::composition::ChildViewWatcherPtr child_view_watcher) { + FML_CHECK(weak); + FML_CHECK(weak->child_view_info_.count(content_id.value) == 0); + FML_CHECK(child_view_watcher); + child_view_watcher.set_error_handler([](zx_status_t status) { FML_LOG(ERROR) << "Interface error on: ChildViewWatcher status: " << status; }); + platform_task_runner->PostTask( fml::MakeCopyable([weak, view_id, content_id, watcher = std::move(child_view_watcher)]() mutable { @@ -159,8 +181,6 @@ void FlatlandPlatformView::OnCreateView(ViewCallback on_view_created, return; } - FML_DCHECK(weak->child_view_info_.count(content_id.value) == 0); - FML_DCHECK(watcher); weak->child_view_info_.emplace( std::piecewise_construct, std::forward_as_tuple(content_id.value), std::forward_as_tuple(view_id, std::move(watcher))); @@ -171,6 +191,14 @@ void FlatlandPlatformView::OnCreateView(ViewCallback on_view_created, fuchsia::ui::composition::ChildViewStatus status) { weak->OnChildViewStatus(id, status); }); + + weak->child_view_info_.at(content_id.value) + .child_view_watcher->GetViewRef( + [weak, content_id = content_id.value, + view_id](fuchsia::ui::views::ViewRef view_ref) { + weak->OnChildViewViewRef(content_id, view_id, + std::move(view_ref)); + }); })); }; on_create_view_callback_(view_id_raw, std::move(on_view_created), diff --git a/shell/platform/fuchsia/flutter/flatland_platform_view.h b/shell/platform/fuchsia/flutter/flatland_platform_view.h index 0b45d8da308ac..e51650cfe6f53 100644 --- a/shell/platform/fuchsia/flutter/flatland_platform_view.h +++ b/shell/platform/fuchsia/flutter/flatland_platform_view.h @@ -27,13 +27,13 @@ class FlatlandPlatformView final : public flutter_runner::PlatformView { flutter::TaskRunners task_runners, fuchsia::ui::views::ViewRef view_ref, std::shared_ptr external_view_embedder, - fidl::InterfaceHandle ime_service, - fidl::InterfaceHandle keyboard, - fidl::InterfaceHandle touch_source, - fidl::InterfaceHandle focuser, - fidl::InterfaceHandle - view_ref_focused, - fidl::InterfaceHandle + fuchsia::ui::input::ImeServiceHandle ime_service, + fuchsia::ui::input3::KeyboardHandle keyboard, + fuchsia::ui::pointer::TouchSourceHandle touch_source, + fuchsia::ui::pointer::MouseSourceHandle mouse_source, + fuchsia::ui::views::FocuserHandle focuser, + fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused, + fuchsia::ui::composition::ParentViewportWatcherHandle parent_viewport_watcher, OnEnableWireframe wireframe_enabled_callback, OnCreateFlatlandView on_create_view_callback, @@ -53,6 +53,9 @@ class FlatlandPlatformView final : public flutter_runner::PlatformView { fuchsia::ui::composition::ParentViewportStatus status); void OnChildViewStatus(uint64_t content_id, fuchsia::ui::composition::ChildViewStatus status); + void OnChildViewViewRef(uint64_t content_id, + uint64_t view_id, + fuchsia::ui::views::ViewRef view_ref); private: void OnCreateView(ViewCallback on_view_created, diff --git a/shell/platform/fuchsia/flutter/flutter_runner_fakes.h b/shell/platform/fuchsia/flutter/flutter_runner_fakes.h index 53ad3e6c4a955..531eb9341f923 100644 --- a/shell/platform/fuchsia/flutter/flutter_runner_fakes.h +++ b/shell/platform/fuchsia/flutter/flutter_runner_fakes.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef TOPAZ_RUNTIME_FLUTTER_RUNNER_PLATFORM_VIEW_FAKES_H_ -#define TOPAZ_RUNTIME_FLUTTER_RUNNER_PLATFORM_VIEW_FAKES_H_ +#ifndef SHELL_PLATFORM_FUCHSIA_FLUTTER_FLUTTER_RUNNER_FAKES_H_ +#define SHELL_PLATFORM_FUCHSIA_FLUTTER_FLUTTER_RUNNER_FAKES_H_ #include @@ -19,8 +19,7 @@ class MockSemanticsManager // |fuchsia::accessibility::semantics::SemanticsManager|: void RegisterViewForSemantics( fuchsia::ui::views::ViewRef view_ref, - fidl::InterfaceHandle - handle, + fuchsia::accessibility::semantics::SemanticListenerHandle handle, fidl::InterfaceRequest semantic_tree) override { tree_binding_.Bind(std::move(semantic_tree)); @@ -119,4 +118,4 @@ class MockSemanticsManager } // namespace flutter_runner_test -#endif // TOPAZ_RUNTIME_FLUTTER_RUNNER_PLATFORM_VIEW_FAKES_H_ +#endif // SHELL_PLATFORM_FUCHSIA_FLUTTER_FLUTTER_RUNNER_FAKES_H_ diff --git a/shell/platform/fuchsia/flutter/focus_delegate.cc b/shell/platform/fuchsia/flutter/focus_delegate.cc index 645bc18f78ee0..b7e85828bda85 100644 --- a/shell/platform/fuchsia/flutter/focus_delegate.cc +++ b/shell/platform/fuchsia/flutter/focus_delegate.cc @@ -43,7 +43,54 @@ bool FocusDelegate::HandlePlatformMessage( next_focus_request_ = std::move(response); } } else if (method->value == "View.focus.request") { - return RequestFocus(std::move(request), std::move(response)); + auto args_it = request.FindMember("args"); + if (args_it == request.MemberEnd() || !args_it->value.IsObject()) { + FML_LOG(ERROR) << "No arguments found."; + return false; + } + const auto& args = args_it->value; + + auto view_ref = args.FindMember("viewRef"); + if (!view_ref->value.IsUint64()) { + FML_LOG(ERROR) << "Argument 'viewRef' is not a uint64"; + return false; + } + + zx_handle_t handle = view_ref->value.GetUint64(); + zx_handle_t out_handle; + zx_status_t status = + zx_handle_duplicate(handle, ZX_RIGHT_SAME_RIGHTS, &out_handle); + if (status != ZX_OK) { + FML_LOG(ERROR) << "Argument 'viewRef' is not valid"; + return false; + } + auto ref = fuchsia::ui::views::ViewRef({ + .reference = zx::eventpair(out_handle), + }); + return RequestFocusByViewRef(std::move(ref), std::move(response)); + + } else if (method->value == "View.focus.requestById") { + auto args_it = request.FindMember("args"); + if (args_it == request.MemberEnd() || !args_it->value.IsObject()) { + FML_LOG(ERROR) << "No arguments found."; + return false; + } + const auto& args = args_it->value; + + auto view_id = args.FindMember("viewId"); + if (!view_id->value.IsUint64()) { + FML_LOG(ERROR) << "Argument 'viewId' is not a uint64"; + return false; + } + + auto id = view_id->value.GetUint64(); + if (child_view_view_refs_.count(id) != 1) { + FML_LOG(ERROR) << "Argument 'viewId' (" << id + << ") does not refer to a valid ChildView"; + return false; + } + + return RequestFocusById(id, std::move(response)); } else { return false; } @@ -51,6 +98,12 @@ bool FocusDelegate::HandlePlatformMessage( return true; } +void FocusDelegate::OnChildViewViewRef(uint64_t view_id, + fuchsia::ui::views::ViewRef view_ref) { + FML_CHECK(child_view_view_refs_.count(view_id) == 0); + child_view_view_refs_[view_id] = std::move(view_ref); +} + void FocusDelegate::Complete( fml::RefPtr response, std::string value) { @@ -60,35 +113,24 @@ void FocusDelegate::Complete( } } -bool FocusDelegate::RequestFocus( - rapidjson::Value request, +bool FocusDelegate::RequestFocusById( + uint64_t view_id, fml::RefPtr response) { - auto args_it = request.FindMember("args"); - if (args_it == request.MemberEnd() || !args_it->value.IsObject()) { - FML_LOG(ERROR) << "No arguments found."; + fuchsia::ui::views::ViewRef ref; + auto status = child_view_view_refs_[view_id].Clone(&ref); + if (status != ZX_OK) { + FML_LOG(ERROR) << "Failed to clone ViewRef"; return false; } - const auto& args = args_it->value; - auto view_ref = args.FindMember("viewRef"); - if (!view_ref->value.IsUint64()) { - FML_LOG(ERROR) << "Argument 'viewRef' is not a uint64"; - return false; - } + return RequestFocusByViewRef(std::move(ref), std::move(response)); +} - zx_handle_t handle = view_ref->value.GetUint64(); - zx_handle_t out_handle; - zx_status_t status = - zx_handle_duplicate(handle, ZX_RIGHT_SAME_RIGHTS, &out_handle); - if (status != ZX_OK) { - FML_LOG(ERROR) << "Argument 'viewRef' is not valid"; - return false; - } - auto ref = fuchsia::ui::views::ViewRef({ - .reference = zx::eventpair(out_handle), - }); +bool FocusDelegate::RequestFocusByViewRef( + fuchsia::ui::views::ViewRef view_ref, + fml::RefPtr response) { focuser_->RequestFocus( - std::move(ref), + std::move(view_ref), [this, response = std::move(response)]( fuchsia::ui::views::Focuser_RequestFocus_Result result) { int result_code = diff --git a/shell/platform/fuchsia/flutter/focus_delegate.h b/shell/platform/fuchsia/flutter/focus_delegate.h index 2d58579789184..98793939fae1c 100644 --- a/shell/platform/fuchsia/flutter/focus_delegate.h +++ b/shell/platform/fuchsia/flutter/focus_delegate.h @@ -8,6 +8,8 @@ #include #include +#include + #include "flutter/fml/macros.h" #include "flutter/lib/ui/window/platform_message.h" #include "third_party/rapidjson/include/rapidjson/document.h" @@ -16,9 +18,8 @@ namespace flutter_runner { class FocusDelegate { public: - FocusDelegate(fidl::InterfaceHandle - view_ref_focused, - fidl::InterfaceHandle focuser) + FocusDelegate(fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused, + fuchsia::ui::views::FocuserHandle focuser) : view_ref_focused_(view_ref_focused.Bind()), focuser_(focuser.Bind()) {} /// Continuously watches the host viewRef for focus events, invoking a @@ -49,10 +50,17 @@ class FocusDelegate { rapidjson::Value request, fml::RefPtr response); + void OnChildViewViewRef(uint64_t view_id, + fuchsia::ui::views::ViewRef view_ref); + private: fuchsia::ui::views::ViewRefFocusedPtr view_ref_focused_; fuchsia::ui::views::FocuserPtr focuser_; + std::unordered_map + child_view_view_refs_; + std::function watch_loop_; bool is_focused_ = false; fml::RefPtr next_focus_request_; @@ -61,9 +69,12 @@ class FocusDelegate { std::string value); /// Completes a platform message request by attempting to give focus for a - /// given viewRef. - bool RequestFocus(rapidjson::Value request, - fml::RefPtr response); + /// given view. + bool RequestFocusById(uint64_t view_id, + fml::RefPtr response); + bool RequestFocusByViewRef( + fuchsia::ui::views::ViewRef view_ref, + fml::RefPtr response); FML_DISALLOW_COPY_AND_ASSIGN(FocusDelegate); }; diff --git a/shell/platform/fuchsia/flutter/gfx_platform_view.cc b/shell/platform/fuchsia/flutter/gfx_platform_view.cc index 26cc8d228f883..305e2f6fa523f 100644 --- a/shell/platform/fuchsia/flutter/gfx_platform_view.cc +++ b/shell/platform/fuchsia/flutter/gfx_platform_view.cc @@ -13,11 +13,12 @@ GfxPlatformView::GfxPlatformView( flutter::TaskRunners task_runners, fuchsia::ui::views::ViewRef view_ref, std::shared_ptr external_view_embedder, - fidl::InterfaceHandle ime_service, - fidl::InterfaceHandle keyboard, - fidl::InterfaceHandle touch_source, - fidl::InterfaceHandle focuser, - fidl::InterfaceHandle view_ref_focused, + fuchsia::ui::input::ImeServiceHandle ime_service, + fuchsia::ui::input3::KeyboardHandle keyboard, + fuchsia::ui::pointer::TouchSourceHandle touch_source, + fuchsia::ui::pointer::MouseSourceHandle mouse_source, + fuchsia::ui::views::FocuserHandle focuser, + fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused, fidl::InterfaceRequest session_listener_request, fit::closure on_session_listener_error_callback, @@ -39,6 +40,7 @@ GfxPlatformView::GfxPlatformView( std::move(ime_service), std::move(keyboard), std::move(touch_source), + std::move(mouse_source), std::move(focuser), std::move(view_ref_focused), std::move(wireframe_enabled_callback), diff --git a/shell/platform/fuchsia/flutter/gfx_platform_view.h b/shell/platform/fuchsia/flutter/gfx_platform_view.h index c3aacd73ae960..4ecc1debe1193 100644 --- a/shell/platform/fuchsia/flutter/gfx_platform_view.h +++ b/shell/platform/fuchsia/flutter/gfx_platform_view.h @@ -30,12 +30,12 @@ class GfxPlatformView final : public flutter_runner::PlatformView, flutter::TaskRunners task_runners, fuchsia::ui::views::ViewRef view_ref, std::shared_ptr external_view_embedder, - fidl::InterfaceHandle ime_service, - fidl::InterfaceHandle keyboard, - fidl::InterfaceHandle touch_source, - fidl::InterfaceHandle focuser, - fidl::InterfaceHandle - view_ref_focused, + fuchsia::ui::input::ImeServiceHandle ime_service, + fuchsia::ui::input3::KeyboardHandle keyboard, + fuchsia::ui::pointer::TouchSourceHandle touch_source, + fuchsia::ui::pointer::MouseSourceHandle mouse_source, + fuchsia::ui::views::FocuserHandle focuser, + fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused, fidl::InterfaceRequest session_listener_request, fit::closure on_session_listener_error_callback, diff --git a/shell/platform/fuchsia/flutter/gfx_session_connection.cc b/shell/platform/fuchsia/flutter/gfx_session_connection.cc index 7f84ff1bc57bc..530b07d8e7647 100644 --- a/shell/platform/fuchsia/flutter/gfx_session_connection.cc +++ b/shell/platform/fuchsia/flutter/gfx_session_connection.cc @@ -4,12 +4,11 @@ #include "gfx_session_connection.h" -#include -#include - #include +#include #include #include +#include #include "flutter/fml/make_copyable.h" #include "flutter/fml/time/time_point.h" @@ -187,7 +186,7 @@ fml::TimePoint GfxSessionConnection::SnapToNextPhase( GfxSessionConnection::GfxSessionConnection( std::string debug_label, inspect::Node inspect_node, - fidl::InterfaceHandle session, + fuchsia::ui::scenic::SessionHandle session, fml::closure session_error_callback, on_frame_presented_event on_frame_presented_callback, uint64_t max_frames_in_flight, @@ -222,8 +221,11 @@ GfxSessionConnection::GfxSessionConnection( next_presentation_info_.set_presentation_time(0); - session_wrapper_.set_error_handler( - [callback = session_error_callback](zx_status_t status) { callback(); }); + session_wrapper_.set_error_handler([callback = session_error_callback]( + zx_status_t status) { + FML_LOG(ERROR) << "scenic::Session error: " << zx_status_get_string(status); + callback(); + }); // Set the |fuchsia::ui::scenic::OnFramePresented()| event handler that will // fire every time a set of one or more frames is presented. diff --git a/shell/platform/fuchsia/flutter/gfx_session_connection.h b/shell/platform/fuchsia/flutter/gfx_session_connection.h index f31afaaec0d9c..27dc1e1018036 100644 --- a/shell/platform/fuchsia/flutter/gfx_session_connection.h +++ b/shell/platform/fuchsia/flutter/gfx_session_connection.h @@ -74,14 +74,13 @@ class GfxSessionConnection final { fuchsia::scenic::scheduling::FuturePresentationTimes future_info, fuchsia::scenic::scheduling::PresentationInfo& presentation_info); - GfxSessionConnection( - std::string debug_label, - inspect::Node inspect_node, - fidl::InterfaceHandle session, - fml::closure session_error_callback, - on_frame_presented_event on_frame_presented_callback, - uint64_t max_frames_in_flight, - fml::TimeDelta vsync_offset); + GfxSessionConnection(std::string debug_label, + inspect::Node inspect_node, + fuchsia::ui::scenic::SessionHandle session, + fml::closure session_error_callback, + on_frame_presented_event on_frame_presented_callback, + uint64_t max_frames_in_flight, + fml::TimeDelta vsync_offset); ~GfxSessionConnection(); diff --git a/shell/platform/fuchsia/flutter/integration_flutter_tests/BUILD.gn b/shell/platform/fuchsia/flutter/integration_flutter_tests/BUILD.gn index 3c7142cfa3084..6e5e43e0b05ea 100644 --- a/shell/platform/fuchsia/flutter/integration_flutter_tests/BUILD.gn +++ b/shell/platform/fuchsia/flutter/integration_flutter_tests/BUILD.gn @@ -4,6 +4,5 @@ group("integration_flutter_tests") { testonly = true - # TODO(fxbug.dev/86055): re-enable - # deps = [ "embedder:tests" ] + deps = [ "embedder:tests" ] } diff --git a/shell/platform/fuchsia/flutter/integration_flutter_tests/embedder/flutter-embedder-test2.cc b/shell/platform/fuchsia/flutter/integration_flutter_tests/embedder/flutter-embedder-test2.cc index acacd314a90e6..6e7fe2c7b2e74 100644 --- a/shell/platform/fuchsia/flutter/integration_flutter_tests/embedder/flutter-embedder-test2.cc +++ b/shell/platform/fuchsia/flutter/integration_flutter_tests/embedder/flutter-embedder-test2.cc @@ -62,8 +62,6 @@ const std::vector> GetInjectedServices() { std::vector> injected_services = {{ {"fuchsia.accessibility.semantics.SemanticsManager", "fuchsia-pkg://fuchsia.com/a11y-manager#meta/a11y-manager.cmx"}, - {"fuchsia.deprecatedtimezone.Timezone", - "fuchsia-pkg://fuchsia.com/timezone#meta/timezone.cmx"}, {"fuchsia.fonts.Provider", "fuchsia-pkg://fuchsia.com/fonts#meta/fonts.cmx"}, {"fuchsia.hardware.display.Provider", diff --git a/shell/platform/fuchsia/flutter/integration_flutter_tests/fuchsia_testing/src/lib/ui/base_view/embedded_view_utils.h b/shell/platform/fuchsia/flutter/integration_flutter_tests/fuchsia_testing/src/lib/ui/base_view/embedded_view_utils.h index 716bfa6606c36..ff087d4a03c6e 100644 --- a/shell/platform/fuchsia/flutter/integration_flutter_tests/fuchsia_testing/src/lib/ui/base_view/embedded_view_utils.h +++ b/shell/platform/fuchsia/flutter/integration_flutter_tests/fuchsia_testing/src/lib/ui/base_view/embedded_view_utils.h @@ -47,7 +47,7 @@ struct EmbeddedViewInfo { // returned EmbeddedViewInfo, which the caller can use to embed the child. // For example, an interface to a ViewProvider is obtained, a pair of // zx::eventpairs is created, CreateView is called, etc. This encapsulates -// the boilerplate the the client would otherwise write themselves. +// the boilerplate the client would otherwise write themselves. EmbeddedViewInfo LaunchComponentAndCreateView( const fuchsia::sys::LauncherPtr& launcher, const std::string& component_url, diff --git a/shell/platform/fuchsia/flutter/isolate_configurator.cc b/shell/platform/fuchsia/flutter/isolate_configurator.cc index ceb2a85a3d4b5..8b8b8201e98b0 100644 --- a/shell/platform/fuchsia/flutter/isolate_configurator.cc +++ b/shell/platform/fuchsia/flutter/isolate_configurator.cc @@ -15,7 +15,7 @@ namespace flutter_runner { IsolateConfigurator::IsolateConfigurator( UniqueFDIONS fdio_ns, - fidl::InterfaceHandle environment, + fuchsia::sys::EnvironmentHandle environment, zx::channel directory_request, zx::eventpair view_ref) : fdio_ns_(std::move(fdio_ns)), diff --git a/shell/platform/fuchsia/flutter/isolate_configurator.h b/shell/platform/fuchsia/flutter/isolate_configurator.h index 57634ecf97757..f38bed4f01b43 100644 --- a/shell/platform/fuchsia/flutter/isolate_configurator.h +++ b/shell/platform/fuchsia/flutter/isolate_configurator.h @@ -18,11 +18,10 @@ namespace flutter_runner { // the root isolate. class IsolateConfigurator final { public: - IsolateConfigurator( - UniqueFDIONS fdio_ns, - fidl::InterfaceHandle environment, - zx::channel directory_request, - zx::eventpair view_ref); + IsolateConfigurator(UniqueFDIONS fdio_ns, + fuchsia::sys::EnvironmentHandle environment, + zx::channel directory_request, + zx::eventpair view_ref); ~IsolateConfigurator(); @@ -33,7 +32,7 @@ class IsolateConfigurator final { private: bool used_ = false; UniqueFDIONS fdio_ns_; - fidl::InterfaceHandle environment_; + fuchsia::sys::EnvironmentHandle environment_; zx::channel directory_request_; zx::eventpair view_ref_; diff --git a/shell/platform/fuchsia/flutter/meta/common.shard.cml b/shell/platform/fuchsia/flutter/meta/common.shard.cml index dc240f45e82a3..c4ade5c3b128c 100644 --- a/shell/platform/fuchsia/flutter/meta/common.shard.cml +++ b/shell/platform/fuchsia/flutter/meta/common.shard.cml @@ -31,9 +31,7 @@ }, { protocol: [ - "fuchsia.accessibility.SettingsManager", "fuchsia.accessibility.semantics.SemanticsManager", - "fuchsia.deprecatedtimezone.Timezone", "fuchsia.device.NameProvider", "fuchsia.feedback.CrashReporter", "fuchsia.fonts.Provider", @@ -46,6 +44,9 @@ "fuchsia.tracing.provider.Registry", // Copied from vulkan/client.shard.cml. "fuchsia.ui.composition.Allocator", "fuchsia.ui.composition.Flatland", + "fuchsia.ui.input.ImeService", + "fuchsia.ui.input3.Keyboard", + "fuchsia.ui.scenic.Scenic", "fuchsia.vulkan.loader.Loader" // Copied from vulkan/client.shard.cml. ] } diff --git a/shell/platform/fuchsia/flutter/platform_view.cc b/shell/platform/fuchsia/flutter/platform_view.cc index 1871ecc284226..8f8990ff96e8e 100644 --- a/shell/platform/fuchsia/flutter/platform_view.cc +++ b/shell/platform/fuchsia/flutter/platform_view.cc @@ -55,11 +55,12 @@ PlatformView::PlatformView( flutter::TaskRunners task_runners, fuchsia::ui::views::ViewRef view_ref, std::shared_ptr external_view_embedder, - fidl::InterfaceHandle ime_service, - fidl::InterfaceHandle keyboard, - fidl::InterfaceHandle touch_source, - fidl::InterfaceHandle focuser, - fidl::InterfaceHandle view_ref_focused, + fuchsia::ui::input::ImeServiceHandle ime_service, + fuchsia::ui::input3::KeyboardHandle keyboard, + fuchsia::ui::pointer::TouchSourceHandle touch_source, + fuchsia::ui::pointer::MouseSourceHandle mouse_source, + fuchsia::ui::views::FocuserHandle focuser, + fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused, OnEnableWireframe wireframe_enabled_callback, OnUpdateView on_update_view_callback, OnCreateSurface on_create_surface_callback, @@ -75,7 +76,8 @@ PlatformView::PlatformView( std::make_shared(std::move(view_ref_focused), std::move(focuser))), pointer_delegate_( - std::make_shared(std::move(touch_source))), + std::make_shared(std::move(touch_source), + std::move(mouse_source))), ime_client_(this), text_sync_service_(ime_service.Bind()), keyboard_listener_binding_(this), diff --git a/shell/platform/fuchsia/flutter/platform_view.h b/shell/platform/fuchsia/flutter/platform_view.h index c11caedad2c9f..cdf502291ed1a 100644 --- a/shell/platform/fuchsia/flutter/platform_view.h +++ b/shell/platform/fuchsia/flutter/platform_view.h @@ -68,12 +68,12 @@ class PlatformView : public flutter::PlatformView, flutter::TaskRunners task_runners, fuchsia::ui::views::ViewRef view_ref, std::shared_ptr external_view_embedder, - fidl::InterfaceHandle ime_service, - fidl::InterfaceHandle keyboard, - fidl::InterfaceHandle touch_source, - fidl::InterfaceHandle focuser, - fidl::InterfaceHandle - view_ref_focused, + fuchsia::ui::input::ImeServiceHandle ime_service, + fuchsia::ui::input3::KeyboardHandle keyboard, + fuchsia::ui::pointer::TouchSourceHandle touch_source, + fuchsia::ui::pointer::MouseSourceHandle mouse_source, + fuchsia::ui::views::FocuserHandle focuser, + fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused, OnEnableWireframe wireframe_enabled_callback, OnUpdateView on_update_view_callback, OnCreateSurface on_create_surface_callback, diff --git a/shell/platform/fuchsia/flutter/platform_view_unittest.cc b/shell/platform/fuchsia/flutter/platform_view_unittest.cc index e69a409b8e6f2..54d8941789569 100644 --- a/shell/platform/fuchsia/flutter/platform_view_unittest.cc +++ b/shell/platform/fuchsia/flutter/platform_view_unittest.cc @@ -102,10 +102,6 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { pointer_packets_.push_back(std::move(packet)); } // |flutter::PlatformView::Delegate| - void OnPlatformViewDispatchKeyDataPacket( - std::unique_ptr packet, - std::function callback) {} - // |flutter::PlatformView::Delegate| void OnPlatformViewDispatchSemanticsAction(int32_t id, flutter::SemanticsAction action, fml::MallocMapping args) {} @@ -194,10 +190,9 @@ class MockKeyboard : public fuchsia::ui::input3::testing::Keyboard_TestBase { : keyboard_(this, std::move(keyboard)) {} ~MockKeyboard() = default; - void AddListener( - fuchsia::ui::views::ViewRef view_ref, - fidl::InterfaceHandle listener, - AddListenerCallback callback) override { + void AddListener(fuchsia::ui::views::ViewRef view_ref, + fuchsia::ui::input3::KeyboardListenerHandle listener, + AddListenerCallback callback) override { FML_CHECK(!listener_.is_bound()); listener_ = listener.Bind(); @@ -235,32 +230,36 @@ class PlatformViewBuilder { } PlatformViewBuilder& SetImeService( - fidl::InterfaceHandle ime_service) { + fuchsia::ui::input::ImeServiceHandle ime_service) { ime_service_ = std::move(ime_service); return *this; } PlatformViewBuilder& SetKeyboard( - fidl::InterfaceHandle keyboard) { + fuchsia::ui::input3::KeyboardHandle keyboard) { keyboard_ = std::move(keyboard); return *this; } PlatformViewBuilder& SetTouchSource( - fidl::InterfaceHandle touch_source) { + fuchsia::ui::pointer::TouchSourceHandle touch_source) { touch_source_ = std::move(touch_source); return *this; } - PlatformViewBuilder& SetFocuser( - fidl::InterfaceHandle focuser) { + PlatformViewBuilder& SetMouseSource( + fuchsia::ui::pointer::MouseSourceHandle mouse_source) { + mouse_source_ = std::move(mouse_source); + return *this; + } + + PlatformViewBuilder& SetFocuser(fuchsia::ui::views::FocuserHandle focuser) { focuser_ = std::move(focuser); return *this; } PlatformViewBuilder& SetViewRefFocused( - fidl::InterfaceHandle - view_ref_focused) { + fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused) { view_ref_focused_ = std::move(view_ref_focused); return *this; } @@ -308,7 +307,8 @@ class PlatformViewBuilder { return GfxPlatformView( delegate_, task_runners_, std::move(view_ref_pair_.view_ref), external_external_view_embedder_, std::move(ime_service_), - std::move(keyboard_), std::move(touch_source_), std::move(focuser_), + std::move(keyboard_), std::move(touch_source_), + std::move(mouse_source_), std::move(focuser_), std::move(view_ref_focused_), std::move(session_listener_request_), std::move(on_session_listener_error_callback_), std::move(wireframe_enabled_callback_), @@ -330,11 +330,12 @@ class PlatformViewBuilder { std::shared_ptr external_external_view_embedder_; - fidl::InterfaceHandle ime_service_; - fidl::InterfaceHandle keyboard_; - fidl::InterfaceHandle touch_source_; - fidl::InterfaceHandle view_ref_focused_; - fidl::InterfaceHandle focuser_; + fuchsia::ui::input::ImeServiceHandle ime_service_; + fuchsia::ui::input3::KeyboardHandle keyboard_; + fuchsia::ui::pointer::TouchSourceHandle touch_source_; + fuchsia::ui::pointer::MouseSourceHandle mouse_source_; + fuchsia::ui::views::ViewRefFocusedHandle view_ref_focused_; + fuchsia::ui::views::FocuserHandle focuser_; fidl::InterfaceRequest session_listener_request_; fit::closure on_session_listener_error_callback_; @@ -1234,7 +1235,7 @@ TEST_F(PlatformViewTests, OnKeyEvent) { flutter::TaskRunners task_runners = flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); - fidl::InterfaceHandle keyboard_service; + fuchsia::ui::input3::KeyboardHandle keyboard_service; MockKeyboard keyboard(keyboard_service.NewRequest()); flutter_runner::GfxPlatformView platform_view = diff --git a/shell/platform/fuchsia/flutter/pointer_delegate.cc b/shell/platform/fuchsia/flutter/pointer_delegate.cc index 72b2789bac8b4..d28fbb927e31e 100644 --- a/shell/platform/fuchsia/flutter/pointer_delegate.cc +++ b/shell/platform/fuchsia/flutter/pointer_delegate.cc @@ -11,6 +11,8 @@ #include "flutter/fml/logging.h" #include "flutter/fml/trace_event.h" +// TODO(fxbug.dev/87076): Add MouseSource tests. + namespace fuchsia::ui::pointer { // For using TouchInteractionId as a map key. bool operator==(const fuchsia::ui::pointer::TouchInteractionId& a, @@ -23,6 +25,8 @@ bool operator==(const fuchsia::ui::pointer::TouchInteractionId& a, namespace flutter_runner { using fup_EventPhase = fuchsia::ui::pointer::EventPhase; +using fup_MouseDeviceInfo = fuchsia::ui::pointer::MouseDeviceInfo; +using fup_MouseEvent = fuchsia::ui::pointer::MouseEvent; using fup_TouchEvent = fuchsia::ui::pointer::TouchEvent; using fup_TouchIxnStatus = fuchsia::ui::pointer::TouchInteractionStatus; using fup_TouchResponse = fuchsia::ui::pointer::TouchResponse; @@ -30,13 +34,17 @@ using fup_TouchResponseType = fuchsia::ui::pointer::TouchResponseType; using fup_ViewParameters = fuchsia::ui::pointer::ViewParameters; namespace { -void MaybeIssueTraceEvent(const fup_TouchEvent& event) { - if (event.has_trace_flow_id()) { - TRACE_FLOW_END("input", "dispatch_event_to_client", event.trace_flow_id()); - } +void IssueTouchTraceEvent(const fup_TouchEvent& event) { + FML_DCHECK(event.has_trace_flow_id()) << "API guarantee"; + TRACE_FLOW_END("input", "dispatch_event_to_client", event.trace_flow_id()); } -bool HasValidatedPointerSample(const fup_TouchEvent& event) { +void IssueMouseTraceEvent(const fup_MouseEvent& event) { + FML_DCHECK(event.has_trace_flow_id()) << "API guarantee"; + TRACE_FLOW_END("input", "dispatch_event_to_client", event.trace_flow_id()); +} + +bool HasValidatedTouchSample(const fup_TouchEvent& event) { if (!event.has_pointer_sample()) { return false; } @@ -47,6 +55,20 @@ bool HasValidatedPointerSample(const fup_TouchEvent& event) { return true; } +bool HasValidatedMouseSample(const fup_MouseEvent& event) { + if (!event.has_pointer_sample()) { + return false; + } + const auto& sample = event.pointer_sample(); + FML_DCHECK(sample.has_device_id()) << "API guarantee"; + FML_DCHECK(sample.has_position_in_viewport()) << "API guarantee"; + FML_DCHECK(!sample.has_pressed_buttons() || + sample.pressed_buttons().size() > 0) + << "API guarantee"; + + return true; +} + std::array ViewportToViewCoordinates( std::array viewport_coordinates, const std::array& viewport_to_view_transform) { @@ -73,7 +95,7 @@ std::array ViewportToViewCoordinates( } } -flutter::PointerData::Change GetChangeFromPointerEventPhase( +flutter::PointerData::Change GetChangeFromTouchEventPhase( fup_EventPhase phase) { switch (phase) { case fup_EventPhase::ADD: @@ -111,29 +133,62 @@ std::array ClampToViewSpace(const float x, return {clamped_x, clamped_y}; } +flutter::PointerData::Change ComputePhase( + bool any_button_down, + std::unordered_set& mouse_down, + uint32_t id) { + if (!mouse_down.count(id) && !any_button_down) { + return flutter::PointerData::Change::kHover; + } else if (!mouse_down.count(id) && any_button_down) { + mouse_down.insert(id); + return flutter::PointerData::Change::kDown; + } else if (mouse_down.count(id) && any_button_down) { + return flutter::PointerData::Change::kMove; + } else if (mouse_down.count(id) && !any_button_down) { + mouse_down.erase(id); + return flutter::PointerData::Change::kUp; + } + + FML_UNREACHABLE(); + return flutter::PointerData::Change::kCancel; +} + +// Flutter's PointerData.device field is 64 bits and is expected to be unique +// for each pointer. We pack Fuchsia's device ID (hi) and pointer ID (lo) into +// 64 bits to retain uniqueness across multiple touch devices. +uint64_t PackFuchsiaDeviceIdAndPointerId(uint32_t fuchsia_device_id, + uint32_t fuchsia_pointer_id) { + return (((uint64_t)fuchsia_device_id) << 32) | fuchsia_pointer_id; +} + // It returns a "draft" because the coordinates are logical. Later, view pixel // ratio is applied to obtain physical coordinates. // // The flutter pointerdata state machine has extra phases, which this function // synthesizes on the fly. Hence the return data is a flutter pointerdata, and -// optionally a second synthesized one. +// optionally a second one. // For example: , , . +// TODO(fxbug.dev/87074): Let PointerDataPacketConverter synthesize events. // // Flutter gestures expect a gesture to start within the logical view space, and // is not tolerant of floating point drift. This function coerces just the DOWN // event's coordinate to start within the logical view. std::pair> -CreatePointerDraft(const fup_TouchEvent& event, - const fup_ViewParameters& view_parameters) { - FML_DCHECK(HasValidatedPointerSample(event)) << "precondition"; +CreateTouchDraft(const fup_TouchEvent& event, + const fup_ViewParameters& view_parameters) { + FML_DCHECK(HasValidatedTouchSample(event)) << "precondition"; const auto& sample = event.pointer_sample(); + const auto& ixn = sample.interaction(); + flutter::PointerData ptr; ptr.Clear(); ptr.time_stamp = event.timestamp() / 1000; // in microseconds - ptr.change = GetChangeFromPointerEventPhase(sample.phase()); + ptr.change = GetChangeFromTouchEventPhase(sample.phase()); ptr.kind = flutter::PointerData::DeviceKind::kTouch; - ptr.device = sample.interaction().device_id; - ptr.pointer_identifier = sample.interaction().pointer_id; + // Load Fuchsia's pointer ID onto Flutter's |device| field, and not the + // |pointer_identifier| field. The latter is written by + // PointerDataPacketConverter, to track individual gesture interactions. + ptr.device = PackFuchsiaDeviceIdAndPointerId(ixn.device_id, ixn.pointer_id); // View parameters can change mid-interaction; apply transform on the fly. auto logical = ViewportToViewCoordinates(sample.position_in_viewport(), @@ -146,7 +201,7 @@ CreatePointerDraft(const fup_TouchEvent& event, flutter::PointerData down; memcpy(&down, &ptr, sizeof(flutter::PointerData)); down.change = flutter::PointerData::Change::kDown; - { // DOWN event needs to start in the logical view space. + { // Ensure gesture recognition: DOWN starts in the logical view space. auto [x, y] = ClampToViewSpace(down.physical_x, down.physical_y, view_parameters); down.physical_x = x; @@ -163,6 +218,95 @@ CreatePointerDraft(const fup_TouchEvent& event, } } +// It returns a "draft" because the coordinates are logical. Later, view pixel +// ratio is applied to obtain physical coordinates. +// +// Phase data is computed before this call; it involves state tracking based on +// button-down state. +// +// Button data, if available, gets packed into the |buttons| field, in flutter +// button order (kMousePrimaryButton, etc). The device-assigned button IDs are +// provided in priority order in MouseEvent.device_info (at the start of channel +// connection), and maps from device button ID (given in fup_MouseEvent) to +// flutter button ID (flutter::PointerData). +// +// Scroll data, if available, gets packed into the |scroll_delta_x| or +// |scroll_delta_y| fields, and the |signal_kind| field is set to kScroll. +// The PointerDataPacketConverter reads this field to synthesize events to match +// Flutter's expected pointer stream. +// TODO(fxbug.dev/87073): PointerDataPacketConverter should synthesize a +// discrete scroll event on kDown or kUp, to match engine expectations. +// +// Flutter gestures expect a gesture to start within the logical view space, and +// is not tolerant of floating point drift. This function coerces just the DOWN +// event's coordinate to start within the logical view. +flutter::PointerData CreateMouseDraft(const fup_MouseEvent& event, + const flutter::PointerData::Change phase, + const fup_ViewParameters& view_parameters, + const fup_MouseDeviceInfo& device_info) { + FML_DCHECK(HasValidatedMouseSample(event)) << "precondition"; + const auto& sample = event.pointer_sample(); + + flutter::PointerData ptr; + ptr.Clear(); + ptr.time_stamp = event.timestamp() / 1000; // in microseconds + ptr.change = phase; + ptr.kind = flutter::PointerData::DeviceKind::kMouse; + ptr.device = sample.device_id(); + + // View parameters can change mid-interaction; apply transform on the fly. + auto logical = + ViewportToViewCoordinates(sample.position_in_viewport(), + view_parameters.viewport_to_view_transform); + ptr.physical_x = logical[0]; // Not yet physical; adjusted in PlatformView. + ptr.physical_y = logical[1]; // Not yet physical; adjusted in PlatformView. + + // Ensure gesture recognition: DOWN starts in the logical view space. + if (ptr.change == flutter::PointerData::Change::kDown) { + auto [x, y] = + ClampToViewSpace(ptr.physical_x, ptr.physical_y, view_parameters); + ptr.physical_x = x; + ptr.physical_y = y; + } + + if (sample.has_pressed_buttons()) { + int64_t flutter_buttons = 0; + const auto& pressed = sample.pressed_buttons(); + for (size_t idx = 0; idx < pressed.size(); ++idx) { + const uint8_t button_id = pressed[idx]; + FML_DCHECK(device_info.has_buttons()) << "API guarantee"; + // Priority 0 maps to kPrimaryButton, and so on. + for (uint8_t prio = 0; prio < device_info.buttons().size(); ++prio) { + if (button_id == device_info.buttons()[prio]) { + flutter_buttons |= (1 << prio); + } + } + } + FML_DCHECK(flutter_buttons != 0); + ptr.buttons = flutter_buttons; + } + + // Fuchsia currently provides scroll data in "ticks", not physical pixels. + // However, Flutter expects scroll data in physical pixels. To compensate for + // lack of guidance, we make up a "reasonable amount". + // TODO(fxbug.dev/85388): Replace with physical pixel scroll. + const int kScrollOffsetMultiplier = 20; + + if (sample.has_scroll_v()) { + ptr.signal_kind = flutter::PointerData::SignalKind::kScroll; + double dy = -sample.scroll_v() * kScrollOffsetMultiplier; // logical amount + ptr.scroll_delta_y = dy; // Not yet physical; adjusted in Platform View. + } + + if (sample.has_scroll_h()) { + ptr.signal_kind = flutter::PointerData::SignalKind::kScroll; + double dx = sample.scroll_h() * kScrollOffsetMultiplier; // logical amount + ptr.scroll_delta_x = dx; // Not yet physical; adjusted in Platform View. + } + + return ptr; +} + // Helper to insert one or two events into a vector buffer. void InsertIntoBuffer( std::pair> events, @@ -176,37 +320,38 @@ void InsertIntoBuffer( } // namespace // Core logic of this class. +// Aim to keep state management in this function. void PointerDelegate::WatchLoop( std::function)> callback) { FML_LOG(INFO) << "Flutter - PointerDelegate started."; - if (watch_loop_) { + if (touch_responder_) { FML_LOG(ERROR) << "PointerDelegate::WatchLoop() must be called once."; return; } - watch_loop_ = [this, callback](std::vector events) { + touch_responder_ = [this, callback](std::vector events) { TRACE_EVENT0("flutter", "PointerDelegate::TouchHandler"); - FML_DCHECK(responses_.empty()) << "precondition"; + FML_DCHECK(touch_responses_.empty()) << "precondition"; std::vector to_client; for (const fup_TouchEvent& event : events) { - MaybeIssueTraceEvent(event); + IssueTouchTraceEvent(event); fup_TouchResponse response; // Response per event, matched on event's index. if (event.has_view_parameters()) { - view_parameters_ = std::move(event.view_parameters()); + touch_view_parameters_ = std::move(event.view_parameters()); } - if (HasValidatedPointerSample(event)) { + if (HasValidatedTouchSample(event)) { const auto& sample = event.pointer_sample(); const auto& ixn = sample.interaction(); if (sample.phase() == fup_EventPhase::ADD && !event.has_interaction_result()) { - buffer_.emplace(ixn, std::vector()); + touch_buffer_.emplace(ixn, std::vector()); } - FML_DCHECK(view_parameters_.has_value()) << "API guarantee"; - auto events = CreatePointerDraft(event, view_parameters_.value()); - if (buffer_.count(ixn) > 0) { - InsertIntoBuffer(std::move(events), &buffer_[ixn]); + FML_DCHECK(touch_view_parameters_.has_value()) << "API guarantee"; + auto events = CreateTouchDraft(event, touch_view_parameters_.value()); + if (touch_buffer_.count(ixn) > 0) { + InsertIntoBuffer(std::move(events), &touch_buffer_[ixn]); } else { InsertIntoBuffer(std::move(events), &to_client); } @@ -217,23 +362,56 @@ void PointerDelegate::WatchLoop( const auto& result = event.interaction_result(); const auto& ixn = result.interaction; if (result.status == fup_TouchIxnStatus::GRANTED && - buffer_.count(ixn) > 0) { + touch_buffer_.count(ixn) > 0) { FML_DCHECK(to_client.empty()) << "invariant"; - to_client.insert(to_client.end(), buffer_[ixn].begin(), - buffer_[ixn].end()); + to_client.insert(to_client.end(), touch_buffer_[ixn].begin(), + touch_buffer_[ixn].end()); } - buffer_.erase(ixn); // Result seen, delete the buffer. + touch_buffer_.erase(ixn); // Result seen, delete the buffer. } - responses_.push_back(std::move(response)); + touch_responses_.push_back(std::move(response)); } callback(std::move(to_client)); // Notify client of touch events, if any. - touch_source_->Watch(std::move(responses_), /*copy*/ watch_loop_); - responses_.clear(); + touch_source_->Watch(std::move(touch_responses_), + /*copy*/ touch_responder_); + touch_responses_.clear(); + }; + + mouse_responder_ = [this, callback](std::vector events) { + TRACE_EVENT0("flutter", "PointerDelegate::MouseHandler"); + std::vector to_client; + for (fup_MouseEvent& event : events) { + IssueMouseTraceEvent(event); + if (event.has_device_info()) { + const auto& id = event.device_info().id(); + mouse_device_info_[id] = std::move(*event.mutable_device_info()); + } + if (event.has_view_parameters()) { + mouse_view_parameters_ = std::move(event.view_parameters()); + } + if (HasValidatedMouseSample(event)) { + const auto& sample = event.pointer_sample(); + const auto& id = sample.device_id(); + const bool any_button_down = sample.has_pressed_buttons(); + FML_DCHECK(mouse_view_parameters_.has_value()) << "API guarantee"; + FML_DCHECK(mouse_device_info_.count(id) > 0) << "API guarantee"; + + const auto phase = ComputePhase(any_button_down, mouse_down_, id); + flutter::PointerData data = + CreateMouseDraft(event, phase, mouse_view_parameters_.value(), + mouse_device_info_[id]); + to_client.emplace_back(std::move(data)); + } + } + callback(std::move(to_client)); + mouse_source_->Watch(/*copy*/ mouse_responder_); }; - touch_source_->Watch(std::move(responses_), /*copy*/ watch_loop_); - responses_.clear(); + // Start watching both channels. + touch_source_->Watch(std::move(touch_responses_), /*copy*/ touch_responder_); + touch_responses_.clear(); + mouse_source_->Watch(/*copy*/ mouse_responder_); } } // namespace flutter_runner diff --git a/shell/platform/fuchsia/flutter/pointer_delegate.h b/shell/platform/fuchsia/flutter/pointer_delegate.h index 6de02e8032083..d8c592a0a7627 100644 --- a/shell/platform/fuchsia/flutter/pointer_delegate.h +++ b/shell/platform/fuchsia/flutter/pointer_delegate.h @@ -7,9 +7,11 @@ #include +#include #include #include #include +#include #include #include "flutter/lib/ui/window/pointer_data.h" @@ -26,28 +28,34 @@ struct IxnHasher { } }; -// Channel processor for fuchsia.ui.pointer.TouchSource protocol. It manages the -// channel state, collects touch events, and surfaces them to PlatformView as -// flutter::PointerData events for further processing and dispatch. +// Channel processors for fuchsia.ui.pointer.TouchSource and MouseSource +// protocols. It manages the channel state, collects touch and mouse events, and +// surfaces them to PlatformView as flutter::PointerData events for further +// processing and dispatch. class PointerDelegate { public: - PointerDelegate( - fidl::InterfaceHandle touch_source) - : touch_source_(touch_source.Bind()) {} + PointerDelegate(fuchsia::ui::pointer::TouchSourceHandle touch_source, + fuchsia::ui::pointer::MouseSourceHandle mouse_source) + : touch_source_(touch_source.Bind()), + mouse_source_(mouse_source.Bind()) {} - // Each TouchEvent must carry a TouchPointerSample, and the supplied callback - // will translate each to a flutter::PointerData, and the vector of - // PointerData placed in a PointerDataPacket for transport to the Engine. + // This function collects Fuchsia's TouchPointerSample and MousePointerSample + // data and transforms them into flutter::PointerData structs. It then calls + // the supplied callback with a vector of flutter::PointerData, which (1) does + // last processing (applies metrics), and (2) packs these flutter::PointerData + // in a flutter::PointerDataPacket for transport to the Engine. void WatchLoop( std::function)> callback); private: + /***** TOUCH STATE *****/ + // Channel for touch events from Scenic. fuchsia::ui::pointer::TouchSourcePtr touch_source_; // Receive touch events from Scenic. Must be copyable. std::function)> - watch_loop_; + touch_responder_; // Per-interaction buffer of touch events from Scenic. When an interaction // starts with event.pointer_sample.phase == ADD, we allocate a buffer and @@ -85,20 +93,52 @@ class PointerDelegate { std::unordered_map, IxnHasher> - buffer_; + touch_buffer_; // The fuchsia.ui.pointer.TouchSource protocol allows one in-flight // hanging-get Watch() call to gather touch events, and the client is expected // to respond with consumption intent on the following hanging-get Watch() // call. Store responses here for the next call. - std::vector responses_; + std::vector touch_responses_; + + // The fuchsia.ui.pointer.TouchSource protocol issues channel-global view + // parameters on connection and on change. Events must apply these view + // parameters to correctly map to logical view coordinates. The "nullopt" + // state represents the absence of view parameters, early in the protocol + // lifecycle. + std::optional touch_view_parameters_; + + /***** MOUSE STATE *****/ + + // Channel for mouse events from Scenic. + fuchsia::ui::pointer::MouseSourcePtr mouse_source_; + + // Receive mouse events from Scenic. Must be copyable. + std::function)> + mouse_responder_; + + // The set of mouse devices that are currently interacting with the UI. + // A mouse is considered flutter::PointerData::Change::kDown if any button is + // pressed. This set is used to correctly set the phase in + // flutter::PointerData.change, with this high-level algorithm: + // if !mouse_down[id] && !button then: change = kHover + // if !mouse_down[id] && button then: change = kDown; mouse_down.add(id) + // if mouse_down[id] && button then: change = kMove + // if mouse_down[id] && !button then: change = kUp; mouse_down.remove(id) + std::unordered_set mouse_down_; + + // For each mouse device, its device-specific information, such as mouse + // button priority order. + std::unordered_map + mouse_device_info_; - // The fuchsia.ui.pointer.TouchSource protocol issues a channel-global view + // The fuchsia.ui.pointer.MouseSource protocol issues channel-global view // parameters on connection and on change. Events must apply these view // parameters to correctly map to logical view coordinates. The "nullopt" // state represents the absence of view parameters, early in the protocol // lifecycle. - std::optional view_parameters_; + std::optional mouse_view_parameters_; }; } // namespace flutter_runner diff --git a/shell/platform/fuchsia/flutter/pointer_delegate_unittests.cc b/shell/platform/fuchsia/flutter/pointer_delegate_unittests.cc index daaa15e0a60cf..5a6bc9f533918 100644 --- a/shell/platform/fuchsia/flutter/pointer_delegate_unittests.cc +++ b/shell/platform/fuchsia/flutter/pointer_delegate_unittests.cc @@ -15,6 +15,7 @@ #include "flutter/fml/logging.h" #include "flutter/fml/macros.h" #include "pointer_delegate.h" +#include "tests/fakes/mouse_source.h" #include "tests/fakes/touch_source.h" #include "tests/pointer_event_utility.h" @@ -32,7 +33,7 @@ using fup_ViewParameters = fuchsia::ui::pointer::ViewParameters; constexpr std::array, 2> kRect = {{{0, 0}, {20, 20}}}; constexpr std::array kIdentity = {1, 0, 0, 0, 1, 0, 0, 0, 1}; -constexpr fup_TouchIxnId kIxnOne = {.device_id = 0u, +constexpr fup_TouchIxnId kIxnOne = {.device_id = 1u, .pointer_id = 1u, .interaction_id = 2u}; @@ -42,18 +43,22 @@ class PointerDelegateTest : public ::testing::Test { protected: PointerDelegateTest() : loop_(&kAsyncLoopConfigAttachToCurrentThread) { touch_source_ = std::make_unique(); + mouse_source_ = std::make_unique(); pointer_delegate_ = std::make_unique( - touch_source_bindings_.AddBinding(touch_source_.get())); + touch_source_bindings_.AddBinding(touch_source_.get()), + mouse_source_bindings_.AddBinding(mouse_source_.get())); } void RunLoopUntilIdle() { loop_.RunUntilIdle(); } std::unique_ptr touch_source_; + std::unique_ptr mouse_source_; std::unique_ptr pointer_delegate_; private: async::Loop loop_; fidl::BindingSet touch_source_bindings_; + fidl::BindingSet mouse_source_bindings_; FML_DISALLOW_COPY_AND_ASSIGN(PointerDelegateTest); }; @@ -611,7 +616,7 @@ TEST_F(PointerDelegateTest, Protocol_PointersAreIndependent) { RunLoopUntilIdle(); // Server gets watch call. constexpr fup_TouchIxnId kIxnTwo = { - .device_id = 0u, .pointer_id = 2u, .interaction_id = 2u}; + .device_id = 1u, .pointer_id = 2u, .interaction_id = 2u}; // Fuchsia ptr1 ADD and ptr2 ADD, no grant result for either - buffer them. std::vector events = @@ -641,11 +646,17 @@ TEST_F(PointerDelegateTest, Protocol_PointersAreIndependent) { touch_source_->ScheduleCallback(std::move(events)); RunLoopUntilIdle(); + // Note: Fuchsia's device and pointer IDs (both 32 bit) are coerced together + // to fit in Flutter's 64-bit device ID. However, Flutter's pointer_identifier + // is not set by platform runner code - PointerDataCaptureConverter (PDPC) + // sets it. ASSERT_TRUE(pointers.has_value()); ASSERT_EQ(pointers.value().size(), 2u); - EXPECT_EQ(pointers.value()[0].pointer_identifier, 2); + EXPECT_EQ(pointers.value()[0].pointer_identifier, 0); // reserved for PDPC + EXPECT_EQ(pointers.value()[0].device, (int64_t)((1ul << 32) | 2u)); EXPECT_EQ(pointers.value()[0].change, flutter::PointerData::Change::kAdd); - EXPECT_EQ(pointers.value()[1].pointer_identifier, 2); + EXPECT_EQ(pointers.value()[1].pointer_identifier, 0); // reserved for PDPC + EXPECT_EQ(pointers.value()[1].device, (int64_t)((1ul << 32) | 2u)); EXPECT_EQ(pointers.value()[1].change, flutter::PointerData::Change::kDown); pointers = {}; @@ -659,9 +670,11 @@ TEST_F(PointerDelegateTest, Protocol_PointersAreIndependent) { ASSERT_TRUE(pointers.has_value()); ASSERT_EQ(pointers.value().size(), 2u); - EXPECT_EQ(pointers.value()[0].pointer_identifier, 1); + EXPECT_EQ(pointers.value()[0].pointer_identifier, 0); // reserved for PDPC + EXPECT_EQ(pointers.value()[0].device, (int64_t)((1ul << 32) | 1u)); EXPECT_EQ(pointers.value()[0].change, flutter::PointerData::Change::kAdd); - EXPECT_EQ(pointers.value()[1].pointer_identifier, 1); + EXPECT_EQ(pointers.value()[1].pointer_identifier, 0); // reserved for PDPC + EXPECT_EQ(pointers.value()[1].device, (int64_t)((1ul << 32) | 1u)); EXPECT_EQ(pointers.value()[1].change, flutter::PointerData::Change::kDown); pointers = {}; } diff --git a/shell/platform/fuchsia/flutter/runner.cc b/shell/platform/fuchsia/flutter/runner.cc index 93708c6be58b8..6c59e6cdd0815 100644 --- a/shell/platform/fuchsia/flutter/runner.cc +++ b/shell/platform/fuchsia/flutter/runner.cc @@ -188,7 +188,10 @@ Runner::Runner(fml::RefPtr task_runner, SetThreadName("io.flutter.runner.main"); context_->outgoing()->AddPublicService( - std::bind(&Runner::RegisterComponent, this, std::placeholders::_1)); + std::bind(&Runner::RegisterComponentV1, this, std::placeholders::_1)); + context_->outgoing() + ->AddPublicService( + std::bind(&Runner::RegisterComponentV2, this, std::placeholders::_1)); #if !defined(DART_PRODUCT) if (Dart_IsPrecompiledRuntime()) { @@ -203,15 +206,19 @@ Runner::Runner(fml::RefPtr task_runner, Runner::~Runner() { context_->outgoing()->RemovePublicService(); + context_->outgoing() + ->RemovePublicService(); #if !defined(DART_PRODUCT) trace_observer_->Stop(); #endif // !defined(DART_PRODUCT) } -void Runner::RegisterComponent( +// CF v1 lifecycle methods. + +void Runner::RegisterComponentV1( fidl::InterfaceRequest request) { - active_components_bindings_.AddBinding(this, std::move(request)); + active_components_v1_bindings_.AddBinding(this, std::move(request)); } void Runner::StartComponent( @@ -233,15 +240,15 @@ void Runner::StartComponent( // we capture the runner in the termination callback. There is no risk of // there being multiple component runner instances in the process at the same // time. So it is safe to use the raw pointer. - Component::TerminationCallback termination_callback = - [component_runner = this](const Component* component) { + ComponentV1::TerminationCallback termination_callback = + [component_runner = this](const ComponentV1* component) { component_runner->task_runner_->PostTask( [component_runner, component]() { - component_runner->OnComponentTerminate(component); + component_runner->OnComponentV1Terminate(component); }); }; - ActiveComponent active_component = Component::Create( + ActiveComponentV1 active_component = ComponentV1::Create( std::move(termination_callback), // termination callback std::move(package), // component package std::move(startup_info), // startup info @@ -250,29 +257,103 @@ void Runner::StartComponent( ); auto key = active_component.component.get(); - active_components_[key] = std::move(active_component); + active_components_v1_[key] = std::move(active_component); +} + +void Runner::OnComponentV1Terminate(const ComponentV1* component) { + auto app = active_components_v1_.find(component); + if (app == active_components_v1_.end()) { + FML_LOG(INFO) + << "The remote end of the component runner tried to terminate an " + "component that has already been terminated, possibly because we " + "initiated the termination"; + return; + } + ActiveComponentV1& active_component = app->second; + + // Grab the items out of the entry because we will have to rethread the + // destruction. + std::unique_ptr component_to_destroy = + std::move(active_component.component); + std::unique_ptr component_thread = + std::move(active_component.platform_thread); + + // Delete the entry. + active_components_v1_.erase(component); + + // Post the task to destroy the component and quit its message loop. + component_thread->GetTaskRunner()->PostTask(fml::MakeCopyable( + [instance = std::move(component_to_destroy), + thread = component_thread.get()]() mutable { instance.reset(); })); + + // Terminate and join the thread's message loop. + component_thread->Join(); +} + +// CF v2 lifecycle methods. + +void Runner::RegisterComponentV2( + fidl::InterfaceRequest + request) { + active_components_v2_bindings_.AddBinding(this, std::move(request)); +} + +void Runner::Start( + fuchsia::component::runner::ComponentStartInfo start_info, + fidl::InterfaceRequest + controller) { + // TRACE_DURATION currently requires that the string data does not change + // in the traced scope. Since |package| gets moved in the ComponentV2::Create + // call below, we cannot ensure that |package.resolved_url| does not move or + // change, so we make a copy to pass to TRACE_DURATION. + // TODO(PT-169): Remove this copy when TRACE_DURATION reads string arguments + // eagerly. + const std::string url_copy = start_info.resolved_url(); + TRACE_EVENT1("flutter", "Start", "url", url_copy.c_str()); + + // Notes on component termination: Components typically terminate on the + // thread on which they were created. This usually means the thread was + // specifically created to host the component. But we want to ensure that + // access to the active components collection is made on the same thread. So + // we capture the runner in the termination callback. There is no risk of + // there being multiple component runner instances in the process at the same + // time. So it is safe to use the raw pointer. + ComponentV2::TerminationCallback termination_callback = + [component_runner = this](const ComponentV2* component) { + component_runner->task_runner_->PostTask( + [component_runner, component]() { + component_runner->OnComponentV2Terminate(component); + }); + }; + + ActiveComponentV2 active_component = ComponentV2::Create( + std::move(termination_callback), std::move(start_info), + context_->svc() /* runner_incoming_services */, std::move(controller)); + + auto key = active_component.component.get(); + active_components_v2_[key] = std::move(active_component); } -void Runner::OnComponentTerminate(const Component* component) { - auto app = active_components_.find(component); - if (app == active_components_.end()) { +void Runner::OnComponentV2Terminate(const ComponentV2* component) { + auto active_component_it = active_components_v2_.find(component); + if (active_component_it == active_components_v2_.end()) { FML_LOG(INFO) << "The remote end of the component runner tried to terminate an " "component that has already been terminated, possibly because we " "initiated the termination"; return; } - ActiveComponent& active_component = app->second; + ActiveComponentV2& active_component = active_component_it->second; // Grab the items out of the entry because we will have to rethread the // destruction. - std::unique_ptr component_to_destroy = + std::unique_ptr component_to_destroy = std::move(active_component.component); std::unique_ptr component_thread = std::move(active_component.platform_thread); // Delete the entry. - active_components_.erase(component); + active_components_v2_.erase(component); // Post the task to destroy the component and quit its message loop. component_thread->GetTaskRunner()->PostTask(fml::MakeCopyable( @@ -317,7 +398,7 @@ void Runner::SetupTraceObserver() { runner->prolonged_context_ = trace_acquire_prolonged_context(); Dart_StartProfiling(); } else if (trace_state() == TRACE_STOPPING) { - for (auto& it : runner->active_components_) { + for (auto& it : runner->active_components_v1_) { fml::AutoResetWaitableEvent latch; fml::TaskRunner::RunNowOrPostTask( it.second.platform_thread->GetTaskRunner(), [&]() { @@ -326,6 +407,8 @@ void Runner::SetupTraceObserver() { }); latch.Wait(); } + // TODO(fxb/50694): Write v2 component profiles to trace once we're + // convinced they're stable. Dart_StopProfiling(); trace_release_prolonged_context(runner->prolonged_context_); } diff --git a/shell/platform/fuchsia/flutter/runner.h b/shell/platform/fuchsia/flutter/runner.h index e5373a2870ab9..ea3442b8ec295 100644 --- a/shell/platform/fuchsia/flutter/runner.h +++ b/shell/platform/fuchsia/flutter/runner.h @@ -8,13 +8,15 @@ #include #include +#include #include #include #include #include #include -#include "component.h" +#include "component_v1.h" +#include "component_v2.h" #include "flutter/fml/macros.h" #include "fml/memory/ref_ptr.h" #include "fml/task_runner.h" @@ -23,9 +25,14 @@ namespace flutter_runner { -// Publishes the |fuchsia::sys::Runner| service and runs components on -// their own threads. -class Runner final : public fuchsia::sys::Runner { +/// Publishes the CF v1 and CF v2 runner services. +/// +/// Each component will be run on a separate thread dedicated to that component. +/// +/// TODO(fxb/50694): Add unit tests for CF v2. +class Runner final + : public fuchsia::sys::Runner /* CF v1 */, + public fuchsia::component::runner::ComponentRunner /* CF v2 */ { public: // Does not take ownership of context. Runner(fml::RefPtr task_runner, @@ -34,15 +41,42 @@ class Runner final : public fuchsia::sys::Runner { ~Runner(); private: + // CF v1 lifecycle methods. + // TODO(fxb/50694) Deprecate these once all Flutter components have been + // ported to CF v2. + // |fuchsia::sys::Runner| void StartComponent(fuchsia::sys::Package package, fuchsia::sys::StartupInfo startup_info, fidl::InterfaceRequest controller) override; - void RegisterComponent(fidl::InterfaceRequest request); + /// Registers a new CF v1 component with this runner, binding the component + /// to this runner. + void RegisterComponentV1( + fidl::InterfaceRequest request); + + /// Callback that should be fired when a registered CF v2 component is + /// terminated. + void OnComponentV1Terminate(const ComponentV1* component); + + // CF v2 lifecycle methods. + + // |fuchsia::component::runner::ComponentRunner| + void Start( + fuchsia::component::runner::ComponentStartInfo start_info, + fidl::InterfaceRequest + controller) override; + + /// Registers a new CF v2 component with this runner, binding the component + /// to this runner. + void RegisterComponentV2( + fidl::InterfaceRequest + request); - void OnComponentTerminate(const Component* component); + /// Callback that should be fired when a registered CF v2 component is + /// terminated. + void OnComponentV2Terminate(const ComponentV2* component); void SetupICU(); @@ -62,8 +96,21 @@ class Runner final : public fuchsia::sys::Runner { fml::RefPtr task_runner_; sys::ComponentContext* context_; - fidl::BindingSet active_components_bindings_; - std::unordered_map active_components_; + + // CF v1 component state. + fidl::BindingSet active_components_v1_bindings_; + std::unordered_map + active_components_v1_; + + // CF v2 component state. + + /// The components that are currently bound to this runner. + fidl::BindingSet + active_components_v2_bindings_; + + /// The components that are currently actively running on threads. + std::unordered_map + active_components_v2_; #if !defined(DART_PRODUCT) // The connection between the Dart VM service and The Hub. diff --git a/shell/platform/fuchsia/flutter/surface.cc b/shell/platform/fuchsia/flutter/surface.cc index aadfffe73c413..0919d3e908163 100644 --- a/shell/platform/fuchsia/flutter/surface.cc +++ b/shell/platform/fuchsia/flutter/surface.cc @@ -30,8 +30,10 @@ bool Surface::IsValid() { // |flutter::Surface| std::unique_ptr Surface::AcquireFrame( const SkISize& size) { + flutter::SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; return std::make_unique( - nullptr, true, + nullptr, std::move(framebuffer_info), [](const flutter::SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); diff --git a/shell/platform/fuchsia/flutter/tests/fake_flatland_unittests.cc b/shell/platform/fuchsia/flutter/tests/fake_flatland_unittests.cc index 2d0f040fec8bd..0b161d10ab178 100644 --- a/shell/platform/fuchsia/flutter/tests/fake_flatland_unittests.cc +++ b/shell/platform/fuchsia/flutter/tests/fake_flatland_unittests.cc @@ -32,10 +32,10 @@ class FakeFlatlandTest : public ::testing::Test { FakeFlatland& fake_flatland() { return fake_flatland_; } fuchsia::ui::composition::FlatlandPtr ConnectFlatland() { - FML_CHECK(!fake_flatland_.is_bound()); + FML_CHECK(!fake_flatland_.is_flatland_connected()); auto flatland_handle = - fake_flatland_.Connect(flatland_subloop_->dispatcher()); + fake_flatland_.ConnectFlatland(flatland_subloop_->dispatcher()); return flatland_handle.Bind(); } diff --git a/shell/platform/fuchsia/flutter/tests/fakes/mouse_source.h b/shell/platform/fuchsia/flutter/tests/fakes/mouse_source.h new file mode 100644 index 0000000000000..47585e69a4fd5 --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/fakes/mouse_source.h @@ -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. + +#ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_TESTS_FAKES_MOUSE_SOURCE_H_ +#define FLUTTER_SHELL_PLATFORM_FUCHSIA_TESTS_FAKES_MOUSE_SOURCE_H_ + +#include + +#include "flutter/fml/logging.h" + +namespace flutter_runner::testing { + +// A test stub to act as the protocol server. A test can control what is sent +// back by this server implementation, via the ScheduleCallback call. +class FakeMouseSource : public fuchsia::ui::pointer::MouseSource { + public: + // |fuchsia.ui.pointer.MouseSource| + void Watch(MouseSource::WatchCallback callback) override { + callback_ = std::move(callback); + } + + // Have the server issue events to the client's hanging-get Watch call. + void ScheduleCallback(std::vector events) { + FML_CHECK(callback_) << "require a valid WatchCallback"; + callback_(std::move(events)); + } + + private: + // Client-side logic to invoke on Watch() call's return. A test triggers it + // with ScheduleCallback(). + MouseSource::WatchCallback callback_; +}; + +} // namespace flutter_runner::testing + +#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_TESTS_FAKES_MOUSE_SOURCE_H_ diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/BUILD.gn b/shell/platform/fuchsia/flutter/tests/fakes/scenic/BUILD.gn index fb1333b62ab93..f1c40fd448be6 100644 --- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/BUILD.gn +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/BUILD.gn @@ -10,6 +10,8 @@ source_set("scenic") { sources = [ "fake_flatland.cc", "fake_flatland.h", + "fake_flatland_types.cc", + "fake_flatland_types.h", "fake_resources.cc", "fake_resources.h", "fake_session.cc", diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc index fd27b666f4123..9933400a6990a 100644 --- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc @@ -4,26 +4,48 @@ #include "fake_flatland.h" +#include + +#include // For std::remove_if +#include + #include "flutter/fml/logging.h" namespace flutter_runner::testing { FakeFlatland::FakeFlatland() - : binding_(this), present_handler_([](auto args) {}) {} + : allocator_binding_(this), + flatland_binding_(this), + present_handler_([](auto args) {}) {} + +FakeFlatland::~FakeFlatland() = default; + +fuchsia::ui::composition::AllocatorHandle FakeFlatland::ConnectAllocator( + async_dispatcher_t* dispatcher) { + FML_CHECK(!allocator_binding_.is_bound()); + + fuchsia::ui::composition::AllocatorHandle allocator; + allocator_binding_.Bind(allocator.NewRequest(), dispatcher); + + return allocator; +} -fidl::InterfaceHandle FakeFlatland::Connect( +fuchsia::ui::composition::FlatlandHandle FakeFlatland::ConnectFlatland( async_dispatcher_t* dispatcher) { - FML_CHECK(!binding_.is_bound()); + FML_CHECK(!flatland_binding_.is_bound()); - fidl::InterfaceHandle flatland; - binding_.Bind(flatland.NewRequest(), dispatcher); + fuchsia::ui::composition::FlatlandHandle flatland; + flatland_binding_.Bind(flatland.NewRequest(), dispatcher); return flatland; } void FakeFlatland::Disconnect(fuchsia::ui::composition::FlatlandError error) { - binding_.events().OnError(std::move(error)); - binding_.Unbind(); + flatland_binding_.events().OnError(std::move(error)); + flatland_binding_.Unbind(); + allocator_binding_ + .Unbind(); // TODO(fxb/85619): Does the real Scenic unbind this when + // Flatland has an error? Or is it independent? } void FakeFlatland::SetPresentHandler(PresentHandler present_handler) { @@ -34,25 +56,748 @@ void FakeFlatland::SetPresentHandler(PresentHandler present_handler) { void FakeFlatland::FireOnNextFrameBeginEvent( fuchsia::ui::composition::OnNextFrameBeginValues on_next_frame_begin_values) { - binding_.events().OnNextFrameBegin(std::move(on_next_frame_begin_values)); + flatland_binding_.events().OnNextFrameBegin( + std::move(on_next_frame_begin_values)); } void FakeFlatland::FireOnFramePresentedEvent( fuchsia::scenic::scheduling::FramePresentedInfo frame_presented_info) { - binding_.events().OnFramePresented(std::move(frame_presented_info)); + flatland_binding_.events().OnFramePresented(std::move(frame_presented_info)); } void FakeFlatland::NotImplemented_(const std::string& name) { FML_LOG(FATAL) << "FakeFlatland does not implement " << name; } +void FakeFlatland::RegisterBufferCollection( + fuchsia::ui::composition::RegisterBufferCollectionArgs args, + RegisterBufferCollectionCallback callback) { + auto [export_token_koid, _] = GetKoids(args.export_token()); + auto [__, emplace_binding_success] = + graph_bindings_.buffer_collections.emplace( + export_token_koid, + BufferCollectionBinding{ + .export_token = std::move(*args.mutable_export_token()), + .sysmem_token = + std::move(*args.mutable_buffer_collection_token()), + .usage = args.usage(), + }); + // TODO(fxb/85619): Disconnect the Allocator here + FML_CHECK(emplace_binding_success) + << "FakeFlatland::RegisterBufferCollection: BufferCollection already " + "exists with koid " + << export_token_koid; +} + void FakeFlatland::Present(fuchsia::ui::composition::PresentArgs args) { - // TODO(fxb/85619): ApplyCommands() + // Each FIDL call between this `Present()` and the last one mutated the + // `pending_graph_` independently of the `current_graph_`. Only the + // `current_graph_` is visible externally in the test. + // + // `Present()` updates the current graph with a deep clone of the pending one. + current_graph_ = pending_graph_.Clone(); + present_handler_(std::move(args)); } +void FakeFlatland::CreateView( + fuchsia::ui::views::ViewCreationToken token, + fidl::InterfaceRequest + parent_viewport_watcher) { + CreateView2(std::move(token), fuchsia::ui::views::ViewIdentityOnCreation{}, + fuchsia::ui::composition::ViewBoundProtocols{}, + std::move(parent_viewport_watcher)); +} + +void FakeFlatland::CreateView2( + fuchsia::ui::views::ViewCreationToken token, + fuchsia::ui::views::ViewIdentityOnCreation view_identity, + fuchsia::ui::composition::ViewBoundProtocols view_protocols, + fidl::InterfaceRequest + parent_viewport_watcher) { + // TODO(fxb/85619): Handle a 2nd CreateView call + FML_CHECK(!pending_graph_.view.has_value()); + FML_CHECK(!graph_bindings_.viewport_watcher.has_value()); + + auto view_token_koids = GetKoids(token); + auto view_ref_koids = GetKoids(view_identity.view_ref); + auto view_ref_control_koids = GetKoids(view_identity.view_ref_control); + FML_CHECK(view_ref_koids.first == view_ref_control_koids.second); + FML_CHECK(view_ref_koids.second == view_ref_control_koids.first); + + pending_graph_.view.emplace(FakeView{ + .view_token = view_token_koids.first, + .view_ref = view_ref_koids.first, + .view_ref_control = view_ref_control_koids.first, + .view_ref_focused = + view_protocols.has_view_ref_focused() + ? GetKoids(view_protocols.view_ref_focused()).first + : zx_koid_t{}, + .focuser = view_protocols.has_view_focuser() + ? GetKoids(view_protocols.view_focuser()).first + : zx_koid_t{}, + .touch_source = view_protocols.has_touch_source() + ? GetKoids(view_protocols.touch_source()).first + : zx_koid_t{}, + .mouse_source = view_protocols.has_mouse_source() + ? GetKoids(view_protocols.mouse_source()).first + : zx_koid_t{}, + .parent_viewport_watcher = GetKoids(parent_viewport_watcher).first, + }); + graph_bindings_.viewport_watcher.emplace( + view_token_koids.first, + ParentViewportWatcher( + std::move(token), std::move(view_identity), std::move(view_protocols), + std::move(parent_viewport_watcher), flatland_binding_.dispatcher())); +} + +void FakeFlatland::CreateTransform( + fuchsia::ui::composition::TransformId transform_id) { + if (transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::CreateTransform: TransformId 0 is invalid."; + return; + } + if (pending_graph_.transform_map.count(transform_id.value) != 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::CreateTransform: TransformId " + << transform_id.value << " is already in use."; + return; + } + + auto [emplaced_transform, emplace_success] = + pending_graph_.transform_map.emplace( + transform_id.value, std::make_shared(FakeTransform{ + .id = transform_id, + })); + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(emplace_success) + << "FakeFlatland::CreateTransform: Internal error (transform_map) adding " + "transform with id: " + << transform_id.value; + + auto [_, emplace_parent_success] = parents_map_.emplace( + transform_id.value, + std::make_pair(std::weak_ptr(), + std::weak_ptr(emplaced_transform->second))); + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(emplace_parent_success) + << "FakeFlatland::CreateTransform: Internal error (parent_map) adding " + "transform with id: " + << transform_id.value; +} + +void FakeFlatland::SetTranslation( + fuchsia::ui::composition::TransformId transform_id, + fuchsia::math::Vec translation) { + if (transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::SetTranslation: TransformId 0 is invalid."; + return; + } + + auto found_transform = pending_graph_.transform_map.find(transform_id.value); + if (found_transform == pending_graph_.transform_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetTranslation: TransformId " + << transform_id.value << " does not exist."; + return; + } + + auto& transform = found_transform->second; + FML_CHECK(transform); + transform->translation = translation; +} + +void FakeFlatland::SetOrientation( + fuchsia::ui::composition::TransformId transform_id, + fuchsia::ui::composition::Orientation orientation) { + if (transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::SetOrientation: TransformId 0 is invalid."; + return; + } + + auto found_transform = pending_graph_.transform_map.find(transform_id.value); + if (found_transform == pending_graph_.transform_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetOrientation: TransformId " + << transform_id.value << " does not exist."; + return; + } + + auto& transform = found_transform->second; + FML_CHECK(transform); + transform->orientation = orientation; +} + +void FakeFlatland::SetClipBounds( + fuchsia::ui::composition::TransformId transform_id, + fuchsia::math::Rect clip_bounds) { + if (transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::SetClipBounds: TransformId 0 is invalid."; + return; + } + + auto found_transform = pending_graph_.transform_map.find(transform_id.value); + if (found_transform == pending_graph_.transform_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetClipBounds: TransformId " + << transform_id.value << " does not exist."; + return; + } + + auto& transform = found_transform->second; + FML_CHECK(transform); + transform->clip_bounds = clip_bounds; +} + +void FakeFlatland::SetOpacity( + fuchsia::ui::composition::TransformId transform_id, + float opacity) { + if (transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetOpacity: TransformId 0 is invalid."; + return; + } + + auto found_transform = pending_graph_.transform_map.find(transform_id.value); + if (found_transform == pending_graph_.transform_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetOpacity: TransformId " + << transform_id.value << " does not exist."; + return; + } + + auto& transform = found_transform->second; + FML_CHECK(transform); + transform->opacity = opacity; +} + +void FakeFlatland::AddChild( + fuchsia::ui::composition::TransformId parent_transform_id, + fuchsia::ui::composition::TransformId child_transform_id) { + if (parent_transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::AddChild: Parent TransformId 0 is invalid."; + return; + } + if (child_transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::AddChild: Child TransformId 0 is invalid."; + return; + } + + auto found_parent = + pending_graph_.transform_map.find(parent_transform_id.value); + if (found_parent == pending_graph_.transform_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::AddChild: Parent TransformId " + << parent_transform_id.value << " does not exist."; + return; + } + auto found_child = + pending_graph_.transform_map.find(child_transform_id.value); + if (found_child == pending_graph_.transform_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::AddChild: Child TransformId " + << child_transform_id.value << " does not exist."; + return; + } + auto found_child_old_parent = parents_map_.find(child_transform_id.value); + if (found_child_old_parent == parents_map_.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::AddChild: Internal error - Child TransformId " + << child_transform_id.value << " is not in parents_map."; + return; + } + if (found_child_old_parent->second.second.expired()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::AddChild: Internal error - Child TransformId " + << child_transform_id.value << " is expired in parents_map."; + return; + } + + auto& child = found_child->second; + auto& new_parent = found_parent->second; + new_parent->children.push_back(child); + if (auto old_parent = found_child_old_parent->second.first.lock()) { + old_parent->children.erase(std::remove_if( + old_parent->children.begin(), old_parent->children.end(), + [&child](const auto& transform) { return transform == child; })); + } + found_child_old_parent->second.first = std::weak_ptr(new_parent); +} + +void FakeFlatland::RemoveChild( + fuchsia::ui::composition::TransformId parent_transform_id, + fuchsia::ui::composition::TransformId child_transform_id) { + if (parent_transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::RemoveChild: Parent TransformId 0 is invalid."; + return; + } + if (child_transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::RemoveChild: Child TransformId 0 is invalid."; + return; + } + + auto found_child = + pending_graph_.transform_map.find(child_transform_id.value); + if (found_child == pending_graph_.transform_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::RemoveChild: Child TransformId " + << child_transform_id.value << " does not exist."; + return; + } + + auto found_parent = + pending_graph_.transform_map.find(parent_transform_id.value); + if (found_parent == pending_graph_.transform_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::RemoveChild: Parent TransformId " + << parent_transform_id.value << " does not exist."; + return; + } + + auto found_child_parent = parents_map_.find(child_transform_id.value); + if (found_child_parent == parents_map_.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::RemoveChild: Internal error - Child TransformId " + << child_transform_id.value << " is not in parents_map."; + return; + } + if (found_child_parent->second.second.expired()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::RemoveChild: Internal error - Child TransformId " + << child_transform_id.value << " is expired in parents_map."; + return; + } + if (found_child_parent->second.first.lock() != found_parent->second) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::RemoveChild: Internal error - Child TransformId " + << child_transform_id.value << " is not a child of Parent TransformId " + << parent_transform_id.value << "."; + return; + } + + found_child_parent->second.first = std::weak_ptr(); + found_parent->second->children.erase(std::remove_if( + found_parent->second->children.begin(), + found_parent->second->children.end(), + [child_to_remove = found_child->second](const auto& child) { + return child == child_to_remove; + })); +} + +void FakeFlatland::SetContent( + fuchsia::ui::composition::TransformId transform_id, + fuchsia::ui::composition::ContentId content_id) { + if (transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetContent: TransformId 0 is invalid."; + return; + } + + auto found_transform = pending_graph_.transform_map.find(transform_id.value); + if (found_transform == pending_graph_.transform_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetContent: TransformId " + << transform_id.value << " does not exist."; + return; + } + + auto& transform = found_transform->second; + FML_CHECK(transform); + if (content_id.value == 0) { + transform->content.reset(); + return; + } + + auto found_content = pending_graph_.content_map.find(content_id.value); + if (found_content == pending_graph_.content_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetContent: ContentId " + << content_id.value << " does not exist."; + return; + } + + auto& content = found_content->second; + FML_CHECK(content); + transform->content = content; +} + +void FakeFlatland::SetRootTransform( + fuchsia::ui::composition::TransformId transform_id) { + if (transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::SetRootTransform: TransformId 0 is invalid."; + return; + } + + auto found_new_root = pending_graph_.transform_map.find(transform_id.value); + if (found_new_root == pending_graph_.transform_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetRootTransform: TransformId " + << transform_id.value << " does not exist."; + return; + } + + pending_graph_.root_transform = found_new_root->second; +} + +void FakeFlatland::CreateViewport( + fuchsia::ui::composition::ContentId viewport_id, + fuchsia::ui::views::ViewportCreationToken token, + fuchsia::ui::composition::ViewportProperties properties, + fidl::InterfaceRequest + child_view_watcher) { + if (viewport_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::CreateViewport: ContentId 0 is invalid."; + return; + } + if (pending_graph_.content_map.count(viewport_id.value) != 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::CreateViewport: ContentId " + << viewport_id.value << " is already in use."; + return; + } + + auto viewport_token_koids = GetKoids(token.value); + auto [emplaced_viewport, emplace_success] = + pending_graph_.content_map.emplace( + viewport_id.value, + std::make_shared(FakeViewport{ + .id = viewport_id, + .viewport_properties = std::move(properties), + .viewport_token = viewport_token_koids.first, + .child_view_watcher = GetKoids(child_view_watcher).first, + })); + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(emplace_success) + << "FakeFlatland::CreateViewport: Internal error (content_map) adding " + "viewport with id: " + << viewport_id.value; + + auto [_, emplace_binding_success] = graph_bindings_.view_watchers.emplace( + viewport_token_koids.first, + ChildViewWatcher(std::move(token), std::move(child_view_watcher), + flatland_binding_.dispatcher())); + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(emplace_binding_success) + << "FakeFlatland::CreateViewport: Internal error (view_watcher) adding " + "viewport with id: " + << viewport_id.value; +} + +void FakeFlatland::CreateImage( + fuchsia::ui::composition::ContentId image_id, + fuchsia::ui::composition::BufferCollectionImportToken import_token, + uint32_t vmo_index, + fuchsia::ui::composition::ImageProperties properties) { + if (image_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::CreateImage: ContentId 0 is invalid."; + return; + } + if (pending_graph_.content_map.count(image_id.value) != 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::CreateImage: ContentId " + << image_id.value << " is already in use."; + return; + } + + auto import_token_koids = GetKoids(import_token); + auto [emplaced_image, emplace_success] = pending_graph_.content_map.emplace( + image_id.value, std::make_shared(FakeImage{ + .id = image_id, + .image_properties = std::move(properties), + .import_token = import_token_koids.first, + .vmo_index = vmo_index, + })); + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(emplace_success) + << "FakeFlatland::CreateImage: Internal error (content_map) adding " + "image with id: " + << image_id.value; + + auto [_, emplace_binding_success] = graph_bindings_.images.emplace( + import_token_koids.first, ImageBinding{ + .import_token = std::move(import_token), + }); + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(emplace_binding_success) + << "FakeFlatland::CreateImage: Internal error (images_binding) adding " + "viewport with id: " + << image_id.value; +} + +void FakeFlatland::SetImageSampleRegion( + fuchsia::ui::composition::ContentId image_id, + fuchsia::math::RectF rect) { + if (image_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::SetImageSampleRegion: ContentId 0 is invalid."; + return; + } + + auto found_content = pending_graph_.content_map.find(image_id.value); + if (found_content == pending_graph_.content_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetImageSampleRegion: ContentId " + << image_id.value << " does not exist."; + return; + } + + auto& content = found_content->second; + FML_CHECK(content); + FakeImage* image = std::get_if(content.get()); + if (image == nullptr) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetImageSampleRegion: ContentId " + << image_id.value << " is not an Image."; + return; + } + + image->sample_region = rect; +} + +void FakeFlatland::SetImageDestinationSize( + fuchsia::ui::composition::ContentId image_id, + fuchsia::math::SizeU size) { + if (image_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::SetImageDestinationSize: ContentId 0 is invalid."; + return; + } + + auto found_content = pending_graph_.content_map.find(image_id.value); + if (found_content == pending_graph_.content_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetImageDestinationSize: ContentId " + << image_id.value << " does not exist."; + return; + } + + auto& content = found_content->second; + FML_CHECK(content); + FakeImage* image = std::get_if(content.get()); + if (image == nullptr) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetImageDestinationSize: ContentId " + << image_id.value << " is not an Image."; + return; + } + + image->destination_size = size; +} + +void FakeFlatland::SetViewportProperties( + fuchsia::ui::composition::ContentId viewport_id, + fuchsia::ui::composition::ViewportProperties properties) { + if (viewport_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::SetViewportProperties: ContentId 0 is invalid."; + return; + } + + auto found_content = pending_graph_.content_map.find(viewport_id.value); + if (found_content == pending_graph_.content_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetViewportProperties: ContentId " + << viewport_id.value << " does not exist."; + return; + } + + auto& content = found_content->second; + FML_CHECK(content); + FakeViewport* viewport = std::get_if(content.get()); + if (viewport == nullptr) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::SetViewportProperties: ContentId " + << viewport_id.value << " is not a Viewport."; + return; + } + + viewport->viewport_properties = std::move(properties); +} + +void FakeFlatland::ReleaseTransform( + fuchsia::ui::composition::TransformId transform_id) { + if (transform_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::ReleaseTransform: TransformId 0 is invalid."; + return; + } + + size_t erased = pending_graph_.transform_map.erase(transform_id.value); + if (erased == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::ReleaseTransform: TransformId " + << transform_id.value << " does not exist."; + } + + size_t parents_erased = parents_map_.erase(transform_id.value); + if (parents_erased == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::ReleaseTransform: TransformId " + << transform_id.value << " does not exist in parents_map."; + } +} + +void FakeFlatland::ReleaseViewport( + fuchsia::ui::composition::ContentId viewport_id, + ReleaseViewportCallback callback) { + if (viewport_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) + << "FakeFlatland::ReleaseViewport: ContentId 0 is invalid."; + return; + } + + auto found_content = pending_graph_.content_map.find(viewport_id.value); + if (found_content == pending_graph_.content_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::ReleaseViewport: ContentId " + << viewport_id.value << " does not exist."; + return; + } + + auto& content = found_content->second; + FML_CHECK(content); + FakeViewport* viewport = std::get_if(content.get()); + if (viewport == nullptr) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::ReleaseViewport: ContentId " + << viewport_id.value << " is not a Viewport."; + return; + } + + pending_graph_.content_map.erase(found_content); +} + +void FakeFlatland::ReleaseImage(fuchsia::ui::composition::ContentId image_id) { + if (image_id.value == 0) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::ReleaseImage: ContentId 0 is invalid."; + return; + } + + auto found_content = pending_graph_.content_map.find(image_id.value); + if (found_content == pending_graph_.content_map.end()) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::ReleaseImage: ContentId " + << image_id.value << " does not exist."; + return; + } + + auto& content = found_content->second; + FML_CHECK(content); + FakeImage* image = std::get_if(content.get()); + if (image == nullptr) { + // TODO(fxb/85619): Raise a FlatlandError here + FML_CHECK(false) << "FakeFlatland::ReleaseImage: ContentId " + << image_id.value << " is not a Viewport."; + return; + } + + pending_graph_.content_map.erase(found_content); +} + +void FakeFlatland::Clear() { + parents_map_.clear(); + pending_graph_.Clear(); +} + void FakeFlatland::SetDebugName(std::string debug_name) { debug_name_ = std::move(debug_name); } +FakeFlatland::ParentViewportWatcher::ParentViewportWatcher( + fuchsia::ui::views::ViewCreationToken view_token, + fuchsia::ui::views::ViewIdentityOnCreation view_identity, + fuchsia::ui::composition::ViewBoundProtocols view_protocols, + fidl::InterfaceRequest + parent_viewport_watcher, + async_dispatcher_t* dispatcher) + : view_token(std::move(view_token)), + view_identity(std::move(view_identity)), + view_protocols(std::move(view_protocols)), + parent_viewport_watcher(this, + std::move(parent_viewport_watcher), + dispatcher) {} + +FakeFlatland::ParentViewportWatcher::ParentViewportWatcher( + ParentViewportWatcher&& other) + : view_token(std::move(other.view_token)), + view_identity(std::move(other.view_identity)), + view_protocols(std::move(other.view_protocols)), + parent_viewport_watcher(this, + other.parent_viewport_watcher.Unbind(), + other.parent_viewport_watcher.dispatcher()) {} + +FakeFlatland::ParentViewportWatcher::~ParentViewportWatcher() = default; + +void FakeFlatland::ParentViewportWatcher::NotImplemented_( + const std::string& name) { + FML_LOG(FATAL) << "FakeFlatland::ParentViewportWatcher does not implement " + << name; +} + +void FakeFlatland::ParentViewportWatcher::GetLayout( + GetLayoutCallback callback) { + NotImplemented_("GetLayout"); +} + +void FakeFlatland::ParentViewportWatcher::GetStatus( + GetStatusCallback callback) { + NotImplemented_("GetStatus"); +} + +FakeFlatland::ChildViewWatcher::ChildViewWatcher( + fuchsia::ui::views::ViewportCreationToken viewport_token, + fidl::InterfaceRequest + child_view_watcher, + async_dispatcher_t* dispatcher) + : viewport_token(std::move(viewport_token)), + child_view_watcher(this, std::move(child_view_watcher), dispatcher) {} + +FakeFlatland::ChildViewWatcher::ChildViewWatcher(ChildViewWatcher&& other) + : viewport_token(std::move(other.viewport_token)), + child_view_watcher(this, + other.child_view_watcher.Unbind(), + other.child_view_watcher.dispatcher()) {} + +FakeFlatland::ChildViewWatcher::~ChildViewWatcher() = default; + +void FakeFlatland::ChildViewWatcher::NotImplemented_(const std::string& name) { + FML_LOG(FATAL) << "FakeFlatland::ChildViewWatcher does not implement " + << name; +} + +void FakeFlatland::ChildViewWatcher::GetStatus(GetStatusCallback callback) { + NotImplemented_("GetStatus"); +} + } // namespace flutter_runner::testing diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.h b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.h index db3e2040cd87d..48c4db6210cb6 100644 --- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.h +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.h @@ -5,75 +5,100 @@ #ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_TESTS_FAKES_SCENIC_FAKE_FLATLAND_H_ #define FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_TESTS_FAKES_SCENIC_FAKE_FLATLAND_H_ +#include #include +#include #include #include +#include #include #include -#include +#include +#include +#include #include +#include +#include +#include "flutter/fml/logging.h" #include "flutter/fml/macros.h" +#include "fake_flatland_types.h" + namespace flutter_runner::testing { -// A lightweight fake implementation of the scenic Flatland API. The fake has -// no side effects besides mutating its own internal state. +// A lightweight fake implementation of the Flatland API. +// +// The fake has no side effects besides mutating its own internal state +// according to the rules of interacting with the Flatland API. It makes that +// internal state available for inspection by a test. // -// The fake allows tests to do a few things that would be difficult using either +// Thus it allows tests to do a few things that would be difficult using either // a mock implementation or the real implementation: // + It allows the user to hook `Present` invocations and respond with // stubbed-out `FuturePresentationTimes`, but more crucially it mimics the -// real scenic behavior of only processing commands when a `Present` is +// real Flatland behavior of only processing commands when a `Present` is // invoked. -// TODO(fxb/85619): Implement the following bullet-points // + It allows the user to inspect a snapshot of the scene graph at any moment // in time, via the `SceneGraph()` accessor. -// + It stores the various flatland resources generated by commands into a -// std::unordered_map, and also correctly manages the resource lifetimes via -// reference counting. This allows a resource to stay alive if its parent -// still holds a reference to it, in the same way the real scenic -// implementation would. +// + It stores the various Flatland transforms and content generated by +// commands into a std::unordered_map, and also correctly manages the +// transform and content lifetimes via reference counting. This allows a +// given transform or content to stay alive if its parent still holds a +// reference to it, in the same way the real Flatland implementation would. // + The resources returned by `SceneGraph()` that the test uses for // inspection are decoupled from the resources managed internally by the // `FakeFlatland` itself -- they are a snapshot of the scene graph at that // moment in time, with all snapshot state being cloned from the underlying // scene graph state. This allows the `FakeFlatland` and test to naturally -// use `shared_ptr` for reference counting and mimic the real scenic behavior -// exactly, instead of an awkward index-based API. -// -// Error handling / flatland disconnection is still WIP. FakeFlatland will -// likely generate a CHECK in any place where the real scenic would disconnect -// the flatland or send a FlatlandError. -// -// Input is not handled. -// -// Rendering is not handled. +// use `shared_ptr` for reference counting and mimic the real Flatland +// behavior exactly, instead of an awkward index-based API. +// + TODO(fxb/85619): Allow injecting {touch,mouse,focus,views} events using +// the ViewBoundProtocols and {ParentViewport,ChildView}Watcher. +// + TODO(fxb/85619): Error handling / flatland disconnection is still WIP. +// FakeFlatland will likely generate a CHECK in any place where the real +// Flatland would disconnect or send a FlatlandError. // -// Cross-flatland links between View and Viewport are not handled. +// The fake deliberately does not attempt to handle certain aspects of the real +// Flatland implemntation which are complex or burdensome to implement: +// +Rendering/interacting with Vulkan in any way +// +Cross-flatland links between Views and Viewports. class FakeFlatland - : public fuchsia::ui::composition::testing::Flatland_TestBase { + : public fuchsia::ui::composition::testing::Allocator_TestBase, + public fuchsia::ui::composition::testing::Flatland_TestBase { public: using PresentHandler = std::function; FakeFlatland(); - ~FakeFlatland() override = default; + ~FakeFlatland() override; - bool is_bound() const { return binding_.is_bound(); } + bool is_allocator_connected() const { return allocator_binding_.is_bound(); } + + bool is_flatland_connected() const { return flatland_binding_.is_bound(); } const std::string& debug_name() const { return debug_name_; } - // Bind this session's FIDL channels to the `dispatcher` and allow processing - // of incoming FIDL requests. + const FakeGraph& graph() { return current_graph_; } + + // Bind this instance's Flatland FIDL channel to the `dispatcher` and allow + // processing of incoming FIDL requests. + // + // This can only be called once. + fuchsia::ui::composition::AllocatorHandle ConnectAllocator( + async_dispatcher_t* dispatcher = nullptr); + + // Bind this instance's Allocator FIDL channel to the `dispatcher` and allow + // processing of incoming FIDL requests. // // This can only be called once. - fidl::InterfaceHandle Connect( + fuchsia::ui::composition::FlatlandHandle ConnectFlatland( async_dispatcher_t* dispatcher = nullptr); - // Disconnect the session's FIDL channels with an error. - // TODO: Call this internally upon command error, instead of CHECK'ing. + // Disconnect the Flatland's FIDL channel with an error. + // TODO(fxb/85619): Call this internally upon command error, instead of + // CHECK'ing. void Disconnect(fuchsia::ui::composition::FlatlandError error); // Set a handler for `Present`-related FIDL calls' return values. @@ -93,21 +118,205 @@ class FakeFlatland fuchsia::scenic::scheduling::FramePresentedInfo frame_presented_info); private: + struct ParentViewportWatcher : public fuchsia::ui::composition::testing:: + ParentViewportWatcher_TestBase { + public: + ParentViewportWatcher( + fuchsia::ui::views::ViewCreationToken view_token, + fuchsia::ui::views::ViewIdentityOnCreation view_identity, + fuchsia::ui::composition::ViewBoundProtocols view_protocols, + fidl::InterfaceRequest + parent_viewport_watcher, + async_dispatcher_t* dispatcher); + ParentViewportWatcher(ParentViewportWatcher&& other); + ~ParentViewportWatcher() override; + + // |fuchsia::ui::composition::testing::ParentViewportWatcher_TestBase| + void NotImplemented_(const std::string& name) override; + + // |fuchsia::ui::composition::ParentViewportWatcher| + void GetLayout(GetLayoutCallback callback) override; + + // |fuchsia::ui::composition::ParentViewportWatcher| + void GetStatus(GetStatusCallback callback) override; + + fuchsia::ui::views::ViewCreationToken view_token; + fuchsia::ui::views::ViewIdentityOnCreation view_identity; + fuchsia::ui::composition::ViewBoundProtocols view_protocols; + fidl::Binding + parent_viewport_watcher; + }; + + struct ChildViewWatcher + : public fuchsia::ui::composition::testing::ChildViewWatcher_TestBase { + public: + ChildViewWatcher( + fuchsia::ui::views::ViewportCreationToken viewport_token, + fidl::InterfaceRequest + child_view_watcher, + async_dispatcher_t* dispatcher); + ChildViewWatcher(ChildViewWatcher&& other); + ~ChildViewWatcher() override; + + // |fuchsia::ui::composition::testing::ChildViewWatcher_TestBase| + void NotImplemented_(const std::string& name) override; + + // |fuchsia::ui::composition::ChildViewWatcher| + void GetStatus(GetStatusCallback callback) override; + + fuchsia::ui::views::ViewportCreationToken viewport_token; + fidl::Binding + child_view_watcher; + }; + + struct ImageBinding { + fuchsia::ui::composition::BufferCollectionImportToken import_token; + }; + + struct BufferCollectionBinding { + fuchsia::ui::composition::BufferCollectionExportToken export_token; + fuchsia::sysmem::BufferCollectionTokenHandle sysmem_token; + + fuchsia::ui::composition::RegisterBufferCollectionUsage usage; + }; + + struct GraphBindings { + std::optional> viewport_watcher; + std::unordered_map view_watchers; + std::unordered_map images; + std::unordered_map buffer_collections; + }; + + // |fuchsia::ui::composition::testing::Allocator_TestBase| // |fuchsia::ui::composition::testing::Flatland_TestBase| void NotImplemented_(const std::string& name) override; + // |fuchsia::ui::composition::testing::Allocator| + void RegisterBufferCollection( + fuchsia::ui::composition::RegisterBufferCollectionArgs args, + RegisterBufferCollectionCallback callback) override; + // |fuchsia::ui::composition::Flatland| void Present(fuchsia::ui::composition::PresentArgs args) override; // |fuchsia::ui::composition::Flatland| - void SetDebugName(std::string debug_name) override; + void CreateView( + fuchsia::ui::views::ViewCreationToken token, + fidl::InterfaceRequest + parent_viewport_watcher) override; + + // |fuchsia::ui::composition::Flatland| + void CreateView2( + fuchsia::ui::views::ViewCreationToken token, + fuchsia::ui::views::ViewIdentityOnCreation view_identity, + fuchsia::ui::composition::ViewBoundProtocols view_protocols, + fidl::InterfaceRequest + parent_viewport_watcher) override; - fidl::Binding binding_; + // |fuchsia::ui::composition::Flatland| + void CreateTransform( + fuchsia::ui::composition::TransformId transform_id) override; - std::string debug_name_; + // |fuchsia::ui::composition::Flatland| + void SetTranslation(fuchsia::ui::composition::TransformId transform_id, + fuchsia::math::Vec translation) override; + // |fuchsia::ui::composition::Flatland| + void SetOrientation( + fuchsia::ui::composition::TransformId transform_id, + fuchsia::ui::composition::Orientation orientation) override; + + // |fuchsia::ui::composition::Flatland| + void SetClipBounds(fuchsia::ui::composition::TransformId transform_id, + fuchsia::math::Rect clip_bounds) override; + + // |fuchsia::ui::composition::Flatland| + void SetOpacity(fuchsia::ui::composition::TransformId transform_id, + float opacity) override; + + // |fuchsia::ui::composition::Flatland| + void AddChild( + fuchsia::ui::composition::TransformId parent_transform_id, + fuchsia::ui::composition::TransformId child_transform_id) override; + + // |fuchsia::ui::composition::Flatland| + void RemoveChild( + fuchsia::ui::composition::TransformId parent_transform_id, + fuchsia::ui::composition::TransformId child_transform_id) override; + + // |fuchsia::ui::composition::Flatland| + void SetContent(fuchsia::ui::composition::TransformId transform_id, + fuchsia::ui::composition::ContentId content_id) override; + + // |fuchsia::ui::composition::Flatland| + void SetRootTransform( + fuchsia::ui::composition::TransformId transform_id) override; + + // |fuchsia::ui::composition::Flatland| + void CreateViewport( + fuchsia::ui::composition::ContentId viewport_id, + fuchsia::ui::views::ViewportCreationToken token, + fuchsia::ui::composition::ViewportProperties properties, + fidl::InterfaceRequest + child_view_watcher) override; + + // |fuchsia::ui::composition::Flatland| + void CreateImage( + fuchsia::ui::composition::ContentId image_id, + fuchsia::ui::composition::BufferCollectionImportToken import_token, + uint32_t vmo_index, + fuchsia::ui::composition::ImageProperties properties) override; + + // |fuchsia::ui::composition::Flatland| + void SetImageSampleRegion(fuchsia::ui::composition::ContentId image_id, + fuchsia::math::RectF rect) override; + + // |fuchsia::ui::composition::Flatland| + void SetImageDestinationSize(fuchsia::ui::composition::ContentId image_id, + fuchsia::math::SizeU size) override; + + // |fuchsia::ui::composition::Flatland| + void SetViewportProperties( + fuchsia::ui::composition::ContentId viewport_id, + fuchsia::ui::composition::ViewportProperties properties) override; + + // |fuchsia::ui::composition::Flatland| + void ReleaseTransform( + fuchsia::ui::composition::TransformId transform_id) override; + + // |fuchsia::ui::composition::Flatland| + void ReleaseViewport(fuchsia::ui::composition::ContentId viewport_id, + ReleaseViewportCallback callback) override; + + // |fuchsia::ui::composition::Flatland| + void ReleaseImage(fuchsia::ui::composition::ContentId image_id) override; + + // |fuchsia::ui::composition::Flatland| + void Clear() override; + + // |fuchsia::ui::composition::Flatland| + void SetDebugName(std::string debug_name) override; + + fidl::Binding allocator_binding_; + fidl::Binding flatland_binding_; + GraphBindings graph_bindings_; PresentHandler present_handler_; + FakeGraph pending_graph_; + FakeGraph current_graph_; + + // This map is used to cache parent refs for `AddChild` and `RemoveChild`. + // + // Ideally we would like to map weak(child) -> weak(parent), but std::weak_ptr + // cannot be the Key for an associative container. Instead we key on the raw + // parent pointer and store pair(weak(parent), weak(child)) in the map. + std::unordered_map< + FakeGraph::TransformIdKey, + std::pair, std::weak_ptr>> + parents_map_; + + std::string debug_name_; + FML_DISALLOW_COPY_AND_ASSIGN(FakeFlatland); }; diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.cc b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.cc new file mode 100644 index 0000000000000..ae6bde4855b89 --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.cc @@ -0,0 +1,175 @@ +// 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 "fake_flatland_types.h" + +#include +#include +#include + +#include "flutter/fml/logging.h" + +namespace flutter_runner::testing { +namespace { + +using FakeTransformCache = + std::unordered_map>; + +std::vector> CloneFakeTransformVector( + const std::vector>& transforms, + FakeTransformCache& transform_cache); + +std::shared_ptr CloneFakeContent( + const std::shared_ptr& content) { + if (content == nullptr) { + return nullptr; + } + + if (FakeViewport* viewport = std::get_if(content.get())) { + return std::make_shared(FakeViewport{ + .id = viewport->id, + .viewport_properties = fidl::Clone(viewport->viewport_properties), + .viewport_token = viewport->viewport_token, + .child_view_watcher = viewport->child_view_watcher, + }); + } else if (FakeImage* image = std::get_if(content.get())) { + return std::make_shared(FakeImage{ + .id = image->id, + .image_properties = fidl::Clone(image->image_properties), + .sample_region = image->sample_region, + .destination_size = image->destination_size, + .import_token = image->import_token, + .vmo_index = image->vmo_index, + }); + } else { + FML_UNREACHABLE(); + } +} + +std::shared_ptr CloneFakeTransform( + const std::shared_ptr& transform, + FakeTransformCache& transform_cache) { + if (transform == nullptr) { + return nullptr; + } + + auto found_transform = transform_cache.find(transform.get()); + if (found_transform != transform_cache.end()) { + return found_transform->second; + } + + auto [emplaced_transform, success] = transform_cache.emplace( + transform.get(), std::make_shared(FakeTransform{ + .id = transform->id, + .translation = transform->translation, + .clip_bounds = transform->clip_bounds, + .orientation = transform->orientation, + .opacity = transform->opacity, + .children = CloneFakeTransformVector( + transform->children, transform_cache), + .content = CloneFakeContent(transform->content), + })); + FML_CHECK(success); + + return emplaced_transform->second; +} + +std::vector> CloneFakeTransformVector( + const std::vector>& transforms, + FakeTransformCache& transform_cache) { + std::vector> clones; + for (auto& transform : transforms) { + clones.emplace_back(CloneFakeTransform(transform, transform_cache)); + } + return clones; +} + +} // namespace + +ViewTokenPair ViewTokenPair::New() { + ViewTokenPair token_pair; + auto status = zx::channel::create(0u, &token_pair.view_token.value, + &token_pair.viewport_token.value); + FML_CHECK(status == ZX_OK); + + return token_pair; +} + +BufferCollectionTokenPair BufferCollectionTokenPair::New() { + BufferCollectionTokenPair token_pair; + auto status = zx::eventpair::create(0u, &token_pair.export_token.value, + &token_pair.import_token.value); + FML_CHECK(status == ZX_OK); + + return token_pair; +} + +bool FakeView::operator==(const FakeView& other) const { + return view_token == other.view_token && view_ref == other.view_ref && + view_ref_control == other.view_ref_control && + view_ref_focused == other.view_ref_focused && + focuser == other.focuser && touch_source == other.touch_source && + mouse_source == other.mouse_source && + parent_viewport_watcher == other.parent_viewport_watcher; +} + +bool FakeViewport::operator==(const FakeViewport& other) const { + return id == other.id && viewport_properties == other.viewport_properties && + viewport_token == other.viewport_token && + child_view_watcher == other.child_view_watcher; +} + +bool FakeImage::operator==(const FakeImage& other) const { + return id == other.id && image_properties == other.image_properties && + sample_region == other.sample_region && + destination_size == other.destination_size && + import_token == other.import_token && vmo_index == other.vmo_index; +} + +bool FakeTransform::operator==(const FakeTransform& other) const { + return id == other.id && translation == other.translation && + clip_bounds == other.clip_bounds && orientation == other.orientation && + opacity == other.opacity && children == other.children && + content == other.content; +} + +bool FakeGraph::operator==(const FakeGraph& other) const { + return transform_map == other.transform_map && + content_map == other.content_map && + root_transform == other.root_transform && view == other.view; +} + +void FakeGraph::Clear() { + view.reset(); + root_transform.reset(); + transform_map.clear(); + content_map.clear(); +} + +FakeGraph FakeGraph::Clone() const { + FakeGraph clone; + FakeTransformCache transform_cache; + + for (const auto& [transform_id, transform] : transform_map) { + FML_CHECK(transform); + clone.transform_map.emplace(transform_id, + CloneFakeTransform(transform, transform_cache)); + } + for (const auto& [content_id, content] : content_map) { + FML_CHECK(content); + clone.content_map.emplace(content_id, CloneFakeContent(content)); + } + if (root_transform) { + auto found_transform = transform_cache.find(root_transform.get()); + FML_CHECK(found_transform != transform_cache.end()); + clone.root_transform = found_transform->second; + } + if (view.has_value()) { + clone.view.emplace(view.value()); + } + + return clone; +} + +} // namespace flutter_runner::testing diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.h b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.h new file mode 100644 index 0000000000000..8d1af253f4a90 --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.h @@ -0,0 +1,266 @@ +// 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_FUCHSIA_FLUTTER_TESTS_FAKES_SCENIC_FAKE_FLATLAND_GRAPH_H_ +#define FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_TESTS_FAKES_SCENIC_FAKE_FLATLAND_GRAPH_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "flutter/fml/macros.h" + +inline bool operator==(const fuchsia::math::SizeU& a, + const fuchsia::math::SizeU& b) { + return a.width == b.width && a.height == b.height; +} + +inline bool operator==(const fuchsia::math::Vec& a, + const fuchsia::math::Vec& b) { + return a.x == b.x && a.y == b.y; +} + +inline bool operator==(const fuchsia::math::Rect& a, + const fuchsia::math::Rect& b) { + return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height; +} + +inline bool operator==(const fuchsia::math::RectF& a, + const fuchsia::math::RectF& b) { + return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height; +} + +inline bool operator==(const fuchsia::ui::composition::ContentId& a, + const fuchsia::ui::composition::ContentId& b) { + return a.value == b.value; +} + +inline bool operator==(const fuchsia::ui::composition::TransformId& a, + const fuchsia::ui::composition::TransformId& b) { + return a.value == b.value; +} + +inline bool operator==(const fuchsia::ui::composition::ViewportProperties& a, + const fuchsia::ui::composition::ViewportProperties& b) { + if (a.has_logical_size() != b.has_logical_size()) { + return false; + } + + bool logical_size_equal = true; + if (a.has_logical_size()) { + const fuchsia::math::SizeU& a_logical_size = a.logical_size(); + const fuchsia::math::SizeU& b_logical_size = b.logical_size(); + logical_size_equal = (a_logical_size.width == b_logical_size.width && + a_logical_size.height == b_logical_size.height); + } + + return logical_size_equal; +} + +inline bool operator==(const fuchsia::ui::composition::ImageProperties& a, + const fuchsia::ui::composition::ImageProperties& b) { + if (a.has_size() != b.has_size()) { + return false; + } + + bool size_equal = true; + if (a.has_size()) { + const fuchsia::math::SizeU& a_size = a.size(); + const fuchsia::math::SizeU& b_size = b.size(); + size_equal = + (a_size.width == b_size.width && a_size.height == b_size.height); + } + + return size_equal; +} + +namespace flutter_runner::testing { + +constexpr static fuchsia::ui::composition::TransformId kInvalidTransformId{0}; +constexpr static fuchsia::ui::composition::ContentId kInvalidContentId{0}; + +// Convenience structure which allows clients to easily create a valid +// `ViewCreationToken` / `ViewportCreationToken` pair for use with Flatland +// `CreateView` and `CreateViewport`. +struct ViewTokenPair { + static ViewTokenPair New(); + + fuchsia::ui::views::ViewCreationToken view_token; + fuchsia::ui::views::ViewportCreationToken viewport_token; +}; + +// Convenience structure which allows clients to easily create a valid +// `BufferCollectionExportToken` / `BufferCollectionImportToken` pair for use +// with Flatland `RegisterBufferCollection` and `CreateImage`. +struct BufferCollectionTokenPair { + static BufferCollectionTokenPair New(); + + fuchsia::ui::composition::BufferCollectionExportToken export_token; + fuchsia::ui::composition::BufferCollectionImportToken import_token; +}; + +struct FakeView { + bool operator==(const FakeView& other) const; + + zx_koid_t view_token{}; + zx_koid_t view_ref{}; + zx_koid_t view_ref_control{}; + zx_koid_t view_ref_focused{}; + zx_koid_t focuser{}; + zx_koid_t touch_source{}; + zx_koid_t mouse_source{}; + zx_koid_t parent_viewport_watcher{}; +}; + +struct FakeViewport { + bool operator==(const FakeViewport& other) const; + + constexpr static fuchsia::math::SizeU kDefaultViewportLogicalSize{}; + + fuchsia::ui::composition::ContentId id{kInvalidContentId}; + + fuchsia::ui::composition::ViewportProperties viewport_properties{}; + zx_koid_t viewport_token{}; + zx_koid_t child_view_watcher{}; +}; + +struct FakeImage { + bool operator==(const FakeImage& other) const; + + constexpr static fuchsia::math::SizeU kDefaultImageSize{}; + constexpr static fuchsia::math::RectF kDefaultSampleRegion{}; + constexpr static fuchsia::math::SizeU kDefaultDestinationSize{}; + + fuchsia::ui::composition::ContentId id{kInvalidContentId}; + + fuchsia::ui::composition::ImageProperties image_properties{}; + fuchsia::math::RectF sample_region{kDefaultSampleRegion}; + fuchsia::math::SizeU destination_size{kDefaultDestinationSize}; + + zx_koid_t import_token{}; + uint32_t vmo_index{0}; +}; + +using FakeContent = std::variant; + +struct FakeTransform { + bool operator==(const FakeTransform& other) const; + + constexpr static fuchsia::math::Vec kDefaultTranslation{.x = 0, .y = 0}; + constexpr static fuchsia::math::Rect kDefaultClipBounds{.x = 0, + .y = 0, + .width = 0, + .height = 0}; + constexpr static fuchsia::ui::composition::Orientation kDefaultOrientation{ + fuchsia::ui::composition::Orientation::CCW_0_DEGREES}; + constexpr static float kDefaultOpacity{1.f}; + + fuchsia::ui::composition::TransformId id{kInvalidTransformId}; + + fuchsia::math::Vec translation{kDefaultTranslation}; + fuchsia::math::Rect clip_bounds{kDefaultClipBounds}; + fuchsia::ui::composition::Orientation orientation{kDefaultOrientation}; + float opacity{kDefaultOpacity}; + + std::vector> children; + std::shared_ptr content; +}; + +struct FakeGraph { + using ContentIdKey = decltype(fuchsia::ui::composition::ContentId::value); + using TransformIdKey = decltype(fuchsia::ui::composition::TransformId::value); + + bool operator==(const FakeGraph& other) const; + + void Clear(); + FakeGraph Clone() const; + + std::unordered_map> content_map; + std::unordered_map> + transform_map; + std::shared_ptr root_transform; + std::optional view; +}; + +template +std::pair GetKoids(const ZX& kobj) { + zx_info_handle_basic_t info; + zx_status_t status = + kobj.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), + /*actual_records*/ nullptr, /*avail_records*/ nullptr); + return status == ZX_OK ? std::make_pair(info.koid, info.related_koid) + : std::make_pair(zx_koid_t{}, zx_koid_t{}); +} + +template +std::pair GetKoids( + const fidl::InterfaceHandle& interface_handle) { + return GetKoids(interface_handle.channel()); +} + +template +std::pair GetKoids( + const fidl::InterfaceRequest& interface_request) { + return GetKoids(interface_request.channel()); +} + +template +std::pair GetKoids( + const fidl::InterfacePtr& interface_ptr) { + return GetKoids(interface_ptr.channel()); +} + +template +std::pair GetKoids( + const fidl::Binding& interface_binding) { + return GetKoids(interface_binding.channel()); +} + +inline std::pair GetKoids( + const fuchsia::ui::views::ViewCreationToken& view_token) { + return GetKoids(view_token.value); +} + +inline std::pair GetKoids( + const fuchsia::ui::views::ViewportCreationToken& viewport_token) { + return GetKoids(viewport_token.value); +} + +inline std::pair GetKoids( + const fuchsia::ui::views::ViewRef& view_ref) { + return GetKoids(view_ref.reference); +} + +inline std::pair GetKoids( + const fuchsia::ui::views::ViewRefControl& view_ref_control) { + return GetKoids(view_ref_control.reference); +} + +inline std::pair GetKoids( + const fuchsia::ui::composition::BufferCollectionExportToken& + buffer_collection_token) { + return GetKoids(buffer_collection_token.value); +} + +inline std::pair GetKoids( + const fuchsia::ui::composition::BufferCollectionImportToken& + buffer_collection_token) { + return GetKoids(buffer_collection_token.value); +} + +}; // namespace flutter_runner::testing + +#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_TESTS_FAKES_SCENIC_FAKE_FLATLAND_GRAPH_H_ diff --git a/shell/platform/fuchsia/flutter/tests/flatland_connection_unittests.cc b/shell/platform/fuchsia/flutter/tests/flatland_connection_unittests.cc index f6e81e412c724..c161fe6c02f11 100644 --- a/shell/platform/fuchsia/flutter/tests/flatland_connection_unittests.cc +++ b/shell/platform/fuchsia/flutter/tests/flatland_connection_unittests.cc @@ -46,7 +46,7 @@ class FlatlandConnectionTest : public ::testing::Test { FlatlandConnectionTest() : session_subloop_(loop_.StartNewLoop()), flatland_handle_( - fake_flatland_.Connect(session_subloop_->dispatcher())) {} + fake_flatland_.ConnectFlatland(session_subloop_->dispatcher())) {} ~FlatlandConnectionTest() override = default; async::TestLoop& loop() { return loop_; } diff --git a/shell/platform/fuchsia/flutter/tests/flatland_external_view_embedder_unittests.cc b/shell/platform/fuchsia/flutter/tests/flatland_external_view_embedder_unittests.cc new file mode 100644 index 0000000000000..20983bd49fb9d --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/flatland_external_view_embedder_unittests.cc @@ -0,0 +1,579 @@ +// 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/fuchsia/flutter/flatland_external_view_embedder.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "flutter/flow/embedded_views.h" +#include "flutter/fml/logging.h" +#include "flutter/fml/time/time_delta.h" +#include "flutter/fml/time/time_point.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkSize.h" +#include "third_party/skia/include/core/SkSurface.h" + +#include "fakes/scenic/fake_flatland.h" +#include "fakes/scenic/fake_flatland_types.h" +#include "flutter/shell/platform/fuchsia/flutter/surface_producer.h" + +#include "gmock/gmock.h" // For EXPECT_THAT and matchers +#include "gtest/gtest.h" + +using fuchsia::scenic::scheduling::FramePresentedInfo; +using fuchsia::scenic::scheduling::FuturePresentationTimes; +using fuchsia::scenic::scheduling::PresentReceivedInfo; +using ::testing::_; +using ::testing::AllOf; +using ::testing::ElementsAre; +using ::testing::Eq; +using ::testing::FieldsAre; +using ::testing::IsEmpty; +using ::testing::Matcher; +using ::testing::Pointee; +using ::testing::Property; +using ::testing::SizeIs; +using ::testing::VariantWith; + +namespace flutter_runner::testing { +namespace { + +class FakeSurfaceProducerSurface : public SurfaceProducerSurface { + public: + explicit FakeSurfaceProducerSurface( + fidl::InterfaceRequest + sysmem_token_request, + fuchsia::ui::composition::BufferCollectionImportToken buffer_import_token, + const SkISize& size) + : sysmem_token_request_(std::move(sysmem_token_request)), + buffer_import_token_(std::move(buffer_import_token)), + surface_(SkSurface::MakeNull(size.width(), size.height())) { + zx_status_t acquire_status = zx::event::create(0, &acquire_fence_); + if (acquire_status != ZX_OK) { + FML_LOG(ERROR) + << "FakeSurfaceProducerSurface: Failed to create acquire event"; + } + zx_status_t release_status = zx::event::create(0, &release_fence_); + if (release_status != ZX_OK) { + FML_LOG(ERROR) + << "FakeSurfaceProducerSurface: Failed to create release event"; + } + } + ~FakeSurfaceProducerSurface() override {} + + bool IsValid() const override { return true; } + + SkISize GetSize() const override { + return SkISize::Make(surface_->width(), surface_->height()); + } + + void SetImageId(uint32_t image_id) override { image_id_ = image_id; } + uint32_t GetImageId() override { return image_id_; } + + sk_sp GetSkiaSurface() const override { return surface_; } + + fuchsia::ui::composition::BufferCollectionImportToken + GetBufferCollectionImportToken() override { + return std::move(buffer_import_token_); + } + + zx::event GetAcquireFence() override { + zx::event fence; + acquire_fence_.duplicate(ZX_RIGHT_SAME_RIGHTS, &fence); + return fence; + } + + zx::event GetReleaseFence() override { + zx::event fence; + release_fence_.duplicate(ZX_RIGHT_SAME_RIGHTS, &fence); + return fence; + } + + void SetReleaseImageCallback( + ReleaseImageCallback release_image_callback) override {} + + size_t AdvanceAndGetAge() override { return 0; } + bool FlushSessionAcquireAndReleaseEvents() override { return true; } + void SignalWritesFinished( + const std::function& on_writes_committed) override {} + + private: + fidl::InterfaceRequest + sysmem_token_request_; + fuchsia::ui::composition::BufferCollectionImportToken buffer_import_token_; + zx::event acquire_fence_; + zx::event release_fence_; + + sk_sp surface_; + uint32_t image_id_{0}; +}; + +class FakeSurfaceProducer : public SurfaceProducer { + public: + explicit FakeSurfaceProducer( + fuchsia::ui::composition::AllocatorHandle flatland_allocator) + : flatland_allocator_(flatland_allocator.Bind()) {} + ~FakeSurfaceProducer() override = default; + + std::unique_ptr ProduceSurface( + const SkISize& size) override { + auto [buffer_export_token, buffer_import_token] = + BufferCollectionTokenPair::New(); + fuchsia::sysmem::BufferCollectionTokenHandle sysmem_token; + auto sysmem_token_request = sysmem_token.NewRequest(); + + fuchsia::ui::composition::RegisterBufferCollectionArgs + buffer_collection_args; + buffer_collection_args.set_export_token(std::move(buffer_export_token)); + buffer_collection_args.set_buffer_collection_token(std::move(sysmem_token)); + buffer_collection_args.set_usage( + fuchsia::ui::composition::RegisterBufferCollectionUsage::DEFAULT); + flatland_allocator_->RegisterBufferCollection( + std::move(buffer_collection_args), + [](fuchsia::ui::composition::Allocator_RegisterBufferCollection_Result + result) { + if (result.is_err()) { + FAIL() + << "fuhsia::ui::composition::RegisterBufferCollection error: " + << static_cast(result.err()); + } + }); + + return std::make_unique( + std::move(sysmem_token_request), std::move(buffer_import_token), size); + } + + void SubmitSurfaces( + std::vector> surfaces) override {} + + fuchsia::ui::composition::AllocatorPtr flatland_allocator_; +}; + +Matcher IsImageProperties( + const fuchsia::math::SizeU& size) { + return AllOf( + Property("has_size", &fuchsia::ui::composition::ImageProperties::has_size, + true), + Property("size", &fuchsia::ui::composition::ImageProperties::size, size)); +} + +Matcher IsViewportProperties( + const fuchsia::math::SizeU& logical_size) { + return AllOf( + Property("has_logical_size", + &fuchsia::ui::composition::ViewportProperties::has_logical_size, + true), + Property("logical_size", + &fuchsia::ui::composition::ViewportProperties::logical_size, + logical_size)); +} + +Matcher IsEmptyGraph() { + return FieldsAre(IsEmpty(), IsEmpty(), Eq(nullptr), Eq(std::nullopt)); +} + +Matcher IsFlutterGraph( + const fuchsia::ui::composition::ParentViewportWatcherPtr& + parent_viewport_watcher, + const fuchsia::ui::views::ViewportCreationToken& viewport_creation_token, + const fuchsia::ui::views::ViewRef& view_ref, + std::vector>> layer_matchers = {}) { + auto viewport_token_koids = GetKoids(viewport_creation_token); + auto view_ref_koids = GetKoids(view_ref); + auto watcher_koids = GetKoids(parent_viewport_watcher); + + return FieldsAre( + /*content_map*/ _, /*transform_map*/ _, + Pointee(FieldsAre( + /*id*/ _, FakeTransform::kDefaultTranslation, + FakeTransform::kDefaultClipBounds, FakeTransform::kDefaultOrientation, + FakeTransform::kDefaultOpacity, ElementsAreArray(layer_matchers), + /*content*/ Eq(nullptr))), + Eq(FakeView{ + .view_token = viewport_token_koids.second, + .view_ref = view_ref_koids.first, + .view_ref_control = view_ref_koids.second, + .view_ref_focused = ZX_KOID_INVALID, + .focuser = ZX_KOID_INVALID, + .touch_source = ZX_KOID_INVALID, + .mouse_source = ZX_KOID_INVALID, + .parent_viewport_watcher = watcher_koids.second, + })); +} + +Matcher> IsImageLayer( + const fuchsia::math::SizeU& layer_size) { + return Pointee(FieldsAre( + /*id*/ _, FakeTransform::kDefaultTranslation, + FakeTransform::kDefaultClipBounds, FakeTransform::kDefaultOrientation, + FakeTransform::kDefaultOpacity, IsEmpty(), + Pointee(VariantWith(FieldsAre( + /*id*/ _, IsImageProperties(layer_size), + FakeImage::kDefaultSampleRegion, layer_size, + /*buffer_import_token*/ _, /*vmo_index*/ 0))))); +} + +Matcher> IsViewportLayer( + const fuchsia::ui::views::ViewCreationToken& view_token, + const fuchsia::math::SizeU& view_logical_size, + const fuchsia::math::Vec& view_transform) { + return Pointee( + FieldsAre(_ /* id */, view_transform, FakeTransform::kDefaultClipBounds, + FakeTransform::kDefaultOrientation, + FakeTransform::kDefaultOpacity, IsEmpty(), + Pointee(VariantWith(FieldsAre( + /* id */ _, IsViewportProperties(view_logical_size), + /* viewport_token */ GetKoids(view_token).second, + /* child_view_watcher */ _))))); +} + +fuchsia::ui::composition::OnNextFrameBeginValues WithPresentCredits( + uint32_t additional_present_credits) { + fuchsia::ui::composition::OnNextFrameBeginValues values; + + values.set_additional_present_credits(additional_present_credits); + return values; +} + +void DrawSimpleFrame(FlatlandExternalViewEmbedder& external_view_embedder, + SkISize frame_size, + float frame_dpr, + std::function draw_callback) { + external_view_embedder.BeginFrame(frame_size, nullptr, frame_dpr, nullptr); + { + SkCanvas* root_canvas = external_view_embedder.GetRootCanvas(); + external_view_embedder.PostPrerollAction(nullptr); + draw_callback(root_canvas); + } + external_view_embedder.EndFrame(false, nullptr); + flutter::SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; + external_view_embedder.SubmitFrame( + nullptr, std::make_unique( + nullptr, std::move(framebuffer_info), + [](const flutter::SurfaceFrame& surface_frame, + SkCanvas* canvas) { return true; })); +} + +void DrawFrameWithView(FlatlandExternalViewEmbedder& external_view_embedder, + SkISize frame_size, + float frame_dpr, + int view_id, + flutter::EmbeddedViewParams& view_params, + std::function background_draw_callback, + std::function overlay_draw_callback) { + external_view_embedder.BeginFrame(frame_size, nullptr, frame_dpr, nullptr); + { + SkCanvas* root_canvas = external_view_embedder.GetRootCanvas(); + external_view_embedder.PrerollCompositeEmbeddedView( + view_id, std::make_unique(view_params)); + external_view_embedder.PostPrerollAction(nullptr); + background_draw_callback(root_canvas); + SkCanvas* overlay_canvas = + external_view_embedder.CompositeEmbeddedView(view_id); + overlay_draw_callback(overlay_canvas); + } + external_view_embedder.EndFrame(false, nullptr); + flutter::SurfaceFrame::FramebufferInfo framebuffer_info; + framebuffer_info.supports_readback = true; + external_view_embedder.SubmitFrame( + nullptr, std::make_unique( + nullptr, std::move(framebuffer_info), + [](const flutter::SurfaceFrame& surface_frame, + SkCanvas* canvas) { return true; })); +} + +}; // namespace + +class FlatlandExternalViewEmbedderTest : public ::testing::Test { + protected: + FlatlandExternalViewEmbedderTest() + : session_subloop_(loop_.StartNewLoop()), + fake_surface_producer_(CreateFlatlandAllocator()), + flatland_connection_(CreateFlatlandConnection()) {} + ~FlatlandExternalViewEmbedderTest() override = default; + + async::TestLoop& loop() { return loop_; } + + FakeSurfaceProducer& fake_surface_producer() { + return fake_surface_producer_; + } + + FakeFlatland& fake_flatland() { return fake_flatland_; } + + FlatlandConnection& flatland_connection() { return flatland_connection_; } + + private: + fuchsia::ui::composition::AllocatorHandle CreateFlatlandAllocator() { + FML_CHECK(!fake_flatland_.is_allocator_connected()); + fuchsia::ui::composition::AllocatorHandle flatland_allocator = + fake_flatland_.ConnectAllocator(session_subloop_->dispatcher()); + + return flatland_allocator; + } + + FlatlandConnection CreateFlatlandConnection() { + FML_CHECK(!fake_flatland_.is_flatland_connected()); + fuchsia::ui::composition::FlatlandHandle flatland = + fake_flatland_.ConnectFlatland(session_subloop_->dispatcher()); + + auto test_name = + ::testing::UnitTest::GetInstance()->current_test_info()->name(); + return FlatlandConnection( + std::move(test_name), std::move(flatland), []() { FAIL(); }, + [](auto...) {}, 1, fml::TimeDelta::Zero()); + } + + // Primary loop and subloop for the FakeFlatland instance to process its + // messages. The subloop allocates it's own zx_port_t, allowing us to use a + // separate port for each end of the message channel, rather than sharing a + // single one. Dual ports allow messages and responses to be intermingled, + // which is how production code behaves; this improves test realism. + async::TestLoop loop_; + std::unique_ptr session_subloop_; + + FakeFlatland fake_flatland_; + FakeSurfaceProducer fake_surface_producer_; + + FlatlandConnection flatland_connection_; +}; + +TEST_F(FlatlandExternalViewEmbedderTest, RootScene) { + fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher; + fuchsia::ui::views::ViewportCreationToken viewport_creation_token; + fuchsia::ui::views::ViewCreationToken view_creation_token; + fuchsia::ui::views::ViewRef view_ref; + auto view_creation_token_status = zx::channel::create( + 0u, &viewport_creation_token.value, &view_creation_token.value); + ASSERT_EQ(view_creation_token_status, ZX_OK); + auto view_ref_pair = scenic::ViewRefPair::New(); + view_ref_pair.view_ref.Clone(&view_ref); + + FlatlandExternalViewEmbedder external_view_embedder( + std::move(view_creation_token), + fuchsia::ui::views::ViewIdentityOnCreation{ + .view_ref = std::move(view_ref_pair.view_ref), + .view_ref_control = std::move(view_ref_pair.control_ref), + }, + fuchsia::ui::composition::ViewBoundProtocols{}, + parent_viewport_watcher.NewRequest(), flatland_connection(), + fake_surface_producer()); + EXPECT_THAT(fake_flatland().graph(), IsEmptyGraph()); + + // Pump the loop; the graph should still be empty because nothing called + // `Present` yet. + loop().RunUntilIdle(); + EXPECT_THAT(fake_flatland().graph(), IsEmptyGraph()); + + // Pump the loop; the contents of the initial `Present` should be processed. + flatland_connection().Present(); + loop().RunUntilIdle(); + EXPECT_THAT(fake_flatland().graph(), + IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, + view_ref)); +} + +TEST_F(FlatlandExternalViewEmbedderTest, SimpleScene) { + fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher; + fuchsia::ui::views::ViewportCreationToken viewport_creation_token; + fuchsia::ui::views::ViewCreationToken view_creation_token; + fuchsia::ui::views::ViewRef view_ref; + auto view_creation_token_status = zx::channel::create( + 0u, &viewport_creation_token.value, &view_creation_token.value); + ASSERT_EQ(view_creation_token_status, ZX_OK); + auto view_ref_pair = scenic::ViewRefPair::New(); + view_ref_pair.view_ref.Clone(&view_ref); + + // Create the `FlatlandExternalViewEmbedder` and pump the message loop until + // the initial scene graph is setup. + FlatlandExternalViewEmbedder external_view_embedder( + std::move(view_creation_token), + fuchsia::ui::views::ViewIdentityOnCreation{ + .view_ref = std::move(view_ref_pair.view_ref), + .view_ref_control = std::move(view_ref_pair.control_ref), + }, + fuchsia::ui::composition::ViewBoundProtocols{}, + parent_viewport_watcher.NewRequest(), flatland_connection(), + fake_surface_producer()); + flatland_connection().Present(); + loop().RunUntilIdle(); + fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u)); + loop().RunUntilIdle(); + EXPECT_THAT(fake_flatland().graph(), + IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, + view_ref)); + + // Draw the scene. The scene graph shouldn't change yet. + const SkISize frame_size_signed = SkISize::Make(512, 512); + const fuchsia::math::SizeU frame_size{ + static_cast(frame_size_signed.width()), + static_cast(frame_size_signed.height())}; + DrawSimpleFrame( + external_view_embedder, frame_size_signed, 1.f, [](SkCanvas* canvas) { + const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), + canvas->imageInfo().height()); + SkPaint rect_paint; + rect_paint.setColor(SK_ColorGREEN); + canvas->translate(canvas_size.width() / 4.f, + canvas_size.height() / 2.f); + canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f, + canvas_size.height() / 32.f), + rect_paint); + }); + EXPECT_THAT(fake_flatland().graph(), + IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, + view_ref)); + + // Pump the message loop. The scene updates should propagate to flatland. + loop().RunUntilIdle(); + EXPECT_THAT( + fake_flatland().graph(), + IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, view_ref, + /*layers*/ + {IsImageLayer(frame_size)})); +} + +TEST_F(FlatlandExternalViewEmbedderTest, SceneWithOneView) { + fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher; + fuchsia::ui::views::ViewportCreationToken viewport_creation_token; + fuchsia::ui::views::ViewCreationToken view_creation_token; + fuchsia::ui::views::ViewRef view_ref; + auto view_creation_token_status = zx::channel::create( + 0u, &viewport_creation_token.value, &view_creation_token.value); + ASSERT_EQ(view_creation_token_status, ZX_OK); + auto view_ref_pair = scenic::ViewRefPair::New(); + view_ref_pair.view_ref.Clone(&view_ref); + + // Create the `FlatlandExternalViewEmbedder` and pump the message loop until + // the initial scene graph is setup. + FlatlandExternalViewEmbedder external_view_embedder( + std::move(view_creation_token), + fuchsia::ui::views::ViewIdentityOnCreation{ + .view_ref = std::move(view_ref_pair.view_ref), + .view_ref_control = std::move(view_ref_pair.control_ref), + }, + fuchsia::ui::composition::ViewBoundProtocols{}, + parent_viewport_watcher.NewRequest(), flatland_connection(), + fake_surface_producer()); + flatland_connection().Present(); + loop().RunUntilIdle(); + fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u)); + loop().RunUntilIdle(); + EXPECT_THAT(fake_flatland().graph(), + IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, + view_ref)); + + // Create the view before drawing the scene. + const SkSize child_view_size_signed = SkSize::Make(256.f, 512.f); + const fuchsia::math::SizeU child_view_size{ + static_cast(child_view_size_signed.width()), + static_cast(child_view_size_signed.height())}; + auto [child_view_token, child_viewport_token] = ViewTokenPair::New(); + const uint32_t child_view_id = child_viewport_token.value.get(); + flutter::EmbeddedViewParams child_view_params( + SkMatrix::I(), child_view_size_signed, flutter::MutatorsStack()); + external_view_embedder.CreateView( + child_view_id, []() {}, + [](fuchsia::ui::composition::ContentId, + fuchsia::ui::composition::ChildViewWatcherPtr) {}); + + // Draw the scene. The scene graph shouldn't change yet. + const SkISize frame_size_signed = SkISize::Make(512, 512); + const fuchsia::math::SizeU frame_size{ + static_cast(frame_size_signed.width()), + static_cast(frame_size_signed.height())}; + DrawFrameWithView( + external_view_embedder, frame_size_signed, 1.f, child_view_id, + child_view_params, + [](SkCanvas* canvas) { + const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), + canvas->imageInfo().height()); + SkPaint rect_paint; + rect_paint.setColor(SK_ColorGREEN); + canvas->translate(canvas_size.width() / 4.f, + canvas_size.height() / 2.f); + canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f, + canvas_size.height() / 32.f), + rect_paint); + }, + [](SkCanvas* canvas) { + const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), + canvas->imageInfo().height()); + SkPaint rect_paint; + rect_paint.setColor(SK_ColorRED); + canvas->translate(canvas_size.width() * 3.f / 4.f, + canvas_size.height() / 2.f); + canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f, + canvas_size.height() / 32.f), + rect_paint); + }); + EXPECT_THAT(fake_flatland().graph(), + IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, + view_ref)); + + // Pump the message loop. The scene updates should propagate to flatland. + loop().RunUntilIdle(); + fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u)); + loop().RunUntilIdle(); + EXPECT_THAT( + fake_flatland().graph(), + IsFlutterGraph( + parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/ + {IsImageLayer(frame_size), + IsViewportLayer(child_view_token, child_view_size, {0, 0}), + IsImageLayer(frame_size)})); + + // Destroy the view. The scene graph shouldn't change yet. + external_view_embedder.DestroyView( + child_view_id, [](fuchsia::ui::composition::ContentId) {}); + EXPECT_THAT( + fake_flatland().graph(), + IsFlutterGraph( + parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/ + {IsImageLayer(frame_size), + IsViewportLayer(child_view_token, child_view_size, {0, 0}), + IsImageLayer(frame_size)})); + + // Draw another frame without the view. The scene graph shouldn't change yet. + DrawSimpleFrame( + external_view_embedder, frame_size_signed, 1.f, [](SkCanvas* canvas) { + const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), + canvas->imageInfo().height()); + SkPaint rect_paint; + rect_paint.setColor(SK_ColorGREEN); + canvas->translate(canvas_size.width() / 4.f, + canvas_size.height() / 2.f); + canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f, + canvas_size.height() / 32.f), + rect_paint); + }); + EXPECT_THAT( + fake_flatland().graph(), + IsFlutterGraph( + parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/ + {IsImageLayer(frame_size), + IsViewportLayer(child_view_token, child_view_size, {0, 0}), + IsImageLayer(frame_size)})); + + // Pump the message loop. The scene updates should propagate to flatland. + loop().RunUntilIdle(); + EXPECT_THAT(fake_flatland().graph(), + IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, + view_ref, /*layers*/ + {IsImageLayer(frame_size)})); +} + +} // namespace flutter_runner::testing diff --git a/shell/platform/fuchsia/flutter/tests/gfx_external_view_embedder_unittests.cc b/shell/platform/fuchsia/flutter/tests/gfx_external_view_embedder_unittests.cc index 6e682ba0fa3d4..47eadc3e1479c 100644 --- a/shell/platform/fuchsia/flutter/tests/gfx_external_view_embedder_unittests.cc +++ b/shell/platform/fuchsia/flutter/tests/gfx_external_view_embedder_unittests.cc @@ -402,9 +402,10 @@ void DrawSimpleFrame(GfxExternalViewEmbedder& external_view_embedder, draw_callback(root_canvas); } external_view_embedder.EndFrame(false, nullptr); + flutter::SurfaceFrame::FramebufferInfo framebuffer_info; external_view_embedder.SubmitFrame( nullptr, std::make_unique( - nullptr, true, + nullptr, framebuffer_info, [](const flutter::SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; })); } @@ -428,9 +429,10 @@ void DrawFrameWithView(GfxExternalViewEmbedder& external_view_embedder, overlay_draw_callback(overlay_canvas); } external_view_embedder.EndFrame(false, nullptr); + flutter::SurfaceFrame::FramebufferInfo framebuffer_info; external_view_embedder.SubmitFrame( nullptr, std::make_unique( - nullptr, true, + nullptr, framebuffer_info, [](const flutter::SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; })); } diff --git a/shell/platform/fuchsia/flutter/vsync_waiter.cc b/shell/platform/fuchsia/flutter/vsync_waiter.cc index c66e12690e310..aa7d3c2ebfe0e 100644 --- a/shell/platform/fuchsia/flutter/vsync_waiter.cc +++ b/shell/platform/fuchsia/flutter/vsync_waiter.cc @@ -30,10 +30,17 @@ VsyncWaiter::VsyncWaiter(AwaitVsyncCallback await_vsync_callback, weak_factory_(this) { fire_callback_callback_ = [this](fml::TimePoint frame_start, fml::TimePoint frame_end) { - // Note: It is VERY important to set |pause_secondary_tasks| to false, else - // Animator will almost immediately crash on Fuchsia. - // FML_LOG(INFO) << "CRASH:: VsyncWaiter about to FireCallback"; - FireCallback(frame_start, frame_end, /*pause_secondary_tasks=*/false); + task_runners_.GetUITaskRunner()->PostTaskForTime( + [frame_start, frame_end, weak_this = weak_ui_]() { + if (weak_this) { + // Note: It is VERY important to set |pause_secondary_tasks| to + // false, else Animator will almost immediately crash on Fuchsia. + // FML_LOG(INFO) << "CRASH:: VsyncWaiter about to FireCallback"; + weak_this->FireCallback(frame_start, frame_end, + /*pause_secondary_tasks*/ false); + } + }, + frame_start); }; // Generate a WeakPtrFactory for use with the UI thread. This does not need @@ -41,8 +48,9 @@ VsyncWaiter::VsyncWaiter(AwaitVsyncCallback await_vsync_callback, // thread so we have ordering guarantees (see ::AwaitVSync()) fml::TaskRunner::RunNowOrPostTask( task_runners_.GetUITaskRunner(), fml::MakeCopyable([this]() mutable { - this->weak_factory_ui_ = + weak_factory_ui_ = std::make_unique>(this); + weak_ui_ = weak_factory_ui_->GetWeakPtr(); })); } diff --git a/shell/platform/fuchsia/flutter/vsync_waiter.h b/shell/platform/fuchsia/flutter/vsync_waiter.h index 1e92064c8735b..ce46b90d362a7 100644 --- a/shell/platform/fuchsia/flutter/vsync_waiter.h +++ b/shell/platform/fuchsia/flutter/vsync_waiter.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_VSYNC_WAITER_H_ -#define FLUTTER_SHELL_PLATFORM_FUCHSIA_VSYNC_WAITER_H_ +#ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_VSYNC_WAITER_H_ +#define FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_VSYNC_WAITER_H_ #include @@ -46,6 +46,7 @@ class VsyncWaiter final : public flutter::VsyncWaiter { AwaitVsyncForSecondaryCallbackCallback await_vsync_for_secondary_callback_callback_; + fml::WeakPtr weak_ui_; std::unique_ptr> weak_factory_ui_; fml::WeakPtrFactory weak_factory_; @@ -54,4 +55,4 @@ class VsyncWaiter final : public flutter::VsyncWaiter { } // namespace flutter_runner -#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_VSYNC_WAITER_H_ +#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_VSYNC_WAITER_H_ diff --git a/shell/platform/fuchsia/flutter/vsync_waiter_unittest.cc b/shell/platform/fuchsia/flutter/vsync_waiter_unittest.cc new file mode 100644 index 0000000000000..f01a2a02c90c4 --- /dev/null +++ b/shell/platform/fuchsia/flutter/vsync_waiter_unittest.cc @@ -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 + +#include + +#include "flutter/fml/task_runner.h" +#include "flutter/shell/common/thread_host.h" +#include "fml/make_copyable.h" +#include "fml/message_loop.h" +#include "fml/synchronization/waitable_event.h" +#include "fml/time/time_delta.h" +#include "fml/time/time_point.h" +#include "vsync_waiter.h" + +namespace flutter_runner { + +TEST(VSyncWaiterFuchsia, FrameScheduledForStartTime) { + using flutter::ThreadHost; + std::string prefix = "vsync_waiter_test"; + + fml::MessageLoop::EnsureInitializedForCurrentThread(); + auto platform_task_runner = fml::MessageLoop::GetCurrent().GetTaskRunner(); + + ThreadHost thread_host = + ThreadHost(prefix, flutter::ThreadHost::Type::RASTER | + flutter::ThreadHost::Type::UI | + flutter::ThreadHost::Type::IO); + const flutter::TaskRunners task_runners( + prefix, // Dart thread labels + platform_task_runner, // platform + thread_host.raster_thread->GetTaskRunner(), // raster + thread_host.ui_thread->GetTaskRunner(), // ui + thread_host.io_thread->GetTaskRunner() // io + ); + + // await vsync will invoke the callback right away, but vsync waiter will post + // the task for frame_start time. + VsyncWaiter vsync_waiter( + [](FireCallbackCallback callback) { + const auto now = fml::TimePoint::Now(); + const auto frame_start = now + fml::TimeDelta::FromMilliseconds(20); + const auto frame_end = now + fml::TimeDelta::FromMilliseconds(36); + callback(frame_start, frame_end); + }, + /*secondary callback*/ nullptr, task_runners); + + fml::AutoResetWaitableEvent latch; + task_runners.GetUITaskRunner()->PostTask([&]() { + vsync_waiter.AsyncWaitForVsync( + [&](std::unique_ptr recorder) { + const auto now = fml::TimePoint::Now(); + EXPECT_GT(now, recorder->GetVsyncStartTime()); + latch.Signal(); + }); + }); + + latch.Wait(); +} + +} // namespace flutter_runner diff --git a/shell/platform/fuchsia/flutter/vulkan_surface.cc b/shell/platform/fuchsia/flutter/vulkan_surface.cc index b7be38702a0e8..4b9a1e0c29839 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface.cc +++ b/shell/platform/fuchsia/flutter/vulkan_surface.cc @@ -86,7 +86,7 @@ bool VulkanSurface::CreateVulkanImage(vulkan::VulkanProvider& vulkan_provider, return false; } - out_vulkan_image->vk_image = { + out_vulkan_image->vk_image = vulkan::VulkanHandle{ vk_image, [&vulkan_provider = vulkan_provider](VkImage image) { vulkan_provider.vk().DestroyImage(vulkan_provider.vk_device(), image, NULL); @@ -290,11 +290,12 @@ bool VulkanSurface::AllocateDeviceMemory( return false; } - collection_ = {collection, [&vulkan_provider = vulkan_provider_]( - VkBufferCollectionFUCHSIAX collection) { - vulkan_provider.vk().DestroyBufferCollectionFUCHSIAX( - vulkan_provider.vk_device(), collection, nullptr); - }}; + collection_ = vulkan::VulkanHandle{ + collection, [&vulkan_provider = vulkan_provider_]( + VkBufferCollectionFUCHSIAX collection) { + vulkan_provider.vk().DestroyBufferCollectionFUCHSIAX( + vulkan_provider.vk_device(), collection, nullptr); + }}; VulkanImage vulkan_image; LOG_AND_RETURN(!CreateVulkanImage(vulkan_provider_, size, &vulkan_image), @@ -339,11 +340,12 @@ bool VulkanSurface::AllocateDeviceMemory( return false; } - vk_memory_ = {vk_memory, - [&vulkan_provider = vulkan_provider_](VkDeviceMemory memory) { - vulkan_provider.vk().FreeMemory(vulkan_provider.vk_device(), - memory, NULL); - }}; + vk_memory_ = vulkan::VulkanHandle{ + vk_memory, + [&vulkan_provider = vulkan_provider_](VkDeviceMemory memory) { + vulkan_provider.vk().FreeMemory(vulkan_provider.vk_device(), memory, + NULL); + }}; vk_memory_info_ = allocation_info; } diff --git a/shell/platform/fuchsia/flutter/vulkan_surface_producer.h b/shell/platform/fuchsia/flutter/vulkan_surface_producer.h index dc0092587f7a6..9933f6b781f77 100644 --- a/shell/platform/fuchsia/flutter/vulkan_surface_producer.h +++ b/shell/platform/fuchsia/flutter/vulkan_surface_producer.h @@ -56,6 +56,14 @@ class VulkanSurfaceProducer final : public SurfaceProducer, bool TransitionSurfacesToExternal( const std::vector>& surfaces); + // Keep track of the last time we produced a surface. This is used to + // determine whether it is safe to shrink |surface_pool_| or not. + zx::time last_produce_time_ = async::Now(async_get_default_dispatcher()); + + // Disallow copy and assignment. + VulkanSurfaceProducer(const VulkanSurfaceProducer&) = delete; + VulkanSurfaceProducer& operator=(const VulkanSurfaceProducer&) = delete; + // Note: the order here is very important. The proctable must be destroyed // last because it contains the function pointers for VkDestroyDevice and // VkDestroyInstance. @@ -66,14 +74,8 @@ class VulkanSurfaceProducer final : public SurfaceProducer, std::unique_ptr surface_pool_; bool valid_ = false; - // Keep track of the last time we produced a surface. This is used to - // determine whether it is safe to shrink |surface_pool_| or not. - zx::time last_produce_time_ = async::Now(async_get_default_dispatcher()); + // WeakPtrFactory must be the last member. fml::WeakPtrFactory weak_factory_{this}; - - // Disallow copy and assignment. - VulkanSurfaceProducer(const VulkanSurfaceProducer&) = delete; - VulkanSurfaceProducer& operator=(const VulkanSurfaceProducer&) = delete; }; } // namespace flutter_runner diff --git a/shell/platform/glfw/client_wrapper/include/flutter/flutter_window_controller.h b/shell/platform/glfw/client_wrapper/include/flutter/flutter_window_controller.h index 7c1e04ae0b13a..60bc4c151e9aa 100644 --- a/shell/platform/glfw/client_wrapper/include/flutter/flutter_window_controller.h +++ b/shell/platform/glfw/client_wrapper/include/flutter/flutter_window_controller.h @@ -63,7 +63,7 @@ class FlutterWindowController : public PluginRegistry { // application to be run. // // The |arguments| are passed to the Flutter engine. See: - // https://github.com/flutter/engine/blob/master/shell/common/switches.h for + // https://github.com/flutter/engine/blob/main/shell/common/switches.h for // for details. Not all arguments will apply to desktop. // // The |aot_library_path| is the path to the libapp.so file for the Flutter diff --git a/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h b/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h index fd96dd4becc27..b654063995667 100644 --- a/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h +++ b/shell/platform/glfw/client_wrapper/testing/stub_flutter_glfw_api.h @@ -98,7 +98,7 @@ class StubFlutterGlfwApi { class ScopedStubFlutterGlfwApi { public: // Calls SetTestFlutterStub with |stub|. - ScopedStubFlutterGlfwApi(std::unique_ptr stub); + explicit ScopedStubFlutterGlfwApi(std::unique_ptr stub); // Restores the previous test stub. ~ScopedStubFlutterGlfwApi(); diff --git a/shell/platform/glfw/public/flutter_glfw.h b/shell/platform/glfw/public/flutter_glfw.h index 5b26cf7481976..30d3261e8b2a6 100644 --- a/shell/platform/glfw/public/flutter_glfw.h +++ b/shell/platform/glfw/public/flutter_glfw.h @@ -51,7 +51,7 @@ typedef struct { const char* aot_library_path; // The switches to pass to the Flutter engine. // - // See: https://github.com/flutter/engine/blob/master/shell/common/switches.h + // See: https://github.com/flutter/engine/blob/main/shell/common/switches.h // for details. Not all arguments will apply to desktop. const char** switches; // The number of elements in |switches|. diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn index 20adc1e31d068..0c4b703683af9 100644 --- a/shell/platform/linux/BUILD.gn +++ b/shell/platform/linux/BUILD.gn @@ -193,6 +193,7 @@ executable("flutter_linux_unittests") { "fl_method_codec_test.cc", "fl_method_response_test.cc", "fl_pixel_buffer_texture_test.cc", + "fl_plugin_registrar_test.cc", "fl_standard_message_codec_test.cc", "fl_standard_method_codec_test.cc", "fl_string_codec_test.cc", @@ -200,10 +201,13 @@ executable("flutter_linux_unittests") { "fl_texture_registrar_test.cc", "fl_value_test.cc", "testing/fl_test.cc", + "testing/mock_binary_messenger_response_handle.cc", "testing/mock_engine.cc", "testing/mock_epoxy.cc", + "testing/mock_plugin_registrar.cc", "testing/mock_renderer.cc", "testing/mock_text_input_plugin.cc", + "testing/mock_texture_registrar.cc", ] public_configs = [ "//flutter:config" ] diff --git a/shell/platform/linux/fl_accessible_node.cc b/shell/platform/linux/fl_accessible_node.cc index 8b0905adafc30..21cdb24de985a 100644 --- a/shell/platform/linux/fl_accessible_node.cc +++ b/shell/platform/linux/fl_accessible_node.cc @@ -112,8 +112,9 @@ static gboolean has_action(FlutterSemanticsAction actions, // Gets the nth action. static ActionData* get_action(FlAccessibleNode* self, gint index) { - if (index < 0 || static_cast(index) >= self->actions->len) + if (index < 0 || static_cast(index) >= self->actions->len) { return nullptr; + } return static_cast(g_ptr_array_index(self->actions, index)); } diff --git a/shell/platform/linux/fl_binary_messenger.cc b/shell/platform/linux/fl_binary_messenger.cc index 4c1363379aa98..92c3f69dc29e2 100644 --- a/shell/platform/linux/fl_binary_messenger.cc +++ b/shell/platform/linux/fl_binary_messenger.cc @@ -10,10 +10,27 @@ #include +// Added here to stop the compiler from optimizing this function away. +G_MODULE_EXPORT GType fl_binary_messenger_get_type(); + G_DEFINE_QUARK(fl_binary_messenger_codec_error_quark, fl_binary_messenger_codec_error) -struct _FlBinaryMessenger { +G_DECLARE_FINAL_TYPE(FlBinaryMessengerImpl, + fl_binary_messenger_impl, + FL, + BINARY_MESSENGER_IMPL, + GObject) + +G_DECLARE_FINAL_TYPE(FlBinaryMessengerResponseHandleImpl, + fl_binary_messenger_response_handle_impl, + FL, + BINARY_MESSENGER_RESPONSE_HANDLE_IMPL, + FlBinaryMessengerResponseHandle) + +G_DEFINE_INTERFACE(FlBinaryMessenger, fl_binary_messenger, G_TYPE_OBJECT) + +struct _FlBinaryMessengerImpl { GObject parent_instance; FlEngine* engine; @@ -22,26 +39,47 @@ struct _FlBinaryMessenger { GHashTable* platform_message_handlers; }; -G_DEFINE_TYPE(FlBinaryMessenger, fl_binary_messenger, G_TYPE_OBJECT) +static void fl_binary_messenger_impl_iface_init( + FlBinaryMessengerInterface* iface); -struct _FlBinaryMessengerResponseHandle { - GObject parent_instance; +G_DEFINE_TYPE_WITH_CODE( + FlBinaryMessengerImpl, + fl_binary_messenger_impl, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(fl_binary_messenger_get_type(), + fl_binary_messenger_impl_iface_init)) + +static void fl_binary_messenger_response_handle_class_init( + FlBinaryMessengerResponseHandleClass* klass) {} + +G_DEFINE_TYPE(FlBinaryMessengerResponseHandle, + fl_binary_messenger_response_handle, + G_TYPE_OBJECT) + +static void fl_binary_messenger_response_handle_init( + FlBinaryMessengerResponseHandle* self) {} + +struct _FlBinaryMessengerResponseHandleImpl { + FlBinaryMessengerResponseHandle parent_instance; // Messenger sending response on. - FlBinaryMessenger* messenger; + FlBinaryMessengerImpl* messenger; // Handle to send the response with. This is cleared to nullptr when it is // used. const FlutterPlatformMessageResponseHandle* response_handle; }; -G_DEFINE_TYPE(FlBinaryMessengerResponseHandle, - fl_binary_messenger_response_handle, - G_TYPE_OBJECT) +G_DEFINE_TYPE(FlBinaryMessengerResponseHandleImpl, + fl_binary_messenger_response_handle_impl, + fl_binary_messenger_response_handle_get_type()) + +static void fl_binary_messenger_default_init( + FlBinaryMessengerInterface* iface) {} -static void fl_binary_messenger_response_handle_dispose(GObject* object) { - FlBinaryMessengerResponseHandle* self = - FL_BINARY_MESSENGER_RESPONSE_HANDLE(object); +static void fl_binary_messenger_response_handle_impl_dispose(GObject* object) { + FlBinaryMessengerResponseHandleImpl* self = + FL_BINARY_MESSENGER_RESPONSE_HANDLE_IMPL(object); if (self->response_handle != nullptr && self->messenger->engine != nullptr) { g_critical("FlBinaryMessengerResponseHandle was not responded to"); @@ -50,25 +88,28 @@ static void fl_binary_messenger_response_handle_dispose(GObject* object) { g_clear_object(&self->messenger); self->response_handle = nullptr; - G_OBJECT_CLASS(fl_binary_messenger_response_handle_parent_class) + G_OBJECT_CLASS(fl_binary_messenger_response_handle_impl_parent_class) ->dispose(object); } -static void fl_binary_messenger_response_handle_class_init( - FlBinaryMessengerResponseHandleClass* klass) { - G_OBJECT_CLASS(klass)->dispose = fl_binary_messenger_response_handle_dispose; +static void fl_binary_messenger_response_handle_impl_class_init( + FlBinaryMessengerResponseHandleImplClass* klass) { + G_OBJECT_CLASS(klass)->dispose = + fl_binary_messenger_response_handle_impl_dispose; } -static void fl_binary_messenger_response_handle_init( - FlBinaryMessengerResponseHandle* self) {} +static void fl_binary_messenger_response_handle_impl_init( + FlBinaryMessengerResponseHandleImpl* self) {} -static FlBinaryMessengerResponseHandle* fl_binary_messenger_response_handle_new( - FlBinaryMessenger* messenger, +static FlBinaryMessengerResponseHandleImpl* +fl_binary_messenger_response_handle_impl_new( + FlBinaryMessengerImpl* messenger, const FlutterPlatformMessageResponseHandle* response_handle) { - FlBinaryMessengerResponseHandle* self = FL_BINARY_MESSENGER_RESPONSE_HANDLE( - g_object_new(fl_binary_messenger_response_handle_get_type(), nullptr)); + FlBinaryMessengerResponseHandleImpl* self = + FL_BINARY_MESSENGER_RESPONSE_HANDLE_IMPL(g_object_new( + fl_binary_messenger_response_handle_impl_get_type(), nullptr)); - self->messenger = FL_BINARY_MESSENGER(g_object_ref(messenger)); + self->messenger = FL_BINARY_MESSENGER_IMPL(g_object_ref(messenger)); self->response_handle = response_handle; return self; @@ -102,7 +143,7 @@ static void platform_message_handler_free(gpointer data) { static void engine_weak_notify_cb(gpointer user_data, GObject* where_the_object_was) { - FlBinaryMessenger* self = FL_BINARY_MESSENGER(user_data); + FlBinaryMessengerImpl* self = FL_BINARY_MESSENGER_IMPL(user_data); self->engine = nullptr; // Disconnect any handlers. @@ -119,7 +160,7 @@ static gboolean fl_binary_messenger_platform_message_cb( GBytes* message, const FlutterPlatformMessageResponseHandle* response_handle, void* user_data) { - FlBinaryMessenger* self = FL_BINARY_MESSENGER(user_data); + FlBinaryMessengerImpl* self = FL_BINARY_MESSENGER_IMPL(user_data); PlatformMessageHandler* handler = static_cast( g_hash_table_lookup(self->platform_message_handlers, channel)); @@ -127,16 +168,17 @@ static gboolean fl_binary_messenger_platform_message_cb( return FALSE; } - g_autoptr(FlBinaryMessengerResponseHandle) handle = - fl_binary_messenger_response_handle_new(self, response_handle); - handler->message_handler(self, channel, message, handle, + g_autoptr(FlBinaryMessengerResponseHandleImpl) handle = + fl_binary_messenger_response_handle_impl_new(self, response_handle); + handler->message_handler(FL_BINARY_MESSENGER(self), channel, message, + FL_BINARY_MESSENGER_RESPONSE_HANDLE(handle), handler->message_handler_data); return TRUE; } -static void fl_binary_messenger_dispose(GObject* object) { - FlBinaryMessenger* self = FL_BINARY_MESSENGER(object); +static void fl_binary_messenger_impl_dispose(GObject* object) { + FlBinaryMessengerImpl* self = FL_BINARY_MESSENGER_IMPL(object); if (self->engine != nullptr) { g_object_weak_unref(G_OBJECT(self->engine), engine_weak_notify_cb, self); @@ -145,41 +187,16 @@ static void fl_binary_messenger_dispose(GObject* object) { g_clear_pointer(&self->platform_message_handlers, g_hash_table_unref); - G_OBJECT_CLASS(fl_binary_messenger_parent_class)->dispose(object); -} - -static void fl_binary_messenger_class_init(FlBinaryMessengerClass* klass) { - G_OBJECT_CLASS(klass)->dispose = fl_binary_messenger_dispose; + G_OBJECT_CLASS(fl_binary_messenger_impl_parent_class)->dispose(object); } -static void fl_binary_messenger_init(FlBinaryMessenger* self) { - self->platform_message_handlers = g_hash_table_new_full( - g_str_hash, g_str_equal, g_free, platform_message_handler_free); -} - -FlBinaryMessenger* fl_binary_messenger_new(FlEngine* engine) { - g_return_val_if_fail(FL_IS_ENGINE(engine), nullptr); - - FlBinaryMessenger* self = FL_BINARY_MESSENGER( - g_object_new(fl_binary_messenger_get_type(), nullptr)); - - self->engine = engine; - g_object_weak_ref(G_OBJECT(engine), engine_weak_notify_cb, self); - - fl_engine_set_platform_message_handler( - engine, fl_binary_messenger_platform_message_cb, self, NULL); - - return self; -} - -G_MODULE_EXPORT void fl_binary_messenger_set_message_handler_on_channel( - FlBinaryMessenger* self, +static void set_message_handler_on_channel( + FlBinaryMessenger* messenger, const gchar* channel, FlBinaryMessengerMessageHandler handler, gpointer user_data, GDestroyNotify destroy_notify) { - g_return_if_fail(FL_IS_BINARY_MESSENGER(self)); - g_return_if_fail(channel != nullptr); + FlBinaryMessengerImpl* self = FL_BINARY_MESSENGER_IMPL(messenger); // Don't set handlers if engine already gone. if (self->engine == nullptr) { @@ -203,13 +220,16 @@ G_MODULE_EXPORT void fl_binary_messenger_set_message_handler_on_channel( } } -G_MODULE_EXPORT gboolean fl_binary_messenger_send_response( - FlBinaryMessenger* self, - FlBinaryMessengerResponseHandle* response_handle, - GBytes* response, - GError** error) { - g_return_val_if_fail(FL_IS_BINARY_MESSENGER(self), FALSE); - g_return_val_if_fail(response_handle != nullptr, FALSE); +static gboolean send_response(FlBinaryMessenger* messenger, + FlBinaryMessengerResponseHandle* response_handle_, + GBytes* response, + GError** error) { + FlBinaryMessengerImpl* self = FL_BINARY_MESSENGER_IMPL(messenger); + g_return_val_if_fail( + FL_IS_BINARY_MESSENGER_RESPONSE_HANDLE_IMPL(response_handle_), FALSE); + FlBinaryMessengerResponseHandleImpl* response_handle = + FL_BINARY_MESSENGER_RESPONSE_HANDLE_IMPL(response_handle_); + g_return_val_if_fail(response_handle->messenger == self, FALSE); g_return_val_if_fail(response_handle->response_handle != nullptr, FALSE); @@ -239,15 +259,13 @@ static void platform_message_ready_cb(GObject* object, g_task_return_pointer(task, result, g_object_unref); } -G_MODULE_EXPORT void fl_binary_messenger_send_on_channel( - FlBinaryMessenger* self, - const gchar* channel, - GBytes* message, - GCancellable* cancellable, - GAsyncReadyCallback callback, - gpointer user_data) { - g_return_if_fail(FL_IS_BINARY_MESSENGER(self)); - g_return_if_fail(channel != nullptr); +static void send_on_channel(FlBinaryMessenger* messenger, + const gchar* channel, + GBytes* message, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer user_data) { + FlBinaryMessengerImpl* self = FL_BINARY_MESSENGER_IMPL(messenger); if (self->engine == nullptr) { return; @@ -260,11 +278,10 @@ G_MODULE_EXPORT void fl_binary_messenger_send_on_channel( : nullptr); } -G_MODULE_EXPORT GBytes* fl_binary_messenger_send_on_channel_finish( - FlBinaryMessenger* self, - GAsyncResult* result, - GError** error) { - g_return_val_if_fail(FL_IS_BINARY_MESSENGER(self), FALSE); +static GBytes* send_on_channel_finish(FlBinaryMessenger* messenger, + GAsyncResult* result, + GError** error) { + FlBinaryMessengerImpl* self = FL_BINARY_MESSENGER_IMPL(messenger); g_return_val_if_fail(g_task_is_valid(result, self), FALSE); g_autoptr(GTask) task = G_TASK(result); @@ -276,3 +293,89 @@ G_MODULE_EXPORT GBytes* fl_binary_messenger_send_on_channel_finish( return fl_engine_send_platform_message_finish(self->engine, r, error); } + +static void fl_binary_messenger_impl_class_init( + FlBinaryMessengerImplClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_binary_messenger_impl_dispose; +} + +static void fl_binary_messenger_impl_iface_init( + FlBinaryMessengerInterface* iface) { + iface->set_message_handler_on_channel = set_message_handler_on_channel; + iface->send_response = send_response; + iface->send_on_channel = send_on_channel; + iface->send_on_channel_finish = send_on_channel_finish; +} + +static void fl_binary_messenger_impl_init(FlBinaryMessengerImpl* self) { + self->platform_message_handlers = g_hash_table_new_full( + g_str_hash, g_str_equal, g_free, platform_message_handler_free); +} + +FlBinaryMessenger* fl_binary_messenger_new(FlEngine* engine) { + g_return_val_if_fail(FL_IS_ENGINE(engine), nullptr); + + FlBinaryMessengerImpl* self = FL_BINARY_MESSENGER_IMPL( + g_object_new(fl_binary_messenger_impl_get_type(), nullptr)); + + // Added to stop compiler complaining about an unused function. + FL_IS_BINARY_MESSENGER_IMPL(self); + + self->engine = engine; + g_object_weak_ref(G_OBJECT(engine), engine_weak_notify_cb, self); + + fl_engine_set_platform_message_handler( + engine, fl_binary_messenger_platform_message_cb, self, NULL); + + return FL_BINARY_MESSENGER(self); +} + +G_MODULE_EXPORT void fl_binary_messenger_set_message_handler_on_channel( + FlBinaryMessenger* self, + const gchar* channel, + FlBinaryMessengerMessageHandler handler, + gpointer user_data, + GDestroyNotify destroy_notify) { + g_return_if_fail(FL_IS_BINARY_MESSENGER(self)); + g_return_if_fail(channel != nullptr); + + FL_BINARY_MESSENGER_GET_IFACE(self)->set_message_handler_on_channel( + self, channel, handler, user_data, destroy_notify); +} + +G_MODULE_EXPORT gboolean fl_binary_messenger_send_response( + FlBinaryMessenger* self, + FlBinaryMessengerResponseHandle* response_handle, + GBytes* response, + GError** error) { + g_return_val_if_fail(FL_IS_BINARY_MESSENGER(self), FALSE); + g_return_val_if_fail(FL_IS_BINARY_MESSENGER_RESPONSE_HANDLE(response_handle), + FALSE); + + return FL_BINARY_MESSENGER_GET_IFACE(self)->send_response( + self, response_handle, response, error); +} + +G_MODULE_EXPORT void fl_binary_messenger_send_on_channel( + FlBinaryMessenger* self, + const gchar* channel, + GBytes* message, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer user_data) { + g_return_if_fail(FL_IS_BINARY_MESSENGER(self)); + g_return_if_fail(channel != nullptr); + + FL_BINARY_MESSENGER_GET_IFACE(self)->send_on_channel( + self, channel, message, cancellable, callback, user_data); +} + +G_MODULE_EXPORT GBytes* fl_binary_messenger_send_on_channel_finish( + FlBinaryMessenger* self, + GAsyncResult* result, + GError** error) { + g_return_val_if_fail(FL_IS_BINARY_MESSENGER(self), FALSE); + + return FL_BINARY_MESSENGER_GET_IFACE(self)->send_on_channel_finish( + self, result, error); +} diff --git a/shell/platform/linux/fl_binary_messenger_test.cc b/shell/platform/linux/fl_binary_messenger_test.cc index 8d0bce14bd59f..5edb66da69c18 100644 --- a/shell/platform/linux/fl_binary_messenger_test.cc +++ b/shell/platform/linux/fl_binary_messenger_test.cc @@ -11,8 +11,208 @@ #include "flutter/shell/platform/linux/fl_engine_private.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h" #include "flutter/shell/platform/linux/testing/fl_test.h" +#include "flutter/shell/platform/linux/testing/mock_binary_messenger_response_handle.h" #include "flutter/shell/platform/linux/testing/mock_renderer.h" +G_DECLARE_FINAL_TYPE(FlMockBinaryMessenger, + fl_mock_binary_messenger, + FL, + MOCK_BINARY_MESSENGER, + GObject) + +struct _FlMockBinaryMessenger { + GObject parent_instance; + + GMainLoop* loop; + GAsyncReadyCallback send_callback; + gpointer send_callback_user_data; + FlBinaryMessengerMessageHandler message_handler; + gpointer message_handler_user_data; +}; + +static void fl_mock_binary_messenger_iface_init( + FlBinaryMessengerInterface* iface); + +G_DEFINE_TYPE_WITH_CODE( + FlMockBinaryMessenger, + fl_mock_binary_messenger, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(fl_binary_messenger_get_type(), + fl_mock_binary_messenger_iface_init)) + +static void fl_mock_binary_messenger_class_init( + FlMockBinaryMessengerClass* klass) {} + +static gboolean send_message_cb(gpointer user_data) { + FlMockBinaryMessenger* self = FL_MOCK_BINARY_MESSENGER(user_data); + + const char* text = "Marco!"; + g_autoptr(GBytes) message = g_bytes_new(text, strlen(text)); + self->message_handler(FL_BINARY_MESSENGER(self), "CHANNEL", message, + FL_BINARY_MESSENGER_RESPONSE_HANDLE( + fl_mock_binary_messenger_response_handle_new()), + self->message_handler_user_data); + + return FALSE; +} + +static void set_message_handler_on_channel( + FlBinaryMessenger* messenger, + const gchar* channel, + FlBinaryMessengerMessageHandler handler, + gpointer user_data, + GDestroyNotify destroy_notify) { + FlMockBinaryMessenger* self = FL_MOCK_BINARY_MESSENGER(messenger); + + EXPECT_STREQ(channel, "CHANNEL"); + + // Send message. + self->message_handler = handler; + self->message_handler_user_data = user_data; + g_idle_add(send_message_cb, messenger); +} + +static gboolean send_response(FlBinaryMessenger* messenger, + FlBinaryMessengerResponseHandle* response_handle, + GBytes* response, + GError** error) { + FlMockBinaryMessenger* self = FL_MOCK_BINARY_MESSENGER(messenger); + + EXPECT_TRUE(FL_IS_MOCK_BINARY_MESSENGER_RESPONSE_HANDLE(response_handle)); + + g_autofree gchar* text = + g_strndup(static_cast(g_bytes_get_data(response, nullptr)), + g_bytes_get_size(response)); + EXPECT_STREQ(text, "Polo!"); + + g_main_loop_quit(self->loop); + + return TRUE; +} + +static gboolean send_ready_cb(gpointer user_data) { + FlMockBinaryMessenger* self = FL_MOCK_BINARY_MESSENGER(user_data); + + self->send_callback(G_OBJECT(self), NULL, self->send_callback_user_data); + + return FALSE; +} + +static void send_on_channel(FlBinaryMessenger* messenger, + const gchar* channel, + GBytes* message, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer user_data) { + FlMockBinaryMessenger* self = FL_MOCK_BINARY_MESSENGER(messenger); + + EXPECT_STREQ(channel, "CHANNEL"); + g_autofree gchar* text = + g_strndup(static_cast(g_bytes_get_data(message, nullptr)), + g_bytes_get_size(message)); + EXPECT_STREQ(text, "Marco!"); + + // Send response. + self->send_callback = callback; + self->send_callback_user_data = user_data; + g_idle_add(send_ready_cb, messenger); +} + +static GBytes* send_on_channel_finish(FlBinaryMessenger* messenger, + GAsyncResult* result, + GError** error) { + const char* text = "Polo!"; + return g_bytes_new(text, strlen(text)); +} + +static void fl_mock_binary_messenger_iface_init( + FlBinaryMessengerInterface* iface) { + iface->set_message_handler_on_channel = set_message_handler_on_channel; + iface->send_response = send_response; + iface->send_on_channel = send_on_channel; + iface->send_on_channel_finish = send_on_channel_finish; +} + +static void fl_mock_binary_messenger_init(FlMockBinaryMessenger* self) {} + +static FlBinaryMessenger* fl_mock_binary_messenger_new(GMainLoop* loop) { + FlMockBinaryMessenger* self = FL_MOCK_BINARY_MESSENGER( + g_object_new(fl_mock_binary_messenger_get_type(), NULL)); + self->loop = loop; + return FL_BINARY_MESSENGER(self); +} + +// Called when the message response is received in the MockMessengerSend test. +static void mock_response_cb(GObject* object, + GAsyncResult* result, + gpointer user_data) { + g_autoptr(GError) error = nullptr; + g_autoptr(GBytes) message = fl_binary_messenger_send_on_channel_finish( + FL_BINARY_MESSENGER(object), result, &error); + EXPECT_NE(message, nullptr); + EXPECT_EQ(error, nullptr); + + g_autofree gchar* text = + g_strndup(static_cast(g_bytes_get_data(message, nullptr)), + g_bytes_get_size(message)); + EXPECT_STREQ(text, "Polo!"); + + g_main_loop_quit(static_cast(user_data)); +} + +// Checks can make a mock messenger and send a message. +TEST(FlBinaryMessengerTest, MockMessengerSend) { + g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); + + g_autoptr(FlBinaryMessenger) messenger = fl_mock_binary_messenger_new(loop); + EXPECT_TRUE(FL_IS_MOCK_BINARY_MESSENGER(messenger)); + + const char* text = "Marco!"; + g_autoptr(GBytes) message = g_bytes_new(text, strlen(text)); + fl_binary_messenger_send_on_channel(messenger, "CHANNEL", message, nullptr, + mock_response_cb, loop); + + // Blocks here until mock_response_cb is called. + g_main_loop_run(loop); +} + +// Called when a message is received in the MockMessengerReceive test. +static void mock_message_cb(FlBinaryMessenger* messenger, + const gchar* channel, + GBytes* message, + FlBinaryMessengerResponseHandle* response_handle, + gpointer user_data) { + EXPECT_STREQ(channel, "CHANNEL"); + + EXPECT_NE(message, nullptr); + g_autofree gchar* text = + g_strndup(static_cast(g_bytes_get_data(message, nullptr)), + g_bytes_get_size(message)); + EXPECT_STREQ(text, "Marco!"); + + const char* response_text = "Polo!"; + g_autoptr(GBytes) response = + g_bytes_new(response_text, strlen(response_text)); + g_autoptr(GError) error = nullptr; + EXPECT_TRUE(fl_binary_messenger_send_response(messenger, response_handle, + response, &error)); + EXPECT_EQ(error, nullptr); +} + +// Checks can make a mock messenger and receive a message. +TEST(FlBinaryMessengerTest, MockMessengerReceive) { + g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); + + g_autoptr(FlBinaryMessenger) messenger = fl_mock_binary_messenger_new(loop); + EXPECT_TRUE(FL_IS_MOCK_BINARY_MESSENGER(messenger)); + + fl_binary_messenger_set_message_handler_on_channel( + messenger, "CHANNEL", mock_message_cb, nullptr, nullptr); + + // Blocks here until response is received in mock messenger. + g_main_loop_run(loop); +} + // Checks sending nullptr for a message works. TEST(FlBinaryMessengerTest, SendNullptrMessage) { g_autoptr(FlEngine) engine = make_mock_engine(); diff --git a/shell/platform/linux/fl_dart_project_test.cc b/shell/platform/linux/fl_dart_project_test.cc index 1c1f29892c069..4cd3b2ce82b66 100644 --- a/shell/platform/linux/fl_dart_project_test.cc +++ b/shell/platform/linux/fl_dart_project_test.cc @@ -47,11 +47,11 @@ TEST(FlDartProjectTest, DartEntrypointArgs) { EXPECT_EQ(g_strv_length(retrieved_args), 0U); GPtrArray* args_array = g_ptr_array_new(); - g_ptr_array_add(args_array, (gpointer) "arg_one"); - g_ptr_array_add(args_array, (gpointer) "arg_two"); - g_ptr_array_add(args_array, (gpointer) "arg_three"); + g_ptr_array_add(args_array, const_cast("arg_one")); + g_ptr_array_add(args_array, const_cast("arg_two")); + g_ptr_array_add(args_array, const_cast("arg_three")); g_ptr_array_add(args_array, nullptr); - gchar** args = (gchar**)g_ptr_array_free(args_array, false); + gchar** args = reinterpret_cast(g_ptr_array_free(args_array, false)); fl_dart_project_set_dart_entrypoint_arguments(project, args); diff --git a/shell/platform/linux/fl_engine.cc b/shell/platform/linux/fl_engine.cc index 0033a3195dd17..8684feae285f3 100644 --- a/shell/platform/linux/fl_engine.cc +++ b/shell/platform/linux/fl_engine.cc @@ -12,10 +12,12 @@ #include "flutter/shell/platform/linux/fl_binary_messenger_private.h" #include "flutter/shell/platform/linux/fl_dart_project_private.h" #include "flutter/shell/platform/linux/fl_engine_private.h" +#include "flutter/shell/platform/linux/fl_pixel_buffer_texture_private.h" #include "flutter/shell/platform/linux/fl_plugin_registrar_private.h" #include "flutter/shell/platform/linux/fl_renderer.h" #include "flutter/shell/platform/linux/fl_renderer_headless.h" #include "flutter/shell/platform/linux/fl_settings_plugin.h" +#include "flutter/shell/platform/linux/fl_texture_gl_private.h" #include "flutter/shell/platform/linux/fl_texture_registrar_private.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h" @@ -48,7 +50,7 @@ struct _FlEngine { gpointer update_semantics_node_handler_data; GDestroyNotify update_semantics_node_handler_destroy_notify; - // Function to call when the engine is restarted. + // Function to call right before the engine is restarted. FlEngineOnPreEngineRestartHandler on_pre_engine_restart_handler; gpointer on_pre_engine_restart_handler_data; GDestroyNotify on_pre_engine_restart_handler_destroy_notify; @@ -203,13 +205,9 @@ static uint32_t fl_engine_gl_get_fbo(void* user_data) { } static bool fl_engine_gl_present(void* user_data) { - FlEngine* self = static_cast(user_data); - g_autoptr(GError) error = nullptr; - gboolean result = fl_renderer_present(self->renderer, &error); - if (!result) { - g_warning("%s", error->message); - } - return result; + // No action required, as this is handled in + // compositor_present_layers_callback. + return true; } static bool fl_engine_gl_make_resource_current(void* user_data) { @@ -228,18 +226,39 @@ static bool fl_engine_gl_external_texture_frame_callback( int64_t texture_id, size_t width, size_t height, - FlutterOpenGLTexture* texture) { + FlutterOpenGLTexture* opengl_texture) { FlEngine* self = static_cast(user_data); if (!self->texture_registrar) { return false; } + + FlTexture* texture = + fl_texture_registrar_lookup_texture(self->texture_registrar, texture_id); + if (texture == nullptr) { + g_warning("Unable to find texture %" G_GINT64_FORMAT, texture_id); + return false; + } + + gboolean result; g_autoptr(GError) error = nullptr; - gboolean result = fl_texture_registrar_populate_gl_external_texture( - self->texture_registrar, texture_id, width, height, texture, &error); + if (FL_IS_TEXTURE_GL(texture)) { + result = fl_texture_gl_populate(FL_TEXTURE_GL(texture), width, height, + opengl_texture, &error); + } else if (FL_IS_PIXEL_BUFFER_TEXTURE(texture)) { + result = + fl_pixel_buffer_texture_populate(FL_PIXEL_BUFFER_TEXTURE(texture), + width, height, opengl_texture, &error); + } else { + g_warning("Unsupported texture type %" G_GINT64_FORMAT, texture_id); + return false; + } + if (!result) { g_warning("%s", error->message); + return false; } - return result; + + return true; } // Called by the engine to determine if it is on the GTK thread. @@ -288,7 +307,7 @@ static void fl_engine_update_semantics_node_cb(const FlutterSemanticsNode* node, } } -// Called when the engine is restarted. +// Called right before the engine is restarted. // // This method should reset states to as if the engine has just been started, // which usually indicates the user has requested a hot restart (Shift-R in the @@ -501,8 +520,9 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { fl_settings_plugin_start(self->settings_plugin); result = self->embedder_api.UpdateSemanticsEnabled(self->engine, TRUE); - if (result != kSuccess) + if (result != kSuccess) { g_warning("Failed to enable accessibility features on Flutter engine"); + } return TRUE; } diff --git a/shell/platform/linux/fl_engine_private.h b/shell/platform/linux/fl_engine_private.h index a576a64e9608c..15197ad8361c9 100644 --- a/shell/platform/linux/fl_engine_private.h +++ b/shell/platform/linux/fl_engine_private.h @@ -133,7 +133,7 @@ void fl_engine_set_update_semantics_node_handler( * @destroy_notify: (allow-none): a function which gets called to free * @user_data, or %NULL. * - * Registers the function called when the engine is restarted. + * Registers the function called right before the engine is restarted. */ void fl_engine_set_on_pre_engine_restart_handler( FlEngine* engine, diff --git a/shell/platform/linux/fl_engine_test.cc b/shell/platform/linux/fl_engine_test.cc index 082f936ee8fe7..d2f6d408ab34c 100644 --- a/shell/platform/linux/fl_engine_test.cc +++ b/shell/platform/linux/fl_engine_test.cc @@ -285,11 +285,11 @@ TEST(FlEngineTest, DartEntrypointArgs) { g_autoptr(FlDartProject) project = fl_dart_project_new(); GPtrArray* args_array = g_ptr_array_new(); - g_ptr_array_add(args_array, (gpointer) "arg_one"); - g_ptr_array_add(args_array, (gpointer) "arg_two"); - g_ptr_array_add(args_array, (gpointer) "arg_three"); + g_ptr_array_add(args_array, const_cast("arg_one")); + g_ptr_array_add(args_array, const_cast("arg_two")); + g_ptr_array_add(args_array, const_cast("arg_three")); g_ptr_array_add(args_array, nullptr); - gchar** args = (gchar**)g_ptr_array_free(args_array, false); + gchar** args = reinterpret_cast(g_ptr_array_free(args_array, false)); fl_dart_project_set_dart_entrypoint_arguments(project, args); diff --git a/shell/platform/linux/fl_gl_area.cc b/shell/platform/linux/fl_gl_area.cc index 8d7200c351e05..7804d66d88000 100644 --- a/shell/platform/linux/fl_gl_area.cc +++ b/shell/platform/linux/fl_gl_area.cc @@ -55,8 +55,9 @@ static void fl_gl_area_unrealize(GtkWidget* widget) { g_clear_object(&self->texture); /* Make sure to unset the context if current */ - if (self->context == gdk_gl_context_get_current()) + if (self->context == gdk_gl_context_get_current()) { gdk_gl_context_clear_current(); + } GTK_WIDGET_CLASS(fl_gl_area_parent_class)->unrealize(widget); } @@ -67,10 +68,11 @@ static void fl_gl_area_size_allocate(GtkWidget* widget, gtk_widget_set_allocation(widget, allocation); if (gtk_widget_get_has_window(widget)) { - if (gtk_widget_get_realized(widget)) + if (gtk_widget_get_realized(widget)) { gdk_window_move_resize(gtk_widget_get_window(widget), allocation->x, allocation->y, allocation->width, allocation->height); + } } } @@ -118,7 +120,7 @@ GtkWidget* fl_gl_area_new(GdkGLContext* context) { g_return_val_if_fail(GDK_IS_GL_CONTEXT(context), nullptr); FlGLArea* area = reinterpret_cast(g_object_new(fl_gl_area_get_type(), nullptr)); - area->context = context; + area->context = GDK_GL_CONTEXT(g_object_ref(context)); return GTK_WIDGET(area); } diff --git a/shell/platform/linux/fl_key_channel_responder.cc b/shell/platform/linux/fl_key_channel_responder.cc index 1a7ff137b12f4..8f9946691cde1 100644 --- a/shell/platform/linux/fl_key_channel_responder.cc +++ b/shell/platform/linux/fl_key_channel_responder.cc @@ -132,7 +132,7 @@ static void handle_response(GObject* object, gpointer user_data) { g_autoptr(FlKeyChannelUserData) data = FL_KEY_CHANNEL_USER_DATA(user_data); - // Will also return if the weak pointer has been destroyed. + // This is true if the weak pointer has been destroyed. if (data->responder == nullptr) { return; } @@ -146,12 +146,14 @@ static void handle_response(GObject* object, if (self->mock != nullptr && self->mock->value_converter != nullptr) { message = self->mock->value_converter(message); } + bool handled = false; if (error != nullptr) { g_warning("Unable to retrieve framework response: %s", error->message); - return; + } else { + g_autoptr(FlValue) handled_value = + fl_value_lookup_string(message, "handled"); + handled = fl_value_get_bool(handled_value); } - g_autoptr(FlValue) handled_value = fl_value_lookup_string(message, "handled"); - bool handled = fl_value_get_bool(handled_value); data->callback(handled, data->user_data); } diff --git a/shell/platform/linux/fl_key_channel_responder_test.cc b/shell/platform/linux/fl_key_channel_responder_test.cc index c0fefe5effae0..75dcd17b4b3b9 100644 --- a/shell/platform/linux/fl_key_channel_responder_test.cc +++ b/shell/platform/linux/fl_key_channel_responder_test.cc @@ -39,7 +39,7 @@ static char* clone_string(const char* string) { } size_t len = strlen(string); char* result = g_new(char, len + 1); - strcpy(result, string); + strncpy(result, string, len + 1); return result; } diff --git a/shell/platform/linux/fl_key_embedder_responder.cc b/shell/platform/linux/fl_key_embedder_responder.cc index 982273ffb746e..14eb6bb3f2240 100644 --- a/shell/platform/linux/fl_key_embedder_responder.cc +++ b/shell/platform/linux/fl_key_embedder_responder.cc @@ -34,6 +34,23 @@ static uint64_t lookup_hash_table(GHashTable* table, uint64_t key) { g_hash_table_lookup(table, uint64_to_gpointer(key))); } +static gboolean hash_table_find_equal_value(gpointer key, + gpointer value, + gpointer user_data) { + return gpointer_to_uint64(value) == gpointer_to_uint64(user_data); +} + +// Look up a hash table that maps a uint64_t to a uint64_t; given its key, +// find its value. +// +// Returns 0 if not found. +// +// Both key and value should be directly hashed. +static uint64_t reverse_lookup_hash_table(GHashTable* table, uint64_t value) { + return gpointer_to_uint64(g_hash_table_find( + table, hash_table_find_equal_value, uint64_to_gpointer(value))); +} + static uint64_t to_lower(uint64_t n) { constexpr uint64_t lower_a = 0x61; constexpr uint64_t upper_a = 0x41; @@ -306,7 +323,7 @@ static uint64_t event_to_logical_key(const FlKeyEvent* event) { } static uint64_t event_to_timestamp(const FlKeyEvent* event) { - return kMicrosecondsPerMillisecond * (double)event->time; + return kMicrosecondsPerMillisecond * static_cast(event->time); } // Returns a newly accocated UTF-8 string from event->keyval that must be @@ -316,8 +333,9 @@ static char* event_to_character(const FlKeyEvent* event) { glong items_written; gchar* result = g_ucs4_to_utf8(&unicodeChar, 1, NULL, &items_written, NULL); if (items_written == 0) { - if (result != NULL) + if (result != NULL) { g_free(result); + } return nullptr; } return result; @@ -427,48 +445,59 @@ static void synchronize_pressed_states_loop_body(gpointer key, const guint modifier_bit = GPOINTER_TO_INT(key); FlKeyEmbedderResponder* self = context->self; + // Each TestKey contains up to two logical keys, typically the left modifier + // and the right modifier, that correspond to the same modifier_bit. We'd + // like to infer whether to synthesize a down or up event for each key. + // + // The hard part is that, if we want to synthesize a down event, we don't know + // which physical key to use. Here we assume the keyboard layout do not change + // frequently and use the last physical-logical relationship, recorded in + // #mapping_records. const uint64_t logical_keys[] = { checked_key->primary_logical_key, checked_key->secondary_logical_key, }; const guint length = checked_key->secondary_logical_key == 0 ? 1 : 2; - const bool pressed_by_state = (context->state & modifier_bit) != 0; + const bool any_pressed_by_state = (context->state & modifier_bit) != 0; - bool pressed_by_record = false; + bool any_pressed_by_record = false; // Traverse each logical key of this modifier bit for 2 purposes: // - // 1. Find if this logical key is pressed before the event, - // and synthesize a release event if needed. - // 2. Find if any logical key of this modifier is pressed - // before the event (#pressed_by_record), so that we can decide - // whether to synthesize a press event later. + // 1. Perform the synthesization of release events: If the modifier bit is 0 + // and the key is pressed, synthesize a release event. + // 2. Prepare for the synthesization of press events: If the modifier bit is + // 1, and no keys are pressed (discovered here), synthesize a press event + // later. for (guint logical_key_idx = 0; logical_key_idx < length; logical_key_idx++) { const uint64_t logical_key = logical_keys[logical_key_idx]; - const uint64_t recorded_physical_key = - lookup_hash_table(self->mapping_records, logical_key); - const uint64_t pressed_logical_key_before_event = - recorded_physical_key == 0 - ? 0 - : lookup_hash_table(self->pressing_records, recorded_physical_key); - const bool this_key_pressed_before_event = - pressed_logical_key_before_event != 0; - - g_return_if_fail(pressed_logical_key_before_event == 0 || - pressed_logical_key_before_event == logical_key); - - pressed_by_record = pressed_by_record || this_key_pressed_before_event; - - if (this_key_pressed_before_event && !pressed_by_state) { + g_return_if_fail(logical_key != 0); + const uint64_t pressing_physical_key = + reverse_lookup_hash_table(self->pressing_records, logical_key); + const bool this_key_pressed_before_event = pressing_physical_key != 0; + + any_pressed_by_record = + any_pressed_by_record || this_key_pressed_before_event; + + if (this_key_pressed_before_event && !any_pressed_by_state) { + const uint64_t recorded_physical_key = + lookup_hash_table(self->mapping_records, logical_key); + // Since this key has been pressed before, there must have been a recorded + // physical key. + g_return_if_fail(recorded_physical_key != 0); + // In rare cases #recorded_logical_key is different from #logical_key. + const uint64_t recorded_logical_key = + lookup_hash_table(self->pressing_records, recorded_physical_key); synthesize_simple_event(self, kFlutterKeyEventTypeUp, - recorded_physical_key, logical_key, + recorded_physical_key, recorded_logical_key, context->timestamp); update_pressing_state(self, recorded_physical_key, 0); } } - // If the modifier should be pressed, press its primary key. - if (pressed_by_state && !pressed_by_record) { + // If the modifier should be pressed, synthesize a down event for its primary + // key. + if (any_pressed_by_state && !any_pressed_by_record) { const uint64_t logical_key = checked_key->primary_logical_key; const uint64_t recorded_physical_key = lookup_hash_table(self->mapping_records, logical_key); @@ -714,7 +743,8 @@ static void fl_key_embedder_responder_handle_event_impl( out_event.struct_size = sizeof(out_event); out_event.timestamp = timestamp; out_event.physical = physical_key; - out_event.logical = logical_key; + out_event.logical = + last_logical_record != 0 ? last_logical_record : logical_key; out_event.character = nullptr; out_event.synthesized = false; diff --git a/shell/platform/linux/fl_key_embedder_responder_test.cc b/shell/platform/linux/fl_key_embedder_responder_test.cc index 48d2681f0dac3..1ff36b254f0f2 100644 --- a/shell/platform/linux/fl_key_embedder_responder_test.cc +++ b/shell/platform/linux/fl_key_embedder_responder_test.cc @@ -19,8 +19,11 @@ constexpr gboolean kPress = TRUE; constexpr gboolean kIsModifier = TRUE; constexpr gboolean kIsNotModifier = FALSE; +constexpr guint16 kKeyCodeDigit1 = 0x0au; constexpr guint16 kKeyCodeKeyA = 0x26u; +constexpr guint16 kKeyCodeShiftLeft = 0x32u; constexpr guint16 kKeyCodeShiftRight = 0x3Eu; +constexpr guint16 kKeyCodeAltRight = 0x6Cu; constexpr guint16 kKeyCodeNumpad1 = 0x57u; constexpr guint16 kKeyCodeNumLock = 0x4Du; constexpr guint16 kKeyCodeCapsLock = 0x42u; @@ -86,7 +89,7 @@ static FlKeyEmbedderCallRecord* fl_key_embedder_call_record_new( if (event->character != nullptr) { size_t character_length = strlen(event->character); char* clone_character = g_new(char, character_length + 1); - strcpy(clone_character, event->character); + strncpy(clone_character, event->character, character_length + 1); clone_event->character = clone_character; } self->event = clone_event; @@ -527,6 +530,103 @@ TEST(FlKeyEmbedderResponderTest, TapNumPadKeysBetweenNumLockEvents) { g_object_unref(responder); } +// Press or release digit 1 between presses/releases of Shift. +// +// GTK will change the virtual key during a key tap, and the embedder +// should regularize it. +TEST(FlKeyEmbedderResponderTest, ReleaseShiftKeyBetweenDigitKeyEvents) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + guint state = 0; + + // Press shift left + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kPress, GDK_KEY_Shift_L, kKeyCodeShiftLeft, + state, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalShiftLeft); + EXPECT_EQ(record->event->logical, kLogicalShiftLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + state = GDK_SHIFT_MASK; + + // Press digit 1, which is '!' on a US keyboard + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kPress, GDK_KEY_exclam, kKeyCodeDigit1, + state, kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalDigit1); + EXPECT_EQ(record->event->logical, kLogicalExclamation); + EXPECT_STREQ(record->event->character, "!"); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release shift + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(103, kRelease, GDK_KEY_Shift_L, + kKeyCodeShiftLeft, state, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalShiftLeft); + EXPECT_EQ(record->event->logical, kLogicalShiftLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + state = 0; + + // Release digit 1, which is "1" because shift has been released. + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(104, kRelease, GDK_KEY_1, kKeyCodeDigit1, state, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalDigit1); + EXPECT_EQ(record->event->logical, kLogicalExclamation); // Important + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} + // Press or release letter key between presses/releases of CapsLock. // // This tests interaction between lock keys and non-lock keys in cases that do @@ -1538,3 +1638,107 @@ TEST(FlKeyEmbedderResponderTest, SynthesizationOccursOnIgnoredEvents) { g_object_unref(engine); g_object_unref(responder); } + +// This test case occurs when the following two cases collide: +// +// 1. When holding shift, AltRight key gives logical GDK_KEY_Meta_R with the +// state bitmask still MOD3 (Alt). +// 2. When holding AltRight, ShiftLeft key gives logical GDK_KEY_ISO_Next_Group +// with the state bitmask RESERVED_14. +// +// The resulting event sequence is not perfectly ideal: it had to synthesize +// AltLeft down because the physical AltRight key corresponds to logical +// MetaRight at the moment. +TEST(FlKeyEmbedderResponderTest, HandlesShiftAltVersusGroupNext) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + + g_expected_handled = true; + guint32 now_time = 1; + // A convenient shorthand to simulate events. + auto send_key_event = [responder, &now_time](bool is_press, guint keyval, + guint16 keycode, int state) { + now_time += 1; + int user_data = 123; // Arbitrary user data + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(now_time, is_press, keyval, keycode, state, + kIsModifier), + verify_response_handled, &user_data); + }; + + FlKeyEmbedderCallRecord* record; + + send_key_event(kPress, GDK_KEY_Shift_L, kKeyCodeShiftLeft, 0x2000000); + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalShiftLeft); + EXPECT_EQ(record->event->logical, kLogicalShiftLeft); + EXPECT_EQ(record->event->synthesized, false); + + send_key_event(kPress, GDK_KEY_Meta_R, kKeyCodeAltRight, 0x2000001); + EXPECT_EQ(g_call_records->len, 2u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalAltRight); + EXPECT_EQ(record->event->logical, kLogicalMetaRight); + EXPECT_EQ(record->event->synthesized, false); + + send_key_event(kRelease, GDK_KEY_ISO_Next_Group, kKeyCodeShiftLeft, + 0x2000009); + EXPECT_EQ(g_call_records->len, 5u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 2)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalAltLeft); + EXPECT_EQ(record->event->logical, kLogicalAltLeft); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 3)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalAltRight); + EXPECT_EQ(record->event->logical, kLogicalMetaRight); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 4)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalShiftLeft); + EXPECT_EQ(record->event->logical, kLogicalShiftLeft); + EXPECT_EQ(record->event->synthesized, false); + + send_key_event(kPress, GDK_KEY_ISO_Next_Group, kKeyCodeShiftLeft, 0x2000008); + EXPECT_EQ(g_call_records->len, 6u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 5)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalShiftLeft); + EXPECT_EQ(record->event->logical, kLogicalGroupNext); + EXPECT_EQ(record->event->synthesized, false); + + send_key_event(kRelease, GDK_KEY_ISO_Level3_Shift, kKeyCodeAltRight, + 0x2002008); + EXPECT_EQ(g_call_records->len, 7u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 6)); + EXPECT_EQ(record->event->physical, 0u); + EXPECT_EQ(record->event->logical, 0u); + + send_key_event(kRelease, GDK_KEY_Shift_L, kKeyCodeShiftLeft, 0x2002000); + EXPECT_EQ(g_call_records->len, 9u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 7)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalAltLeft); + EXPECT_EQ(record->event->logical, kLogicalAltLeft); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 8)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalShiftLeft); + EXPECT_EQ(record->event->logical, kLogicalGroupNext); + EXPECT_EQ(record->event->synthesized, false); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} diff --git a/shell/platform/linux/fl_key_event.cc b/shell/platform/linux/fl_key_event.cc index b823a3377ae16..2e3c010457986 100644 --- a/shell/platform/linux/fl_key_event.cc +++ b/shell/platform/linux/fl_key_event.cc @@ -15,7 +15,7 @@ static char* clone_string(const char* source) { } size_t length = strlen(source); char* result = g_new(char, length + 1); - strcpy(result, source); + strncpy(result, source, length + 1); return result; } diff --git a/shell/platform/linux/fl_keyboard_manager.cc b/shell/platform/linux/fl_keyboard_manager.cc index 9926dd44caf63..703cb7227c51b 100644 --- a/shell/platform/linux/fl_keyboard_manager.cc +++ b/shell/platform/linux/fl_keyboard_manager.cc @@ -213,8 +213,9 @@ static void fl_keyboard_manager_init(FlKeyboardManager* self) {} static void fl_keyboard_manager_dispose(GObject* object) { FlKeyboardManager* self = FL_KEYBOARD_MANAGER(object); - if (self->text_input_plugin != nullptr) + if (self->text_input_plugin != nullptr) { g_clear_object(&self->text_input_plugin); + } g_ptr_array_free(self->responder_list, TRUE); g_ptr_array_set_free_func(self->pending_responds, g_object_unref); g_ptr_array_free(self->pending_responds, TRUE); @@ -234,8 +235,9 @@ gboolean g_ptr_array_find_with_equal_func1(GPtrArray* haystack, guint* index_) { guint i; g_return_val_if_fail(haystack != NULL, FALSE); - if (equal_func == NULL) + if (equal_func == NULL) { equal_func = g_direct_equal; + } for (i = 0; i < haystack->len; i++) { if (equal_func(g_ptr_array_index(haystack, i), needle)) { if (index_ != NULL) { diff --git a/shell/platform/linux/fl_plugin_registrar.cc b/shell/platform/linux/fl_plugin_registrar.cc index e9fe0318a65b9..c2b83fb6bfb57 100644 --- a/shell/platform/linux/fl_plugin_registrar.cc +++ b/shell/platform/linux/fl_plugin_registrar.cc @@ -7,7 +7,13 @@ #include -struct _FlPluginRegistrar { +G_DECLARE_FINAL_TYPE(FlPluginRegistrarImpl, + fl_plugin_registrar_impl, + FL, + PLUGIN_REGISTRAR_IMPL, + GObject) + +struct _FlPluginRegistrarImpl { GObject parent_instance; // View that plugin is controlling. @@ -23,10 +29,23 @@ struct _FlPluginRegistrar { // Added here to stop the compiler from optimizing this function away. G_MODULE_EXPORT GType fl_plugin_registrar_get_type(); -G_DEFINE_TYPE(FlPluginRegistrar, fl_plugin_registrar, G_TYPE_OBJECT) +static void fl_plugin_registrar_impl_iface_init( + FlPluginRegistrarInterface* iface); + +G_DEFINE_INTERFACE(FlPluginRegistrar, fl_plugin_registrar, G_TYPE_OBJECT) + +G_DEFINE_TYPE_WITH_CODE( + FlPluginRegistrarImpl, + fl_plugin_registrar_impl, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(fl_plugin_registrar_get_type(), + fl_plugin_registrar_impl_iface_init)) -static void fl_plugin_registrar_dispose(GObject* object) { - FlPluginRegistrar* self = FL_PLUGIN_REGISTRAR(object); +static void fl_plugin_registrar_default_init( + FlPluginRegistrarInterface* iface) {} + +static void fl_plugin_registrar_impl_dispose(GObject* object) { + FlPluginRegistrarImpl* self = FL_PLUGIN_REGISTRAR_IMPL(object); if (self->view != nullptr) { g_object_remove_weak_pointer(G_OBJECT(self->view), @@ -36,14 +55,37 @@ static void fl_plugin_registrar_dispose(GObject* object) { g_clear_object(&self->messenger); g_clear_object(&self->texture_registrar); - G_OBJECT_CLASS(fl_plugin_registrar_parent_class)->dispose(object); + G_OBJECT_CLASS(fl_plugin_registrar_impl_parent_class)->dispose(object); +} + +static void fl_plugin_registrar_impl_class_init( + FlPluginRegistrarImplClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_plugin_registrar_impl_dispose; } -static void fl_plugin_registrar_class_init(FlPluginRegistrarClass* klass) { - G_OBJECT_CLASS(klass)->dispose = fl_plugin_registrar_dispose; +static FlBinaryMessenger* get_messenger(FlPluginRegistrar* registrar) { + FlPluginRegistrarImpl* self = FL_PLUGIN_REGISTRAR_IMPL(registrar); + return self->messenger; } -static void fl_plugin_registrar_init(FlPluginRegistrar* self) {} +static FlTextureRegistrar* get_texture_registrar(FlPluginRegistrar* registrar) { + FlPluginRegistrarImpl* self = FL_PLUGIN_REGISTRAR_IMPL(registrar); + return self->texture_registrar; +} + +static FlView* get_view(FlPluginRegistrar* registrar) { + FlPluginRegistrarImpl* self = FL_PLUGIN_REGISTRAR_IMPL(registrar); + return self->view; +} + +static void fl_plugin_registrar_impl_iface_init( + FlPluginRegistrarInterface* iface) { + iface->get_messenger = get_messenger; + iface->get_texture_registrar = get_texture_registrar; + iface->get_view = get_view; +} + +static void fl_plugin_registrar_impl_init(FlPluginRegistrarImpl* self) {} FlPluginRegistrar* fl_plugin_registrar_new( FlView* view, @@ -53,8 +95,11 @@ FlPluginRegistrar* fl_plugin_registrar_new( g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr); g_return_val_if_fail(FL_IS_TEXTURE_REGISTRAR(texture_registrar), nullptr); - FlPluginRegistrar* self = FL_PLUGIN_REGISTRAR( - g_object_new(fl_plugin_registrar_get_type(), nullptr)); + FlPluginRegistrarImpl* self = FL_PLUGIN_REGISTRAR_IMPL( + g_object_new(fl_plugin_registrar_impl_get_type(), nullptr)); + + // Added to stop compiler complaining about an unused function. + FL_IS_PLUGIN_REGISTRAR_IMPL(self); self->view = view; if (view != nullptr) { @@ -65,25 +110,25 @@ FlPluginRegistrar* fl_plugin_registrar_new( self->texture_registrar = FL_TEXTURE_REGISTRAR(g_object_ref(texture_registrar)); - return self; + return FL_PLUGIN_REGISTRAR(self); } G_MODULE_EXPORT FlBinaryMessenger* fl_plugin_registrar_get_messenger( FlPluginRegistrar* self) { g_return_val_if_fail(FL_IS_PLUGIN_REGISTRAR(self), nullptr); - return self->messenger; + return FL_PLUGIN_REGISTRAR_GET_IFACE(self)->get_messenger(self); } G_MODULE_EXPORT FlTextureRegistrar* fl_plugin_registrar_get_texture_registrar( FlPluginRegistrar* self) { g_return_val_if_fail(FL_IS_PLUGIN_REGISTRAR(self), nullptr); - return self->texture_registrar; + return FL_PLUGIN_REGISTRAR_GET_IFACE(self)->get_texture_registrar(self); } G_MODULE_EXPORT FlView* fl_plugin_registrar_get_view(FlPluginRegistrar* self) { g_return_val_if_fail(FL_IS_PLUGIN_REGISTRAR(self), nullptr); - return self->view; + return FL_PLUGIN_REGISTRAR_GET_IFACE(self)->get_view(self); } diff --git a/shell/platform/linux/fl_plugin_registrar_test.cc b/shell/platform/linux/fl_plugin_registrar_test.cc new file mode 100644 index 0000000000000..792e32473a3bf --- /dev/null +++ b/shell/platform/linux/fl_plugin_registrar_test.cc @@ -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. + +#include + +// Included first as it collides with the X11 headers. +#include "gtest/gtest.h" + +#include "flutter/shell/platform/linux/fl_binary_messenger_private.h" +#include "flutter/shell/platform/linux/fl_texture_registrar_private.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registrar.h" +#include "flutter/shell/platform/linux/testing/fl_test.h" +#include "flutter/shell/platform/linux/testing/mock_plugin_registrar.h" + +// Checks can make a mock registrar. +TEST(FlPluginRegistrarTest, FlMockRegistrar) { + g_autoptr(FlEngine) engine = make_mock_engine(); + g_autoptr(FlBinaryMessenger) messenger = fl_binary_messenger_new(engine); + g_autoptr(FlTextureRegistrar) texture_registrar = + fl_texture_registrar_new(engine); + + g_autoptr(FlPluginRegistrar) registrar = + fl_mock_plugin_registrar_new(messenger, texture_registrar); + EXPECT_TRUE(FL_IS_MOCK_PLUGIN_REGISTRAR(registrar)); + + EXPECT_EQ(fl_plugin_registrar_get_messenger(registrar), messenger); + EXPECT_EQ(fl_plugin_registrar_get_texture_registrar(registrar), + texture_registrar); +} diff --git a/shell/platform/linux/fl_renderer.cc b/shell/platform/linux/fl_renderer.cc index 76cfbfad22d93..f32215dd38ecc 100644 --- a/shell/platform/linux/fl_renderer.cc +++ b/shell/platform/linux/fl_renderer.cc @@ -70,8 +70,9 @@ gboolean fl_renderer_start(FlRenderer* self, FlView* view, GError** error) { gdk_gl_context_realize(priv->resource_context, error); } - if (*error != nullptr) + if (*error != nullptr) { return FALSE; + } return TRUE; } @@ -121,10 +122,6 @@ guint32 fl_renderer_get_fbo(FlRenderer* self) { return 0; } -gboolean fl_renderer_present(FlRenderer* self, GError** error) { - return TRUE; -} - gboolean fl_renderer_create_backing_store( FlRenderer* self, const FlutterBackingStoreConfig* config, diff --git a/shell/platform/linux/fl_renderer.h b/shell/platform/linux/fl_renderer.h index 545f7b8f584d7..d5db38cd0b5be 100644 --- a/shell/platform/linux/fl_renderer.h +++ b/shell/platform/linux/fl_renderer.h @@ -186,18 +186,6 @@ gboolean fl_renderer_clear_current(FlRenderer* renderer, GError** error); */ guint32 fl_renderer_get_fbo(FlRenderer* renderer); -/** - * fl_renderer_present: - * @renderer: an #FlRenderer. - * @error: (allow-none): #GError location to store the error occurring, or %NULL - * to ignore. - * - * Presents the current frame. - * - * Returns %TRUE if successful. - */ -gboolean fl_renderer_present(FlRenderer* renderer, GError** error); - /** * fl_renderer_create_backing_store: * @renderer: an #FlRenderer. diff --git a/shell/platform/linux/fl_renderer_gl.cc b/shell/platform/linux/fl_renderer_gl.cc index 9a657f290f497..ec44b9b2dc5b8 100644 --- a/shell/platform/linux/fl_renderer_gl.cc +++ b/shell/platform/linux/fl_renderer_gl.cc @@ -23,13 +23,15 @@ static gboolean fl_renderer_gl_create_contexts(FlRenderer* renderer, *visible = gdk_window_create_gl_context(window, error); - if (*error != nullptr) + if (*error != nullptr) { return FALSE; + } *resource = gdk_window_create_gl_context(window, error); - if (*error != nullptr) + if (*error != nullptr) { return FALSE; + } return TRUE; } @@ -93,8 +95,9 @@ static gboolean fl_renderer_gl_present_layers(FlRenderer* renderer, size_t layers_count) { FlView* view = fl_renderer_get_view(renderer); GdkGLContext* context = fl_renderer_get_context(renderer); - if (!view || !context) + if (!view || !context) { return FALSE; + } fl_view_begin_frame(view); for (size_t i = 0; i < layers_count; ++i) { @@ -103,7 +106,6 @@ static gboolean fl_renderer_gl_present_layers(FlRenderer* renderer, case kFlutterLayerContentTypeBackingStore: { const FlutterBackingStore* backing_store = layer->backing_store; auto framebuffer = &backing_store->open_gl.framebuffer; - g_object_ref(context); fl_view_add_gl_area( view, context, reinterpret_cast(framebuffer->user_data)); diff --git a/shell/platform/linux/fl_settings_plugin.cc b/shell/platform/linux/fl_settings_plugin.cc index c7b31a466bd2d..1cf50112f8efa 100644 --- a/shell/platform/linux/fl_settings_plugin.cc +++ b/shell/platform/linux/fl_settings_plugin.cc @@ -42,8 +42,9 @@ G_DEFINE_TYPE(FlSettingsPlugin, fl_settings_plugin, G_TYPE_OBJECT) // See . static gdouble linearize_color_component(gdouble component) { - if (component <= 0.03928) + if (component <= 0.03928) { return component / 12.92; + } return pow((component + 0.055) / 1.055, 2.4); } @@ -61,8 +62,9 @@ static Brightness estimate_brightness_for_color(GdkRGBA* color) { // See and // . const gdouble kThreshold = 0.15; - if ((relative_luminance + 0.05) * (relative_luminance + 0.05) > kThreshold) + if ((relative_luminance + 0.05) * (relative_luminance + 0.05) > kThreshold) { return Brightness::Light; + } return Brightness::Dark; } @@ -70,8 +72,9 @@ static bool is_dark_theme() { // GTK doesn't have a specific flag for dark themes, so we check if the // style text color is light or dark GList* windows = gtk_window_list_toplevels(); - if (windows == nullptr) + if (windows == nullptr) { return false; + } GtkWidget* window = GTK_WIDGET(windows->data); g_list_free(windows); diff --git a/shell/platform/linux/fl_standard_message_codec_test.cc b/shell/platform/linux/fl_standard_message_codec_test.cc index a9b3d75f08789..b3b13a5a5c557 100644 --- a/shell/platform/linux/fl_standard_message_codec_test.cc +++ b/shell/platform/linux/fl_standard_message_codec_test.cc @@ -661,10 +661,11 @@ TEST(FlStandardMessageCodecTest, EncodeListNested) { g_autoptr(FlValue) even_numbers = fl_value_new_list(); g_autoptr(FlValue) odd_numbers = fl_value_new_list(); for (int i = 0; i < 10; i++) { - if (i % 2 == 0) + if (i % 2 == 0) { fl_value_append_take(even_numbers, fl_value_new_int(i)); - else + } else { fl_value_append_take(odd_numbers, fl_value_new_int(i)); + } } g_autoptr(FlValue) value = fl_value_new_list(); fl_value_append(value, even_numbers); @@ -758,8 +759,9 @@ TEST(FlStandardMessageCodecTest, EncodeDecodeLargeList) { g_autoptr(FlStandardMessageCodec) codec = fl_standard_message_codec_new(); g_autoptr(FlValue) value = fl_value_new_list(); - for (int i = 0; i < 65535; i++) + for (int i = 0; i < 65535; i++) { fl_value_append_take(value, fl_value_new_int(i)); + } g_autoptr(GError) error = nullptr; g_autoptr(GBytes) message = diff --git a/shell/platform/linux/fl_texture_registrar.cc b/shell/platform/linux/fl_texture_registrar.cc index 70c6e59cbd198..a2a6669a5ed08 100644 --- a/shell/platform/linux/fl_texture_registrar.cc +++ b/shell/platform/linux/fl_texture_registrar.cc @@ -13,7 +13,13 @@ #include "flutter/shell/platform/linux/fl_texture_private.h" #include "flutter/shell/platform/linux/fl_texture_registrar_private.h" -struct _FlTextureRegistrar { +G_DECLARE_FINAL_TYPE(FlTextureRegistrarImpl, + fl_texture_registrar_impl, + FL, + TEXTURE_REGISTRAR_IMPL, + GObject) + +struct _FlTextureRegistrarImpl { GObject parent_instance; // Weak reference to the engine this texture registrar is created for. @@ -27,11 +33,24 @@ struct _FlTextureRegistrar { GHashTable* textures; }; -G_DEFINE_TYPE(FlTextureRegistrar, fl_texture_registrar, G_TYPE_OBJECT) +static void fl_texture_registrar_impl_iface_init( + FlTextureRegistrarInterface* iface); + +G_DEFINE_INTERFACE(FlTextureRegistrar, fl_texture_registrar, G_TYPE_OBJECT) + +G_DEFINE_TYPE_WITH_CODE( + FlTextureRegistrarImpl, + fl_texture_registrar_impl, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(fl_texture_registrar_get_type(), + fl_texture_registrar_impl_iface_init)) + +static void fl_texture_registrar_default_init( + FlTextureRegistrarInterface* iface) {} static void engine_weak_notify_cb(gpointer user_data, GObject* where_the_object_was) { - FlTextureRegistrar* self = FL_TEXTURE_REGISTRAR(user_data); + FlTextureRegistrarImpl* self = FL_TEXTURE_REGISTRAR_IMPL(user_data); self->engine = nullptr; // Unregister any textures. @@ -41,8 +60,8 @@ static void engine_weak_notify_cb(gpointer user_data, g_hash_table_remove_all(textures); } -static void fl_texture_registrar_dispose(GObject* object) { - FlTextureRegistrar* self = FL_TEXTURE_REGISTRAR(object); +static void fl_texture_registrar_impl_dispose(GObject* object) { + FlTextureRegistrarImpl* self = FL_TEXTURE_REGISTRAR_IMPL(object); g_clear_pointer(&self->textures, g_hash_table_unref); @@ -51,23 +70,17 @@ static void fl_texture_registrar_dispose(GObject* object) { self->engine = nullptr; } - G_OBJECT_CLASS(fl_texture_registrar_parent_class)->dispose(object); + G_OBJECT_CLASS(fl_texture_registrar_impl_parent_class)->dispose(object); } -static void fl_texture_registrar_class_init(FlTextureRegistrarClass* klass) { - G_OBJECT_CLASS(klass)->dispose = fl_texture_registrar_dispose; -} - -static void fl_texture_registrar_init(FlTextureRegistrar* self) { - self->textures = g_hash_table_new_full(g_direct_hash, g_direct_equal, nullptr, - g_object_unref); +static void fl_texture_registrar_impl_class_init( + FlTextureRegistrarImplClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_texture_registrar_impl_dispose; } -G_MODULE_EXPORT gboolean -fl_texture_registrar_register_texture(FlTextureRegistrar* self, - FlTexture* texture) { - g_return_val_if_fail(FL_IS_TEXTURE_REGISTRAR(self), FALSE); - g_return_val_if_fail(FL_IS_TEXTURE(texture), FALSE); +static gboolean register_texture(FlTextureRegistrar* registrar, + FlTexture* texture) { + FlTextureRegistrarImpl* self = FL_TEXTURE_REGISTRAR_IMPL(registrar); if (FL_IS_TEXTURE_GL(texture) || FL_IS_PIXEL_BUFFER_TEXTURE(texture)) { g_hash_table_insert(self->textures, @@ -86,17 +99,23 @@ fl_texture_registrar_register_texture(FlTextureRegistrar* self, } } -G_MODULE_EXPORT gboolean -fl_texture_registrar_mark_texture_frame_available(FlTextureRegistrar* self, - FlTexture* texture) { - g_return_val_if_fail(FL_IS_TEXTURE_REGISTRAR(self), FALSE); +static FlTexture* lookup_texture(FlTextureRegistrar* registrar, + int64_t texture_id) { + FlTextureRegistrarImpl* self = FL_TEXTURE_REGISTRAR_IMPL(registrar); + return reinterpret_cast( + g_hash_table_lookup(self->textures, GINT_TO_POINTER(texture_id))); +} + +static gboolean mark_texture_frame_available(FlTextureRegistrar* registrar, + FlTexture* texture) { + FlTextureRegistrarImpl* self = FL_TEXTURE_REGISTRAR_IMPL(registrar); if (self->engine == nullptr) { return FALSE; } - if (fl_texture_registrar_get_texture( - self, fl_texture_get_texture_id(texture)) == nullptr) { + if (lookup_texture(registrar, fl_texture_get_texture_id(texture)) == + nullptr) { g_warning("Unregistered texture %p", texture); return FALSE; } @@ -105,36 +124,9 @@ fl_texture_registrar_mark_texture_frame_available(FlTextureRegistrar* self, self->engine, fl_texture_get_texture_id(texture)); } -gboolean fl_texture_registrar_populate_gl_external_texture( - FlTextureRegistrar* self, - int64_t texture_id, - uint32_t width, - uint32_t height, - FlutterOpenGLTexture* opengl_texture, - GError** error) { - FlTexture* texture = fl_texture_registrar_get_texture(self, texture_id); - if (texture == nullptr) { - g_set_error(error, fl_engine_error_quark(), FL_ENGINE_ERROR_FAILED, - "Unable to find texture %" G_GINT64_FORMAT, texture_id); - return FALSE; - } - if (FL_IS_TEXTURE_GL(texture)) { - return fl_texture_gl_populate(FL_TEXTURE_GL(texture), width, height, - opengl_texture, error); - } else if (FL_IS_PIXEL_BUFFER_TEXTURE(texture)) { - return fl_pixel_buffer_texture_populate( - FL_PIXEL_BUFFER_TEXTURE(texture), width, height, opengl_texture, error); - } else { - g_set_error(error, fl_engine_error_quark(), FL_ENGINE_ERROR_FAILED, - "Unsupported texture type %" G_GINT64_FORMAT, texture_id); - return FALSE; - } -} - -G_MODULE_EXPORT gboolean -fl_texture_registrar_unregister_texture(FlTextureRegistrar* self, - FlTexture* texture) { - g_return_val_if_fail(FL_IS_TEXTURE_REGISTRAR(self), FALSE); +static gboolean unregister_texture(FlTextureRegistrar* registrar, + FlTexture* texture) { + FlTextureRegistrarImpl* self = FL_TEXTURE_REGISTRAR_IMPL(registrar); if (!g_hash_table_remove( self->textures, @@ -151,18 +143,62 @@ fl_texture_registrar_unregister_texture(FlTextureRegistrar* self, self->engine, fl_texture_get_texture_id(texture)); } -FlTexture* fl_texture_registrar_get_texture(FlTextureRegistrar* registrar, - int64_t texture_id) { - return reinterpret_cast( - g_hash_table_lookup(registrar->textures, GINT_TO_POINTER(texture_id))); +static void fl_texture_registrar_impl_iface_init( + FlTextureRegistrarInterface* iface) { + iface->register_texture = register_texture; + iface->lookup_texture = lookup_texture; + iface->mark_texture_frame_available = mark_texture_frame_available; + iface->unregister_texture = unregister_texture; +} + +static void fl_texture_registrar_impl_init(FlTextureRegistrarImpl* self) { + self->textures = g_hash_table_new_full(g_direct_hash, g_direct_equal, nullptr, + g_object_unref); +} + +G_MODULE_EXPORT gboolean +fl_texture_registrar_register_texture(FlTextureRegistrar* self, + FlTexture* texture) { + g_return_val_if_fail(FL_IS_TEXTURE_REGISTRAR(self), FALSE); + g_return_val_if_fail(FL_IS_TEXTURE(texture), FALSE); + + return FL_TEXTURE_REGISTRAR_GET_IFACE(self)->register_texture(self, texture); +} + +FlTexture* fl_texture_registrar_lookup_texture(FlTextureRegistrar* self, + int64_t texture_id) { + g_return_val_if_fail(FL_IS_TEXTURE_REGISTRAR(self), NULL); + + return FL_TEXTURE_REGISTRAR_GET_IFACE(self)->lookup_texture(self, texture_id); +} + +G_MODULE_EXPORT gboolean +fl_texture_registrar_mark_texture_frame_available(FlTextureRegistrar* self, + FlTexture* texture) { + g_return_val_if_fail(FL_IS_TEXTURE_REGISTRAR(self), FALSE); + + return FL_TEXTURE_REGISTRAR_GET_IFACE(self)->mark_texture_frame_available( + self, texture); +} + +G_MODULE_EXPORT gboolean +fl_texture_registrar_unregister_texture(FlTextureRegistrar* self, + FlTexture* texture) { + g_return_val_if_fail(FL_IS_TEXTURE_REGISTRAR(self), FALSE); + + return FL_TEXTURE_REGISTRAR_GET_IFACE(self)->unregister_texture(self, + texture); } FlTextureRegistrar* fl_texture_registrar_new(FlEngine* engine) { - FlTextureRegistrar* self = FL_TEXTURE_REGISTRAR( - g_object_new(fl_texture_registrar_get_type(), nullptr)); + FlTextureRegistrarImpl* self = FL_TEXTURE_REGISTRAR_IMPL( + g_object_new(fl_texture_registrar_impl_get_type(), nullptr)); + + // Added to stop compiler complaining about an unused function. + FL_IS_TEXTURE_REGISTRAR_IMPL(self); self->engine = engine; g_object_weak_ref(G_OBJECT(engine), engine_weak_notify_cb, self); - return self; + return FL_TEXTURE_REGISTRAR(self); } diff --git a/shell/platform/linux/fl_texture_registrar_private.h b/shell/platform/linux/fl_texture_registrar_private.h index b4ea9b2119219..3f1e69b7fef4b 100644 --- a/shell/platform/linux/fl_texture_registrar_private.h +++ b/shell/platform/linux/fl_texture_registrar_private.h @@ -22,38 +22,16 @@ G_BEGIN_DECLS FlTextureRegistrar* fl_texture_registrar_new(FlEngine* engine); /** - * fl_texture_registrar_populate_gl_external_texture: + * fl_texture_registrar_lookup_texture: * @registrar: an #FlTextureRegistrar. * @texture_id: ID of texture. - * @width: width of the texture. - * @height: height of the texture. - * @opengl_texture: (out): return an #FlutterOpenGLTexture. - * @error: (allow-none): #GError location to store the error occurring, or - * %NULL to ignore. * - * Attempts to populate the given @texture_id. + * Looks for the texture with the given ID. * - * Returns: %TRUE on success. + * Returns: an #FlTexture or %NULL if no texture with this ID. */ -gboolean fl_texture_registrar_populate_gl_external_texture( - FlTextureRegistrar* registrar, - int64_t texture_id, - uint32_t width, - uint32_t height, - FlutterOpenGLTexture* opengl_texture, - GError** error); - -/** - * fl_texture_registrar_get_texture: - * @registrar: an #FlTextureRegistrar. - * @texture_id: ID of texture. - * - * Gets a registered texture by @texture_id. - * - * Returns: an #FlTexture, or %NULL if not found. - */ -FlTexture* fl_texture_registrar_get_texture(FlTextureRegistrar* registrar, - int64_t texture_id); +FlTexture* fl_texture_registrar_lookup_texture(FlTextureRegistrar* registrar, + int64_t texture_id); G_END_DECLS diff --git a/shell/platform/linux/fl_texture_registrar_test.cc b/shell/platform/linux/fl_texture_registrar_test.cc index 6d9845ee59602..45ac18edfe2bc 100644 --- a/shell/platform/linux/fl_texture_registrar_test.cc +++ b/shell/platform/linux/fl_texture_registrar_test.cc @@ -8,6 +8,7 @@ #include "flutter/shell/platform/linux/public/flutter_linux/fl_pixel_buffer_texture.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_texture_gl.h" #include "flutter/shell/platform/linux/testing/fl_test.h" +#include "flutter/shell/platform/linux/testing/mock_texture_registrar.h" #include "gtest/gtest.h" #include @@ -64,6 +65,27 @@ static FlTestRegistrarTexture* fl_test_registrar_texture_new() { g_object_new(fl_test_registrar_texture_get_type(), nullptr)); } +// Checks can make a mock registrar. +TEST(FlTextureRegistrarTest, MockRegistrar) { + g_autoptr(FlTexture) texture = FL_TEXTURE(fl_test_registrar_texture_new()); + g_autoptr(FlMockTextureRegistrar) registrar = fl_mock_texture_registrar_new(); + EXPECT_TRUE(FL_IS_MOCK_TEXTURE_REGISTRAR(registrar)); + + EXPECT_TRUE(fl_texture_registrar_register_texture( + FL_TEXTURE_REGISTRAR(registrar), texture)); + EXPECT_EQ(fl_mock_texture_registrar_get_texture(registrar), texture); + EXPECT_EQ( + fl_texture_registrar_lookup_texture(FL_TEXTURE_REGISTRAR(registrar), + fl_texture_get_texture_id(texture)), + texture); + EXPECT_TRUE(fl_texture_registrar_mark_texture_frame_available( + FL_TEXTURE_REGISTRAR(registrar), texture)); + EXPECT_TRUE(fl_mock_texture_registrar_get_frame_available(registrar)); + EXPECT_TRUE(fl_texture_registrar_unregister_texture( + FL_TEXTURE_REGISTRAR(registrar), texture)); + EXPECT_EQ(fl_mock_texture_registrar_get_texture(registrar), nullptr); +} + // Test that registering a texture works. TEST(FlTextureRegistrarTest, RegisterTexture) { g_autoptr(FlEngine) engine = make_mock_engine(); @@ -87,19 +109,3 @@ TEST(FlTextureRegistrarTest, MarkTextureFrameAvailable) { EXPECT_TRUE( fl_texture_registrar_mark_texture_frame_available(registrar, texture)); } - -// Test that populating an OpenGL texture works. -TEST(FlTextureRegistrarTest, PopulateTexture) { - g_autoptr(FlEngine) engine = make_mock_engine(); - g_autoptr(FlTextureRegistrar) registrar = fl_texture_registrar_new(engine); - g_autoptr(FlTexture) texture = FL_TEXTURE(fl_test_registrar_texture_new()); - EXPECT_TRUE(fl_texture_registrar_register_texture(registrar, texture)); - FlutterOpenGLTexture opengl_texture; - g_autoptr(GError) error = nullptr; - EXPECT_TRUE(fl_texture_registrar_populate_gl_external_texture( - registrar, fl_texture_get_texture_id(texture), BUFFER_WIDTH, - BUFFER_HEIGHT, &opengl_texture, &error)); - EXPECT_EQ(error, nullptr); - EXPECT_EQ(opengl_texture.width, REAL_BUFFER_WIDTH); - EXPECT_EQ(opengl_texture.height, REAL_BUFFER_HEIGHT); -} diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc index 1fbdd633348ef..ca955d891fba3 100644 --- a/shell/platform/linux/fl_view.cc +++ b/shell/platform/linux/fl_view.cc @@ -102,7 +102,7 @@ static void fl_view_init_keyboard(FlView* self) { FL_KEY_RESPONDER(fl_key_channel_responder_new(messenger))); } -// Called when the engine is restarted. +// Invoked by the engine right before the engine is restarted. // // This method should reset states to be as if the engine had just been started, // which usually indicates the user has requested a hot restart (Shift-R in the @@ -375,8 +375,9 @@ static void fl_view_get_preferred_width(GtkWidget* widget, iterator = iterator->next) { FlViewChild* child = reinterpret_cast(iterator->data); - if (!gtk_widget_get_visible(child->widget)) + if (!gtk_widget_get_visible(child->widget)) { continue; + } gtk_widget_get_preferred_width(child->widget, &child_min, &child_nat); @@ -398,8 +399,9 @@ static void fl_view_get_preferred_height(GtkWidget* widget, iterator = iterator->next) { FlViewChild* child = reinterpret_cast(iterator->data); - if (!gtk_widget_get_visible(child->widget)) + if (!gtk_widget_get_visible(child->widget)) { continue; + } gtk_widget_get_preferred_height(child->widget, &child_min, &child_nat); @@ -416,17 +418,19 @@ static void fl_view_size_allocate(GtkWidget* widget, gtk_widget_set_allocation(widget, allocation); if (gtk_widget_get_has_window(widget)) { - if (gtk_widget_get_realized(widget)) + if (gtk_widget_get_realized(widget)) { gdk_window_move_resize(gtk_widget_get_window(widget), allocation->x, allocation->y, allocation->width, allocation->height); + } } for (GList* iterator = self->children_list; iterator; iterator = iterator->next) { FlViewChild* child = reinterpret_cast(iterator->data); - if (!gtk_widget_get_visible(child->widget)) + if (!gtk_widget_get_visible(child->widget)) { continue; + } GtkAllocation child_allocation = child->geometry; GtkRequisition child_requisition; @@ -758,10 +762,11 @@ static void fl_view_add_pending_child(FlView* self, GdkRectangle* geometry) { FlViewChild* child = g_new(FlViewChild, 1); child->widget = widget; - if (geometry) + if (geometry) { child->geometry = *geometry; - else + } else { child->geometry = {0, 0, 0, 0}; + } self->pending_children_list = g_list_append(self->pending_children_list, child); @@ -796,8 +801,9 @@ void fl_view_add_widget(FlView* view, GList* find_child(GList* list, GtkWidget* widget) { for (GList* i = list; i; i = i->next) { FlViewChild* child = reinterpret_cast(i->data); - if (child && child->widget == widget) + if (child && child->widget == widget) { return i; + } } return nullptr; } diff --git a/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h b/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h index 4f73986544762..84af654cada90 100644 --- a/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h +++ b/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h @@ -29,30 +29,17 @@ typedef enum { GQuark fl_binary_messenger_codec_error_quark(void) G_GNUC_CONST; -G_DECLARE_FINAL_TYPE(FlBinaryMessenger, - fl_binary_messenger, - FL, - BINARY_MESSENGER, - GObject) - -G_DECLARE_FINAL_TYPE(FlBinaryMessengerResponseHandle, - fl_binary_messenger_response_handle, - FL, - BINARY_MESSENGER_RESPONSE_HANDLE, - GObject) - -/** - * FlBinaryMessenger: - * - * #FlBinaryMessenger is an object that allows sending and receiving of platform - * messages with an #FlEngine. - */ - -/** - * FlBinaryMessengerResponseHandle: - * - * #FlBinaryMessengerResponseHandle is an object used to send responses with. - */ +G_DECLARE_INTERFACE(FlBinaryMessenger, + fl_binary_messenger, + FL, + BINARY_MESSENGER, + GObject) + +G_DECLARE_DERIVABLE_TYPE(FlBinaryMessengerResponseHandle, + fl_binary_messenger_response_handle, + FL, + BINARY_MESSENGER_RESPONSE_HANDLE, + GObject) /** * FlBinaryMessengerMessageHandler: @@ -76,6 +63,50 @@ typedef void (*FlBinaryMessengerMessageHandler)( FlBinaryMessengerResponseHandle* response_handle, gpointer user_data); +struct _FlBinaryMessengerInterface { + GTypeInterface parent_iface; + + void (*set_message_handler_on_channel)( + FlBinaryMessenger* messenger, + const gchar* channel, + FlBinaryMessengerMessageHandler handler, + gpointer user_data, + GDestroyNotify destroy_notify); + + gboolean (*send_response)(FlBinaryMessenger* messenger, + FlBinaryMessengerResponseHandle* response_handle, + GBytes* response, + GError** error); + + void (*send_on_channel)(FlBinaryMessenger* messenger, + const gchar* channel, + GBytes* message, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + + GBytes* (*send_on_channel_finish)(FlBinaryMessenger* messenger, + GAsyncResult* result, + GError** error); +}; + +struct _FlBinaryMessengerResponseHandleClass { + GObjectClass parent_class; +}; + +/** + * FlBinaryMessenger: + * + * #FlBinaryMessenger is an object that allows sending and receiving of platform + * messages with an #FlEngine. + */ + +/** + * FlBinaryMessengerResponseHandle: + * + * #FlBinaryMessengerResponseHandle is an object used to send responses with. + */ + /** * fl_binary_messenger_set_platform_message_handler: * @binary_messenger: an #FlBinaryMessenger. diff --git a/shell/platform/linux/public/flutter_linux/fl_plugin_registrar.h b/shell/platform/linux/public/flutter_linux/fl_plugin_registrar.h index 5ccd74752fa14..6382804024e71 100644 --- a/shell/platform/linux/public/flutter_linux/fl_plugin_registrar.h +++ b/shell/platform/linux/public/flutter_linux/fl_plugin_registrar.h @@ -17,11 +17,21 @@ G_BEGIN_DECLS -G_DECLARE_FINAL_TYPE(FlPluginRegistrar, - fl_plugin_registrar, - FL, - PLUGIN_REGISTRAR, - GObject) +G_DECLARE_INTERFACE(FlPluginRegistrar, + fl_plugin_registrar, + FL, + PLUGIN_REGISTRAR, + GObject) + +struct _FlPluginRegistrarInterface { + GTypeInterface parent_iface; + + FlBinaryMessenger* (*get_messenger)(FlPluginRegistrar* registrar); + + FlTextureRegistrar* (*get_texture_registrar)(FlPluginRegistrar* registrar); + + FlView* (*get_view)(FlPluginRegistrar* registrar); +}; /** * FlPluginRegistrar: diff --git a/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h b/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h index bfc860e7691ff..ac16aba26adac 100644 --- a/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h +++ b/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h @@ -35,7 +35,7 @@ struct _FlPluginRegistryInterface { * @registry: an #FlPluginRegistry. * @name: plugin name. * - * Gets the plugin registrar for the the plugin with @name. + * Gets the plugin registrar for the plugin with @name. * * Returns: (transfer full): an #FlPluginRegistrar. */ @@ -48,7 +48,7 @@ struct _FlPluginRegistryInterface { * @registry: an #FlPluginRegistry. * @name: plugin name. * - * Gets the plugin registrar for the the plugin with @name. + * Gets the plugin registrar for the plugin with @name. * * Returns: (transfer full): an #FlPluginRegistrar. */ diff --git a/shell/platform/linux/public/flutter_linux/fl_texture_registrar.h b/shell/platform/linux/public/flutter_linux/fl_texture_registrar.h index b978559a80b38..1c03cdc3ec731 100644 --- a/shell/platform/linux/public/flutter_linux/fl_texture_registrar.h +++ b/shell/platform/linux/public/flutter_linux/fl_texture_registrar.h @@ -16,11 +16,26 @@ G_BEGIN_DECLS -G_DECLARE_FINAL_TYPE(FlTextureRegistrar, - fl_texture_registrar, - FL, - TEXTURE_REGISTRAR, - GObject) +G_DECLARE_INTERFACE(FlTextureRegistrar, + fl_texture_registrar, + FL, + TEXTURE_REGISTRAR, + GObject) + +struct _FlTextureRegistrarInterface { + GTypeInterface parent_iface; + + gboolean (*register_texture)(FlTextureRegistrar* registrar, + FlTexture* texture); + + FlTexture* (*lookup_texture)(FlTextureRegistrar* registrar, int64_t id); + + gboolean (*mark_texture_frame_available)(FlTextureRegistrar* registrar, + FlTexture* texture); + + gboolean (*unregister_texture)(FlTextureRegistrar* registrar, + FlTexture* texture); +}; /** * FlTextureRegistrar: diff --git a/shell/platform/linux/testing/mock_binary_messenger_response_handle.cc b/shell/platform/linux/testing/mock_binary_messenger_response_handle.cc new file mode 100644 index 0000000000000..4bd7e419d2071 --- /dev/null +++ b/shell/platform/linux/testing/mock_binary_messenger_response_handle.cc @@ -0,0 +1,25 @@ +// 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/testing/mock_binary_messenger_response_handle.h" + +struct _FlMockBinaryMessengerResponseHandle { + FlBinaryMessengerResponseHandle parent_instance; +}; + +G_DEFINE_TYPE(FlMockBinaryMessengerResponseHandle, + fl_mock_binary_messenger_response_handle, + fl_binary_messenger_response_handle_get_type()); + +static void fl_mock_binary_messenger_response_handle_class_init( + FlMockBinaryMessengerResponseHandleClass* klass) {} + +static void fl_mock_binary_messenger_response_handle_init( + FlMockBinaryMessengerResponseHandle* self) {} + +FlMockBinaryMessengerResponseHandle* +fl_mock_binary_messenger_response_handle_new() { + return FL_MOCK_BINARY_MESSENGER_RESPONSE_HANDLE( + g_object_new(fl_mock_binary_messenger_response_handle_get_type(), NULL)); +} diff --git a/shell/platform/linux/testing/mock_binary_messenger_response_handle.h b/shell/platform/linux/testing/mock_binary_messenger_response_handle.h new file mode 100644 index 0000000000000..66372c7109249 --- /dev/null +++ b/shell/platform/linux/testing/mock_binary_messenger_response_handle.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. + +#include "flutter/shell/platform/linux/fl_binary_messenger_private.h" + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlMockBinaryMessengerResponseHandle, + fl_mock_binary_messenger_response_handle, + FL, + MOCK_BINARY_MESSENGER_RESPONSE_HANDLE, + FlBinaryMessengerResponseHandle) + +FlMockBinaryMessengerResponseHandle* +fl_mock_binary_messenger_response_handle_new(); + +G_END_DECLS diff --git a/shell/platform/linux/testing/mock_plugin_registrar.cc b/shell/platform/linux/testing/mock_plugin_registrar.cc new file mode 100644 index 0000000000000..30cf3f25dfabc --- /dev/null +++ b/shell/platform/linux/testing/mock_plugin_registrar.cc @@ -0,0 +1,70 @@ +// 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/testing/mock_plugin_registrar.h" + +struct _FlMockPluginRegistrar { + GObject parent_instance; + + FlBinaryMessenger* messenger; + FlTextureRegistrar* texture_registrar; +}; + +static void fl_mock_plugin_registrar_iface_init( + FlPluginRegistrarInterface* iface); + +G_DEFINE_TYPE_WITH_CODE( + FlMockPluginRegistrar, + fl_mock_plugin_registrar, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(fl_plugin_registrar_get_type(), + fl_mock_plugin_registrar_iface_init)) + +static void fl_mock_plugin_registrar_dispose(GObject* object) { + FlMockPluginRegistrar* self = FL_MOCK_PLUGIN_REGISTRAR(object); + + g_clear_object(&self->messenger); + g_clear_object(&self->texture_registrar); + + G_OBJECT_CLASS(fl_mock_plugin_registrar_parent_class)->dispose(object); +} + +static void fl_mock_plugin_registrar_class_init( + FlMockPluginRegistrarClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_mock_plugin_registrar_dispose; +} + +static FlBinaryMessenger* get_messenger(FlPluginRegistrar* registrar) { + FlMockPluginRegistrar* self = FL_MOCK_PLUGIN_REGISTRAR(registrar); + return self->messenger; +} + +static FlTextureRegistrar* get_texture_registrar(FlPluginRegistrar* registrar) { + FlMockPluginRegistrar* self = FL_MOCK_PLUGIN_REGISTRAR(registrar); + return self->texture_registrar; +} + +static FlView* get_view(FlPluginRegistrar* registrar) { + return NULL; +} + +static void fl_mock_plugin_registrar_iface_init( + FlPluginRegistrarInterface* iface) { + iface->get_messenger = get_messenger; + iface->get_texture_registrar = get_texture_registrar; + iface->get_view = get_view; +} + +static void fl_mock_plugin_registrar_init(FlMockPluginRegistrar* self) {} + +FlPluginRegistrar* fl_mock_plugin_registrar_new( + FlBinaryMessenger* messenger, + FlTextureRegistrar* texture_registrar) { + FlMockPluginRegistrar* registrar = FL_MOCK_PLUGIN_REGISTRAR( + g_object_new(fl_mock_plugin_registrar_get_type(), NULL)); + registrar->messenger = FL_BINARY_MESSENGER(g_object_ref(messenger)); + registrar->texture_registrar = + FL_TEXTURE_REGISTRAR(g_object_ref(texture_registrar)); + return FL_PLUGIN_REGISTRAR(registrar); +} diff --git a/shell/platform/linux/testing/mock_plugin_registrar.h b/shell/platform/linux/testing/mock_plugin_registrar.h new file mode 100644 index 0000000000000..dba77d6535c55 --- /dev/null +++ b/shell/platform/linux/testing/mock_plugin_registrar.h @@ -0,0 +1,21 @@ +// 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_binary_messenger.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registrar.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_texture_registrar.h" + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlMockPluginRegistrar, + fl_mock_plugin_registrar, + FL, + MOCK_PLUGIN_REGISTRAR, + GObject) + +FlPluginRegistrar* fl_mock_plugin_registrar_new( + FlBinaryMessenger* messenger, + FlTextureRegistrar* texture_registrar); + +G_END_DECLS diff --git a/shell/platform/linux/testing/mock_texture_registrar.cc b/shell/platform/linux/testing/mock_texture_registrar.cc new file mode 100644 index 0000000000000..66b5b8cfab9e9 --- /dev/null +++ b/shell/platform/linux/testing/mock_texture_registrar.cc @@ -0,0 +1,100 @@ +// 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/testing/mock_texture_registrar.h" +#include "flutter/shell/platform/linux/fl_texture_private.h" + +struct _FlMockTextureRegistrar { + GObject parent_instance; + FlTexture* texture; + gboolean frame_available; +}; + +static void fl_mock_texture_registrar_iface_init( + FlTextureRegistrarInterface* iface); + +G_DEFINE_TYPE_WITH_CODE( + FlMockTextureRegistrar, + fl_mock_texture_registrar, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(fl_texture_registrar_get_type(), + fl_mock_texture_registrar_iface_init)) + +static gboolean register_texture(FlTextureRegistrar* registrar, + FlTexture* texture) { + FlMockTextureRegistrar* self = FL_MOCK_TEXTURE_REGISTRAR(registrar); + if (self->texture != nullptr) { + return FALSE; + } + self->texture = FL_TEXTURE(g_object_ref(texture)); + return TRUE; +} + +static FlTexture* lookup_texture(FlTextureRegistrar* registrar, + int64_t texture_id) { + FlMockTextureRegistrar* self = FL_MOCK_TEXTURE_REGISTRAR(registrar); + if (self->texture != nullptr && + fl_texture_get_texture_id(self->texture) == texture_id) { + return self->texture; + } + return nullptr; +} + +static gboolean mark_texture_frame_available(FlTextureRegistrar* registrar, + FlTexture* texture) { + FlMockTextureRegistrar* self = FL_MOCK_TEXTURE_REGISTRAR(registrar); + if (lookup_texture(registrar, fl_texture_get_texture_id(texture)) == + nullptr) { + return FALSE; + } + self->frame_available = TRUE; + return TRUE; +} + +static gboolean unregister_texture(FlTextureRegistrar* registrar, + FlTexture* texture) { + FlMockTextureRegistrar* self = FL_MOCK_TEXTURE_REGISTRAR(registrar); + if (self->texture != texture) { + return FALSE; + } + + g_clear_object(&self->texture); + + return TRUE; +} + +static void fl_mock_texture_registrar_iface_init( + FlTextureRegistrarInterface* iface) { + iface->register_texture = register_texture; + iface->lookup_texture = lookup_texture; + iface->mark_texture_frame_available = mark_texture_frame_available; + iface->unregister_texture = unregister_texture; +} + +static void fl_mock_texture_registrar_dispose(GObject* object) { + FlMockTextureRegistrar* self = FL_MOCK_TEXTURE_REGISTRAR(object); + g_clear_object(&self->texture); + G_OBJECT_CLASS(fl_mock_texture_registrar_parent_class)->dispose(object); +} + +static void fl_mock_texture_registrar_class_init( + FlMockTextureRegistrarClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_mock_texture_registrar_dispose; +} + +static void fl_mock_texture_registrar_init(FlMockTextureRegistrar* self) {} + +FlMockTextureRegistrar* fl_mock_texture_registrar_new() { + return FL_MOCK_TEXTURE_REGISTRAR( + g_object_new(fl_mock_texture_registrar_get_type(), nullptr)); +} + +FlTexture* fl_mock_texture_registrar_get_texture(FlMockTextureRegistrar* self) { + return self->texture; +} + +gboolean fl_mock_texture_registrar_get_frame_available( + FlMockTextureRegistrar* self) { + return self->frame_available; +} diff --git a/shell/platform/linux/testing/mock_texture_registrar.h b/shell/platform/linux/testing/mock_texture_registrar.h new file mode 100644 index 0000000000000..65c617da128be --- /dev/null +++ b/shell/platform/linux/testing/mock_texture_registrar.h @@ -0,0 +1,23 @@ +// 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/fl_texture_registrar_private.h" + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlMockTextureRegistrar, + fl_mock_texture_registrar, + FL, + MOCK_TEXTURE_REGISTRAR, + GObject) + +FlMockTextureRegistrar* fl_mock_texture_registrar_new(); + +FlTexture* fl_mock_texture_registrar_get_texture( + FlMockTextureRegistrar* registrar); + +gboolean fl_mock_texture_registrar_get_frame_available( + FlMockTextureRegistrar* registrar); + +G_END_DECLS diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index 525e852577b6b..3cfcfdb81f0cc 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -77,6 +77,7 @@ source_set("flutter_windows_source") { "sequential_id_generator.cc", "sequential_id_generator.h", "system_utils.h", + "task_runner.cc", "task_runner.h", "text_input_plugin.cc", "text_input_plugin.h", @@ -226,6 +227,7 @@ executable("flutter_windows_unittests") { "sequential_id_generator_unittests.cc", "string_conversion_unittests.cc", "system_utils_unittests.cc", + "task_runner_unittests.cc", "testing/engine_modifier.h", "testing/mock_gl_functions.h", ] @@ -251,6 +253,8 @@ executable("flutter_windows_unittests") { "platform_handler_unittests.cc", "testing/flutter_window_win32_test.cc", "testing/flutter_window_win32_test.h", + "testing/mock_text_input_manager_win32.cc", + "testing/mock_text_input_manager_win32.h", "testing/mock_window_binding_handler.cc", "testing/mock_window_binding_handler.h", "testing/mock_window_win32.cc", diff --git a/shell/platform/windows/angle_surface_manager.cc b/shell/platform/windows/angle_surface_manager.cc index 6756a285d91d5..83dac6bd85a90 100644 --- a/shell/platform/windows/angle_surface_manager.cc +++ b/shell/platform/windows/angle_surface_manager.cc @@ -122,20 +122,10 @@ bool AngleSurfaceManager::Initialize() { EGL_NONE, }; - // These are used to request ANGLE's D3D9 renderer as a fallback if D3D11 - // is not available. - const EGLint d3d9_display_attributes[] = { - EGL_PLATFORM_ANGLE_TYPE_ANGLE, - EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE, - EGL_TRUE, - EGL_NONE, - }; - std::vector display_attributes_configs = { d3d11_display_attributes, d3d11_fl_9_3_display_attributes, d3d11_warp_display_attributes, - d3d9_display_attributes, }; PFNEGLGETPLATFORMDISPLAYEXTPROC egl_get_platform_display_EXT = @@ -147,7 +137,7 @@ bool AngleSurfaceManager::Initialize() { } // Attempt to initialize ANGLE's renderer in order of: D3D11, D3D11 Feature - // Level 9_3, D3D11 WARP and finally D3D9. + // Level 9_3 and finally D3D11 WARP. for (auto config : display_attributes_configs) { bool should_log = (config == display_attributes_configs.back()); if (InitializeEGL(egl_get_platform_display_EXT, config, should_log)) { diff --git a/shell/platform/windows/display_helper_winuwp.h b/shell/platform/windows/display_helper_winuwp.h index 4a839abcfe697..2b7572f5c86bc 100644 --- a/shell/platform/windows/display_helper_winuwp.h +++ b/shell/platform/windows/display_helper_winuwp.h @@ -68,11 +68,12 @@ class DisplayHelperWinUWP { // Is current context is executing on a large screen device. bool large_screen_device_ = false; - // Current X overscan compensation factor. - float render_target_x_offset_ = 1.0f; - - // Current Y overscan compensation factor. - float render_target_y_offset_ = 1.0f; + // Current overscan compensation factors. + // + // The default value is no offset from the window's top-left, used when + // large_screen_device_ is false (normal desktop). + float render_target_x_offset_ = 0.0f; + float render_target_y_offset_ = 0.0f; }; } // namespace flutter diff --git a/shell/platform/windows/flutter_window_win32.cc b/shell/platform/windows/flutter_window_win32.cc index 064eb711b2aa0..07a71e8ab6206 100644 --- a/shell/platform/windows/flutter_window_win32.cc +++ b/shell/platform/windows/flutter_window_win32.cc @@ -211,6 +211,10 @@ void FlutterWindowWin32::OnComposeChange(const std::u16string& text, binding_handler_delegate_->OnComposeChange(text, cursor_pos); } +void FlutterWindowWin32::OnUpdateSemanticsEnabled(bool enabled) { + binding_handler_delegate_->OnUpdateSemanticsEnabled(enabled); +} + void FlutterWindowWin32::OnScroll(double delta_x, double delta_y, FlutterPointerDeviceKind device_kind, @@ -232,6 +236,10 @@ void FlutterWindowWin32::OnCursorRectUpdated(const Rect& rect) { UpdateCursorRect(Rect(origin, size)); } +void FlutterWindowWin32::OnResetImeComposing() { + AbortImeComposing(); +} + bool FlutterWindowWin32::OnBitmapSurfaceUpdated(const void* allocation, size_t row_bytes, size_t height) { diff --git a/shell/platform/windows/flutter_window_win32.h b/shell/platform/windows/flutter_window_win32.h index 86969749d21a2..bb867e3b93451 100644 --- a/shell/platform/windows/flutter_window_win32.h +++ b/shell/platform/windows/flutter_window_win32.h @@ -89,6 +89,12 @@ class FlutterWindowWin32 : public WindowWin32, public WindowBindingHandler { // |FlutterWindowBindingHandler| void OnCursorRectUpdated(const Rect& rect) override; + // |FlutterWindowBindingHandler| + void OnResetImeComposing() override; + + // |WindowWin32| + void OnUpdateSemanticsEnabled(bool enabled) override; + // |WindowWin32| void OnScroll(double delta_x, double delta_y, diff --git a/shell/platform/windows/flutter_window_win32_unittests.cc b/shell/platform/windows/flutter_window_win32_unittests.cc index 84af45a610946..1df68eb7ca899 100644 --- a/shell/platform/windows/flutter_window_win32_unittests.cc +++ b/shell/platform/windows/flutter_window_win32_unittests.cc @@ -100,6 +100,7 @@ class SpyTextInputPlugin : public KeyboardHandlerBase, void(const std::u16string& text, int cursor_pos)); virtual void OnCursorRectUpdated(const Rect& rect) {} + virtual void OnResetImeComposing() {} private: std::unique_ptr real_implementation_; @@ -155,6 +156,7 @@ class MockFlutterWindowWin32 : public FlutterWindowWin32, MOCK_METHOD0(GetDpiScale, float()); MOCK_METHOD0(IsVisible, bool()); MOCK_METHOD1(UpdateCursorRect, void(const Rect&)); + MOCK_METHOD0(OnResetImeComposing, void()); protected: virtual BOOL Win32PeekMessage(LPMSG lpMsg, @@ -214,6 +216,7 @@ class MockWindowBindingHandlerDelegate : public WindowBindingHandlerDelegate { MOCK_METHOD0(OnComposeCommit, void()); MOCK_METHOD0(OnComposeEnd, void()); MOCK_METHOD2(OnComposeChange, void(const std::u16string&, int)); + MOCK_METHOD1(OnUpdateSemanticsEnabled, void(bool)); MOCK_METHOD7(OnScroll, void(double, double, @@ -231,10 +234,12 @@ class TestFlutterWindowsView : public FlutterWindowsView { public: TestFlutterWindowsView(std::unique_ptr window_binding, WPARAM virtual_key, - bool is_printable = true) + bool is_printable = true, + bool is_syskey = false) : FlutterWindowsView(std::move(window_binding)), virtual_key_(virtual_key), - is_printable_(is_printable) {} + is_printable_(is_printable), + is_syskey_(is_syskey) {} SpyKeyboardKeyHandler* key_event_handler; SpyTextInputPlugin* text_input_plugin; @@ -268,9 +273,10 @@ class TestFlutterWindowsView : public FlutterWindowsView { // Simulate the event loop by just sending the event sent to // "SendInput" directly to the window. const KEYBDINPUT kbdinput = pInputs->ki; - const UINT message = - (kbdinput.dwFlags & KEYEVENTF_KEYUP) ? WM_KEYUP : WM_KEYDOWN; const bool is_key_up = kbdinput.dwFlags & KEYEVENTF_KEYUP; + const UINT message = is_key_up ? (is_syskey_ ? WM_SYSKEYUP : WM_KEYUP) + : (is_syskey_ ? WM_SYSKEYDOWN : WM_KEYDOWN); + const LPARAM lparam = CreateKeyEventLparam( kbdinput.wScan, kbdinput.dwFlags & KEYEVENTF_EXTENDEDKEY, is_key_up); // Windows would normally fill in the virtual key code for us, so we @@ -294,6 +300,7 @@ class TestFlutterWindowsView : public FlutterWindowsView { std::vector pending_responds_; WPARAM virtual_key_; bool is_printable_; + bool is_syskey_; }; // The static value to return as the "handled" value from the framework for key @@ -381,6 +388,59 @@ TEST(FlutterWindowWin32Test, NonPrintableKeyDownPropagation) { } } +// Tests key event propagation of system (WM_SYSKEYDOWN) key down events. +TEST(FlutterWindowWin32Test, SystemKeyDownPropagation) { + ::testing::InSequence in_sequence; + + constexpr WPARAM virtual_key = VK_LEFT; + constexpr WPARAM scan_code = 10; + constexpr char32_t character = 0; + MockFlutterWindowWin32 win32window; + auto window_binding_handler = + std::make_unique<::testing::NiceMock>(); + TestFlutterWindowsView flutter_windows_view( + std::move(window_binding_handler), virtual_key, false /* is_printable */, + true /* is_syskey */); + win32window.SetView(&flutter_windows_view); + LPARAM lparam = CreateKeyEventLparam(scan_code, false, false); + + // Test an event not handled by the framework + { + test_response = false; + flutter_windows_view.SetEngine(std::move(GetTestEngine())); + EXPECT_CALL(*flutter_windows_view.key_event_handler, + KeyboardHook(_, virtual_key, scan_code, WM_SYSKEYDOWN, + character, false /* extended */, _)) + .Times(2) + .RetiresOnSaturation(); + EXPECT_CALL(*flutter_windows_view.text_input_plugin, + KeyboardHook(_, _, _, _, _, _, _)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*flutter_windows_view.key_event_handler, TextHook(_, _)) + .Times(0); + EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) + .Times(0); + win32window.InjectMessages( + 1, Win32Message{WM_SYSKEYDOWN, virtual_key, lparam, kWmResultDefault}); + flutter_windows_view.InjectPendingEvents(&win32window); + } + + // Test an event handled by the framework + { + test_response = true; + EXPECT_CALL(*flutter_windows_view.key_event_handler, + KeyboardHook(_, _, _, _, _, _, _)) + .Times(0); + EXPECT_CALL(*flutter_windows_view.text_input_plugin, + KeyboardHook(_, _, _, _, _, _, _)) + .Times(0); + win32window.InjectMessages( + 1, Win32Message{WM_SYSKEYDOWN, virtual_key, lparam, kWmResultDefault}); + flutter_windows_view.InjectPendingEvents(&win32window); + } +} + // Tests key event propagation of printable character key down events. These // differ from non-printable characters in that they follow a different code // path in the WndProc (HandleMessage), producing a follow-on WM_CHAR event. diff --git a/shell/platform/windows/flutter_window_winuwp.cc b/shell/platform/windows/flutter_window_winuwp.cc index 29781837fd8ee..7274e1af1d4c9 100644 --- a/shell/platform/windows/flutter_window_winuwp.cc +++ b/shell/platform/windows/flutter_window_winuwp.cc @@ -11,10 +11,11 @@ namespace flutter { // Multipler used to map controller velocity to an appropriate scroll input. static constexpr double kControllerScrollMultiplier = 3; -// TODO(clarkezone): Determine pointer ID in -// OnPointerPressed/OnPointerReleased/OnPointerMoved in order to support multi -// touch. See https://github.com/flutter/flutter/issues/70201 -static constexpr int32_t kDefaultPointerDeviceId = 0; +// Minimum pointer ID that gets emitted by the pointer ID generator. +static constexpr uint32_t kMinPointerId = 0; + +// Maximum pointer ID that gets emitted by the pointer ID generator. +static constexpr uint32_t kMaxPointerId = 128; // Maps a Flutter cursor name to a CoreCursor. // @@ -69,8 +70,8 @@ winrt::Windows::UI::Core::CoreCursor GetCursorByName( } // namespace FlutterWindowWinUWP::FlutterWindowWinUWP( - ABI::Windows::ApplicationModel::Core::CoreApplicationView* - applicationview) { + ABI::Windows::ApplicationModel::Core::CoreApplicationView* applicationview) + : pointer_id_generator_(kMinPointerId, kMaxPointerId) { winrt::Windows::ApplicationModel::Core::CoreApplicationView cav{nullptr}; winrt::copy_from_abi(cav, applicationview); @@ -133,6 +134,11 @@ void FlutterWindowWinUWP::OnCursorRectUpdated(const Rect& rect) { // TODO(cbracken): Implement IMM candidate window positioning. } +void FlutterWindowWinUWP::OnResetImeComposing() { + // TODO(cbracken): Cancel composing, close the candidates view, and clear the + // composing text. +} + void FlutterWindowWinUWP::OnWindowResized() {} FlutterWindowWinUWP::~FlutterWindowWinUWP() {} @@ -206,9 +212,10 @@ void FlutterWindowWinUWP::OnPointerPressed( double y = GetPosY(args); FlutterPointerDeviceKind device_kind = GetPointerDeviceKind(args); FlutterPointerMouseButtons mouse_button = GetPointerMouseButton(args); + auto pointer_id = GetPointerId(args); - binding_handler_delegate_->OnPointerDown( - x, y, device_kind, kDefaultPointerDeviceId, mouse_button); + binding_handler_delegate_->OnPointerDown(x, y, device_kind, pointer_id, + mouse_button); } void FlutterWindowWinUWP::OnPointerReleased( @@ -218,9 +225,11 @@ void FlutterWindowWinUWP::OnPointerReleased( double y = GetPosY(args); FlutterPointerDeviceKind device_kind = GetPointerDeviceKind(args); FlutterPointerMouseButtons mouse_button = GetPointerMouseButton(args); + auto pointer_id = GetPointerId(args); - binding_handler_delegate_->OnPointerUp(x, y, device_kind, - kDefaultPointerDeviceId, mouse_button); + binding_handler_delegate_->OnPointerUp(x, y, device_kind, pointer_id, + mouse_button); + ReleasePointer(args); } void FlutterWindowWinUWP::OnPointerMoved( @@ -229,9 +238,9 @@ void FlutterWindowWinUWP::OnPointerMoved( double x = GetPosX(args); double y = GetPosY(args); FlutterPointerDeviceKind device_kind = GetPointerDeviceKind(args); + auto pointer_id = GetPointerId(args); - binding_handler_delegate_->OnPointerMove(x, y, device_kind, - kDefaultPointerDeviceId); + binding_handler_delegate_->OnPointerMove(x, y, device_kind, pointer_id); } void FlutterWindowWinUWP::OnPointerWheelChanged( @@ -240,9 +249,10 @@ void FlutterWindowWinUWP::OnPointerWheelChanged( double x = GetPosX(args); double y = GetPosY(args); FlutterPointerDeviceKind device_kind = GetPointerDeviceKind(args); + auto pointer_id = GetPointerId(args); int delta = args.CurrentPoint().Properties().MouseWheelDelta(); binding_handler_delegate_->OnScroll(x, y, 0, -delta, 1, device_kind, - kDefaultPointerDeviceId); + pointer_id); } double FlutterWindowWinUWP::GetPosX( @@ -299,6 +309,17 @@ FlutterPointerMouseButtons FlutterWindowWinUWP::GetPointerMouseButton( return kFlutterPointerButtonMousePrimary; } +void FlutterWindowWinUWP::ReleasePointer( + winrt::Windows::UI::Core::PointerEventArgs const& args) { + pointer_id_generator_.ReleaseNumber(args.CurrentPoint().PointerId()); +} + +uint32_t FlutterWindowWinUWP::GetPointerId( + winrt::Windows::UI::Core::PointerEventArgs const& args) { + // Generate a mapped ID in the interval [kMinPointerId, kMaxPointerId]. + return pointer_id_generator_.GetGeneratedId(args.CurrentPoint().PointerId()); +} + void FlutterWindowWinUWP::OnBoundsChanged( winrt::Windows::UI::ViewManagement::ApplicationView const& app_view, winrt::Windows::Foundation::IInspectable const&) { diff --git a/shell/platform/windows/flutter_window_winuwp.h b/shell/platform/windows/flutter_window_winuwp.h index 235caf409fec0..065841fc2a972 100644 --- a/shell/platform/windows/flutter_window_winuwp.h +++ b/shell/platform/windows/flutter_window_winuwp.h @@ -17,6 +17,7 @@ #include "flutter/shell/platform/windows/display_helper_winuwp.h" #include "flutter/shell/platform/windows/flutter_windows_view.h" #include "flutter/shell/platform/windows/game_pad_cursor_winuwp.h" +#include "flutter/shell/platform/windows/sequential_id_generator.h" namespace flutter { @@ -55,6 +56,9 @@ class FlutterWindowWinUWP : public WindowBindingHandler { // |WindowBindingHandler| void OnCursorRectUpdated(const Rect& rect) override; + // |FlutterWindowBindingHandler| + void OnResetImeComposing() override; + // |WindowBindingHandler| void OnWindowResized() override; @@ -129,6 +133,12 @@ class FlutterWindowWinUWP : public WindowBindingHandler { FlutterPointerDeviceKind GetPointerDeviceKind( winrt::Windows::UI::Core::PointerEventArgs const& args); + // Gets the pointer ID. + uint32_t GetPointerId(winrt::Windows::UI::Core::PointerEventArgs const& args); + + // Releases the pointer from the ID generator. + void ReleasePointer(winrt::Windows::UI::Core::PointerEventArgs const& args); + // Gets the mouse button. FlutterPointerMouseButtons GetPointerMouseButton( winrt::Windows::UI::Core::PointerEventArgs const& args); @@ -167,6 +177,9 @@ class FlutterWindowWinUWP : public WindowBindingHandler { // DisplayHelper object used to determine window bounds, DPI etc. std::unique_ptr display_helper_ = {nullptr}; + + // Generates pointer IDs for pointer events. + SequentialIdGenerator pointer_id_generator_; }; } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc index 71897bc727f68..bb020d52f4ccc 100644 --- a/shell/platform/windows/flutter_windows_engine.cc +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -143,8 +143,7 @@ FlutterWindowsEngine::FlutterWindowsEngine(const FlutterProjectBundle& project) FlutterEngineGetProcAddresses(&embedder_api_); task_runner_ = TaskRunner::Create( - GetCurrentThreadId(), embedder_api_.GetCurrentTime, - [this](const auto* task) { + embedder_api_.GetCurrentTime, [this](const auto* task) { if (!engine_) { std::cerr << "Cannot post an engine task when engine is not running." << std::endl; @@ -444,4 +443,19 @@ bool FlutterWindowsEngine::MarkExternalTextureFrameAvailable( engine_, texture_id) == kSuccess); } +bool FlutterWindowsEngine::DispatchSemanticsAction( + uint64_t target, + FlutterSemanticsAction action, + const std::vector& data) { + return (embedder_api_.DispatchSemanticsAction( + engine_, target, action, data.data(), data.size()) == kSuccess); +} + +void FlutterWindowsEngine::UpdateSemanticsEnabled(bool enabled) { + if (engine_ && semantics_enabled_ != enabled) { + semantics_enabled_ = enabled; + embedder_api_.UpdateSemanticsEnabled(engine_, enabled); + } +} + } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h index 40087c7c14519..22e75149e9349 100644 --- a/shell/platform/windows/flutter_windows_engine.h +++ b/shell/platform/windows/flutter_windows_engine.h @@ -144,6 +144,17 @@ class FlutterWindowsEngine { // given |texture_id|. bool MarkExternalTextureFrameAvailable(int64_t texture_id); + // Dispatches a semantics action to the specified semantics node. + bool DispatchSemanticsAction(uint64_t id, + FlutterSemanticsAction action, + const std::vector& data); + + // Informs the engine that the semantics enabled state has changed. + void UpdateSemanticsEnabled(bool enabled); + + // Returns true if the semantics tree is enabled. + bool semantics_enabled() const { return semantics_enabled_; } + private: // Allows swapping out embedder_api_ calls in tests. friend class EngineModifier; @@ -201,6 +212,8 @@ class FlutterWindowsEngine { FlutterDesktopOnPluginRegistrarDestroyed plugin_registrar_destruction_callback_ = nullptr; + bool semantics_enabled_ = false; + #ifndef WINUWP // The manager for WindowProc delegate registration and callbacks. std::unique_ptr window_proc_delegate_manager_; diff --git a/shell/platform/windows/flutter_windows_engine_unittests.cc b/shell/platform/windows/flutter_windows_engine_unittests.cc index 5e24d26e628b0..6f40e3ae9b269 100644 --- a/shell/platform/windows/flutter_windows_engine_unittests.cc +++ b/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -231,5 +231,29 @@ TEST(FlutterWindowsEngine, SendPlatformMessageWithResponse) { EXPECT_TRUE(send_message_called); } +TEST(FlutterWindowsEngine, DispatchSemanticsAction) { + std::unique_ptr engine = GetTestEngine(); + EngineModifier modifier(engine.get()); + + bool called = false; + std::string test_data = "Hello"; + modifier.embedder_api().DispatchSemanticsAction = MOCK_ENGINE_PROC( + DispatchSemanticsAction, + ([&called, &test_data](auto engine, auto target, auto action, auto data, + auto data_length) { + called = true; + EXPECT_EQ(target, 42); + EXPECT_EQ(action, kFlutterSemanticsActionDismiss); + EXPECT_EQ(memcmp(data, test_data.c_str(), test_data.size()), 0); + EXPECT_EQ(data_length, test_data.size()); + return kSuccess; + })); + + engine->DispatchSemanticsAction( + 42, kFlutterSemanticsActionDismiss, + std::vector(test_data.begin(), test_data.end())); + EXPECT_TRUE(called); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_view.cc b/shell/platform/windows/flutter_windows_view.cc index 268fa29606387..91783fcdfadb3 100644 --- a/shell/platform/windows/flutter_windows_view.cc +++ b/shell/platform/windows/flutter_windows_view.cc @@ -248,10 +248,18 @@ void FlutterWindowsView::OnPlatformBrightnessChanged() { SendPlatformBrightnessChanged(); } +void FlutterWindowsView::OnUpdateSemanticsEnabled(bool enabled) { + engine_->UpdateSemanticsEnabled(enabled); +} + void FlutterWindowsView::OnCursorRectUpdated(const Rect& rect) { binding_handler_->OnCursorRectUpdated(rect); } +void FlutterWindowsView::OnResetImeComposing() { + binding_handler_->OnResetImeComposing(); +} + void FlutterWindowsView::InitializeKeyboard() { auto internal_plugin_messenger = internal_plugin_registrar_->messenger(); #ifdef WINUWP diff --git a/shell/platform/windows/flutter_windows_view.h b/shell/platform/windows/flutter_windows_view.h index 2fc4a83af7219..c18032e709eca 100644 --- a/shell/platform/windows/flutter_windows_view.h +++ b/shell/platform/windows/flutter_windows_view.h @@ -90,7 +90,7 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, // Returns the frame buffer id for the engine to render to. uint32_t GetFrameBufferId(size_t width, size_t height); - // Called when the engine is restarted. + // Invoked by the engine right before the engine is restarted. // // This should reset necessary states to as if the view has just been // created. This is typically caused by a hot restart (Shift-R in CLI.) @@ -158,9 +158,15 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, // |WindowBindingHandlerDelegate| void OnPlatformBrightnessChanged() override; + // |WindowBindingHandlerDelegate| + virtual void OnUpdateSemanticsEnabled(bool enabled) override; + // |TextInputPluginDelegate| void OnCursorRectUpdated(const Rect& rect) override; + // |TextInputPluginDelegate| + void OnResetImeComposing() override; + protected: // Called to create the keyboard hook handlers. // @@ -310,7 +316,7 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, std::unique_ptr engine_; // Keeps track of pointer states in relation to the window. - std::map> pointer_states_; + std::unordered_map> pointer_states_; // The plugin registrar managing internal plugins. std::unique_ptr internal_plugin_registrar_; @@ -343,6 +349,9 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, // Target for the window width. Valid when resize_pending_ is set. Guarded by // resize_mutex_. size_t resize_target_height_ = 0; + + // True when flutter's semantics tree is enabled. + bool semantics_enabled_ = false; }; } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_view_unittests.cc b/shell/platform/windows/flutter_windows_view_unittests.cc index 72a4671638e5f..77343522b2363 100644 --- a/shell/platform/windows/flutter_windows_view_unittests.cc +++ b/shell/platform/windows/flutter_windows_view_unittests.cc @@ -35,6 +35,7 @@ struct TestResponseHandle { }; static bool test_response = false; +static bool semantics_enabled = false; constexpr uint64_t kKeyEventFromChannel = 0x11; constexpr uint64_t kKeyEventFromEmbedder = 0x22; @@ -126,5 +127,23 @@ TEST(FlutterWindowsViewTest, RestartClearsKeyboardState) { key_event_logs.clear(); } +TEST(FlutterWindowsViewTest, EnableSemantics) { + std::unique_ptr engine = GetTestEngine(); + EngineModifier modifier(engine.get()); + modifier.embedder_api().UpdateSemanticsEnabled = + [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) { + semantics_enabled = enabled; + return kSuccess; + }; + + auto window_binding_handler = + std::make_unique<::testing::NiceMock>(); + FlutterWindowsView view(std::move(window_binding_handler)); + view.SetEngine(std::move(engine)); + + view.OnUpdateSemanticsEnabled(true); + EXPECT_TRUE(semantics_enabled); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/keyboard_key_channel_handler.cc b/shell/platform/windows/keyboard_key_channel_handler.cc index 6885431681880..a3fefd800e9dd 100644 --- a/shell/platform/windows/keyboard_key_channel_handler.cc +++ b/shell/platform/windows/keyboard_key_channel_handler.cc @@ -43,7 +43,7 @@ static constexpr int kMaxPendingEvents = 1000; // the same scancode as its non-extended counterpart, such as ShiftLeft. In // Chromium's scancode table, from which Flutter's physical key list is // derived, these keys are marked with this bit. See -// https://chromium.googlesource.com/codesearch/chromium/src/+/refs/heads/master/ui/events/keycodes/dom/dom_code_data.inc +// https://chromium.googlesource.com/codesearch/chromium/src/+/refs/heads/main/ui/events/keycodes/dom/dom_code_data.inc static constexpr int kScancodeExtended = 0xe000; // Re-definition of the modifiers for compatibility with the Flutter framework. @@ -189,9 +189,11 @@ void KeyboardKeyChannelHandler::KeyboardHook( event.AddMember(kModifiersKey, GetModsForKeyState(), allocator); switch (action) { + case WM_SYSKEYDOWN: case WM_KEYDOWN: event.AddMember(kTypeKey, kKeyDown, allocator); break; + case WM_SYSKEYUP: case WM_KEYUP: event.AddMember(kTypeKey, kKeyUp, allocator); break; @@ -204,7 +206,7 @@ void KeyboardKeyChannelHandler::KeyboardHook( size_t reply_size) { auto decoded = flutter::JsonMessageCodec::GetInstance().DecodeMessage( reply, reply_size); - bool handled = (*decoded)[kHandledKey].GetBool(); + bool handled = decoded ? (*decoded)[kHandledKey].GetBool() : false; callback(handled); }); } diff --git a/shell/platform/windows/keyboard_key_channel_handler_unittests.cc b/shell/platform/windows/keyboard_key_channel_handler_unittests.cc index 1359d524c274d..0fd63eccb0d52 100644 --- a/shell/platform/windows/keyboard_key_channel_handler_unittests.cc +++ b/shell/platform/windows/keyboard_key_channel_handler_unittests.cc @@ -67,6 +67,22 @@ TEST(KeyboardKeyChannelHandlerTest, KeyboardHookHandling) { [&last_handled](bool handled) { last_handled = handled; }); EXPECT_EQ(received_scancode, kUnhandledScanCode); EXPECT_EQ(last_handled, false); + + received_scancode = 0; + + handler.KeyboardHook( + 64, kHandledScanCode, WM_SYSKEYDOWN, L'a', false, false, + [&last_handled](bool handled) { last_handled = handled; }); + EXPECT_EQ(received_scancode, kHandledScanCode); + EXPECT_EQ(last_handled, true); + + received_scancode = 0; + + handler.KeyboardHook( + 64, kUnhandledScanCode, WM_SYSKEYDOWN, L'c', false, false, + [&last_handled](bool handled) { last_handled = handled; }); + EXPECT_EQ(received_scancode, kUnhandledScanCode); + EXPECT_EQ(last_handled, false); } TEST(KeyboardKeyChannelHandlerTest, ExtendedKeysAreSentToRedispatch) { @@ -110,19 +126,16 @@ TEST(KeyboardKeyChannelHandlerTest, ExtendedKeysAreSentToRedispatch) { } TEST(KeyboardKeyChannelHandlerTest, DeadKeysDoNotCrash) { - auto handled_message = CreateResponse(true); - auto unhandled_message = CreateResponse(false); - int received_scancode = 0; - + bool received = false; TestBinaryMessenger messenger( - [&received_scancode, &handled_message, &unhandled_message]( - const std::string& channel, const uint8_t* message, - size_t message_size, BinaryReply reply) { + [&received](const std::string& channel, const uint8_t* message, + size_t message_size, BinaryReply reply) { if (channel == "flutter/keyevent") { auto message_doc = JsonMessageCodec::GetInstance().DecodeMessage( message, message_size); uint32_t character = (*message_doc)[kCharacterCodePointKey].GetUint(); EXPECT_EQ(character, (uint32_t)'^'); + received = true; } return true; }); @@ -133,6 +146,30 @@ TEST(KeyboardKeyChannelHandlerTest, DeadKeysDoNotCrash) { [](bool handled) {}); // EXPECT is done during the callback above. + EXPECT_TRUE(received); +} + +TEST(KeyboardKeyChannelHandlerTest, EmptyResponsesDoNotCrash) { + bool received = false; + TestBinaryMessenger messenger( + [&received](const std::string& channel, const uint8_t* message, + size_t message_size, BinaryReply reply) { + if (channel == "flutter/keyevent") { + std::string empty_message = ""; + std::vector empty_response(empty_message.begin(), + empty_message.end()); + reply(empty_response.data(), empty_response.size()); + received = true; + } + return true; + }); + + KeyboardKeyChannelHandler handler(&messenger); + handler.KeyboardHook(64, kUnhandledScanCode, WM_KEYDOWN, L'b', false, false, + [](bool handled) {}); + + // Passes if it does not crash. + EXPECT_TRUE(received); } } // namespace testing diff --git a/shell/platform/windows/keyboard_key_embedder_handler.cc b/shell/platform/windows/keyboard_key_embedder_handler.cc index b30a17c279b19..abca2003c69cb 100644 --- a/shell/platform/windows/keyboard_key_embedder_handler.cc +++ b/shell/platform/windows/keyboard_key_embedder_handler.cc @@ -167,8 +167,9 @@ void KeyboardKeyEmbedderHandler::KeyboardHookImpl( std::function callback) { const uint64_t physical_key = GetPhysicalKey(scancode, extended); const uint64_t logical_key = GetLogicalKey(key, extended, scancode); - assert(action == WM_KEYDOWN || action == WM_KEYUP); - const bool is_physical_down = action == WM_KEYDOWN; + assert(action == WM_KEYDOWN || action == WM_KEYUP || + action == WM_SYSKEYDOWN || action == WM_SYSKEYUP); + const bool is_physical_down = action == WM_KEYDOWN || action == WM_SYSKEYDOWN; auto last_logical_record_iter = pressingRecords_.find(physical_key); const bool had_record = last_logical_record_iter != pressingRecords_.end(); @@ -179,9 +180,9 @@ void KeyboardKeyEmbedderHandler::KeyboardHookImpl( FlutterKeyEventType type; // The resulting event's `logical_key`. uint64_t result_logical_key; - // The next value of pressingRecords_[physical_key] (or to remove it). - uint64_t next_logical_record; - bool next_has_record = true; + // What pressingRecords_[physical_key] should be after the KeyboardHookImpl + // returns (0 if the entry should be removed). + uint64_t eventual_logical_record; char character_bytes[kCharacterCacheSize]; character = _UndeadChar(character); @@ -193,7 +194,7 @@ void KeyboardKeyEmbedderHandler::KeyboardHookImpl( type = kFlutterKeyEventTypeRepeat; assert(had_record); ConvertUtf32ToUtf8_(character_bytes, character); - next_logical_record = last_logical_record; + eventual_logical_record = last_logical_record; result_logical_key = last_logical_record; } else { // A non-repeated key has been pressed that has the exact physical key @@ -208,7 +209,7 @@ void KeyboardKeyEmbedderHandler::KeyboardHookImpl( type = kFlutterKeyEventTypeDown; assert(!had_record); ConvertUtf32ToUtf8_(character_bytes, character); - next_logical_record = logical_key; + eventual_logical_record = logical_key; result_logical_key = logical_key; } } else { // isPhysicalDown is false @@ -224,22 +225,37 @@ void KeyboardKeyEmbedderHandler::KeyboardHookImpl( assert(had_record); // Up events never have character. character_bytes[0] = '\0'; - next_has_record = false; + eventual_logical_record = 0; result_logical_key = last_logical_record; } } UpdateLastSeenCritialKey(key, physical_key, result_logical_key); - SynchronizeCritialToggledStates(type == kFlutterKeyEventTypeDown ? key : 0); - - if (next_has_record) { - pressingRecords_[physical_key] = next_logical_record; + // Synchronize the toggled states of critical keys (such as whether CapsLocks + // is enabled). Toggled states can only be changed upon a down event, so if + // the recorded toggled state does not match the true state, this function + // will synthesize (an up event if the key is recorded pressed, then) a down + // event. + // + // After this function, all critical keys will have their toggled state + // updated to the true state, while the critical keys whose toggled state have + // been changed will be pressed regardless of their true pressed state. + // Updating the pressed state will be done by SynchronizeCritialPressedStates. + SynchronizeCritialToggledStates(key, type == kFlutterKeyEventTypeDown); + // Synchronize the pressed states of critical keys (such as whether CapsLocks + // is pressed). + // + // After this function, all critical keys except for the target key will have + // their toggled state and pressed state matched with their true states. The + // target key's pressed state will be updated immediately after this. + SynchronizeCritialPressedStates(key, type != kFlutterKeyEventTypeRepeat); + + if (eventual_logical_record != 0) { + pressingRecords_[physical_key] = eventual_logical_record; } else { pressingRecords_.erase(last_logical_record_iter); } - SynchronizeCritialPressedStates(); - if (result_logical_key == VK_PROCESSKEY) { // VK_PROCESSKEY means that the key press is used by an IME. These key // presses are considered handled and not sent to Flutter. These events must @@ -322,7 +338,8 @@ void KeyboardKeyEmbedderHandler::UpdateLastSeenCritialKey( } void KeyboardKeyEmbedderHandler::SynchronizeCritialToggledStates( - int toggle_virtual_key) { + int this_virtual_key, + bool is_down_event) { // TODO(dkwingsmt) consider adding support for synchronizing key state for UWP // https://github.com/flutter/flutter/issues/70202 #ifdef WINUWP @@ -341,7 +358,7 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialToggledStates( if (key_info.check_toggled) { SHORT state = get_key_state_(virtual_key); bool should_toggled = state & kStateMaskToggled; - if (virtual_key == toggle_virtual_key) { + if (virtual_key == this_virtual_key && is_down_event) { key_info.toggled_on = !key_info.toggled_on; } if (key_info.toggled_on != should_toggled) { @@ -352,10 +369,9 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialToggledStates( kFlutterKeyEventTypeUp, key_info.physical_key, key_info.logical_key, empty_character), nullptr, nullptr); - } else { - // This key will always be pressed in the following synthesized event. - pressingRecords_[key_info.physical_key] = key_info.logical_key; } + // Synchronizing toggle state always ends with the key being pressed. + pressingRecords_[key_info.physical_key] = key_info.logical_key; SendEvent(SynthesizeSimpleEvent(kFlutterKeyEventTypeDown, key_info.physical_key, key_info.logical_key, empty_character), @@ -367,7 +383,9 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialToggledStates( #endif } -void KeyboardKeyEmbedderHandler::SynchronizeCritialPressedStates() { +void KeyboardKeyEmbedderHandler::SynchronizeCritialPressedStates( + int this_virtual_key, + bool pressed_state_will_change) { // TODO(dkwingsmt) consider adding support for synchronizing key state for UWP // https://github.com/flutter/flutter/issues/70202 #ifdef WINUWP @@ -386,16 +404,19 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialPressedStates() { auto recorded_pressed_iter = pressingRecords_.find(key_info.physical_key); bool recorded_pressed = recorded_pressed_iter != pressingRecords_.end(); bool should_pressed = state & kStateMaskPressed; + if (virtual_key == this_virtual_key && pressed_state_will_change) { + should_pressed = !should_pressed; + } if (recorded_pressed != should_pressed) { - if (should_pressed) { - pressingRecords_[key_info.physical_key] = key_info.logical_key; - } else { + if (recorded_pressed) { pressingRecords_.erase(recorded_pressed_iter); + } else { + pressingRecords_[key_info.physical_key] = key_info.logical_key; } const char* empty_character = ""; SendEvent( - SynthesizeSimpleEvent(should_pressed ? kFlutterKeyEventTypeDown - : kFlutterKeyEventTypeUp, + SynthesizeSimpleEvent(recorded_pressed ? kFlutterKeyEventTypeUp + : kFlutterKeyEventTypeDown, key_info.physical_key, key_info.logical_key, empty_character), nullptr, nullptr); diff --git a/shell/platform/windows/keyboard_key_embedder_handler.h b/shell/platform/windows/keyboard_key_embedder_handler.h index d7fc69ede952c..1d2ea9de0b603 100644 --- a/shell/platform/windows/keyboard_key_embedder_handler.h +++ b/shell/platform/windows/keyboard_key_embedder_handler.h @@ -107,10 +107,10 @@ class KeyboardKeyEmbedderHandler uint64_t logical_key); // Check each key's state from |get_key_state_| and synthesize events // if their toggling states have been desynchronized. - void SynchronizeCritialToggledStates(int this_virtual_key); + void SynchronizeCritialToggledStates(int virtual_key, bool is_down); // Check each key's state from |get_key_state_| and synthesize events // if their pressing states have been desynchronized. - void SynchronizeCritialPressedStates(); + void SynchronizeCritialPressedStates(int virtual_key, bool was_down); // Wraps perform_send_event_ with state tracking. Use this instead of // |perform_send_event_| to send events to the framework. diff --git a/shell/platform/windows/keyboard_key_embedder_handler_unittests.cc b/shell/platform/windows/keyboard_key_embedder_handler_unittests.cc index 4012f0435ef89..0345f7c81a9ff 100644 --- a/shell/platform/windows/keyboard_key_embedder_handler_unittests.cc +++ b/shell/platform/windows/keyboard_key_embedder_handler_unittests.cc @@ -72,6 +72,7 @@ namespace testing { namespace { constexpr uint64_t kScanCodeKeyA = 0x1e; +constexpr uint64_t kScanCodeAltLeft = 0x38; constexpr uint64_t kScanCodeNumpad1 = 0x4f; constexpr uint64_t kScanCodeNumLock = 0x45; constexpr uint64_t kScanCodeControl = 0x1d; @@ -79,6 +80,7 @@ constexpr uint64_t kScanCodeShiftLeft = 0x2a; constexpr uint64_t kScanCodeShiftRight = 0x36; constexpr uint64_t kVirtualKeyA = 0x41; +constexpr uint64_t kVirtualAltLeft = 0xa4; using namespace ::flutter::testing::keycodes; } // namespace @@ -323,8 +325,9 @@ TEST(KeyboardKeyEmbedderHandlerTest, ImeEventsAreIgnored) { EXPECT_EQ(last_handled, true); } -// Test if modifier keys that are told apart by the extended bit -// can be identified. +// Test if modifier keys that are told apart by the extended bit can be +// identified. (Their physical keys must be searched with the extended bit +// considered.) TEST(KeyboardKeyEmbedderHandlerTest, ModifierKeysByExtendedBit) { TestKeystate key_state; std::vector results; @@ -343,7 +346,7 @@ TEST(KeyboardKeyEmbedderHandlerTest, ModifierKeysByExtendedBit) { last_handled = false; key_state.Set(VK_LCONTROL, true); handler->KeyboardHook( - VK_CONTROL, kScanCodeControl, WM_KEYDOWN, 0, false, false, + VK_LCONTROL, kScanCodeControl, WM_KEYDOWN, 0, false, false, [&last_handled](bool handled) { last_handled = handled; }); EXPECT_EQ(last_handled, false); EXPECT_EQ(results.size(), 1); @@ -362,7 +365,7 @@ TEST(KeyboardKeyEmbedderHandlerTest, ModifierKeysByExtendedBit) { last_handled = false; key_state.Set(VK_RCONTROL, true); handler->KeyboardHook( - VK_CONTROL, kScanCodeControl, WM_KEYDOWN, 0, true, true, + VK_RCONTROL, kScanCodeControl, WM_KEYDOWN, 0, true, true, [&last_handled](bool handled) { last_handled = handled; }); EXPECT_EQ(last_handled, false); EXPECT_EQ(results.size(), 1); @@ -381,7 +384,7 @@ TEST(KeyboardKeyEmbedderHandlerTest, ModifierKeysByExtendedBit) { last_handled = false; key_state.Set(VK_LCONTROL, false); handler->KeyboardHook( - VK_CONTROL, kScanCodeControl, WM_KEYUP, 0, false, true, + VK_LCONTROL, kScanCodeControl, WM_KEYUP, 0, false, true, [&last_handled](bool handled) { last_handled = handled; }); EXPECT_EQ(last_handled, false); EXPECT_EQ(results.size(), 1); @@ -400,7 +403,7 @@ TEST(KeyboardKeyEmbedderHandlerTest, ModifierKeysByExtendedBit) { last_handled = false; key_state.Set(VK_RCONTROL, false); handler->KeyboardHook( - VK_CONTROL, kScanCodeControl, WM_KEYUP, 0, true, true, + VK_RCONTROL, kScanCodeControl, WM_KEYUP, 0, true, true, [&last_handled](bool handled) { last_handled = handled; }); EXPECT_EQ(last_handled, false); EXPECT_EQ(results.size(), 1); @@ -852,7 +855,8 @@ TEST(KeyboardKeyEmbedderHandlerTest, SynthesizeForDesyncToggledState) { event->callback(false, event->user_data); } -TEST(KeyboardKeyEmbedderHandlerTest, SynthesizeForDesyncToggledStateByItself) { +TEST(KeyboardKeyEmbedderHandlerTest, + SynthesizeForDesyncToggledStateByItselfsUp) { TestKeystate key_state; std::vector results; TestFlutterKeyEvent* event; @@ -911,6 +915,61 @@ TEST(KeyboardKeyEmbedderHandlerTest, SynthesizeForDesyncToggledStateByItself) { EXPECT_EQ(last_handled, true); } +TEST(KeyboardKeyEmbedderHandlerTest, + SynthesizeForDesyncToggledStateByItselfsDown) { + TestKeystate key_state; + std::vector results; + TestFlutterKeyEvent* event; + bool last_handled = false; + + // NumLock is started up and disabled + key_state.Set(VK_NUMLOCK, false, false); + std::unique_ptr handler = + std::make_unique( + [&results](const FlutterKeyEvent& event, + FlutterKeyEventCallback callback, void* user_data) { + results.emplace_back(event, callback, user_data); + }, + key_state.Getter()); + + // NumLock is toggled somewhere else + // key_state.Set(VK_NUMLOCK, false, true); + + // NumLock is pressed + key_state.Set(VK_NUMLOCK, true, false); + handler->KeyboardHook( + VK_NUMLOCK, kScanCodeNumLock, WM_KEYDOWN, 0, true, false, + [&last_handled](bool handled) { last_handled = handled; }); + EXPECT_EQ(last_handled, false); + EXPECT_EQ(results.size(), 3); + event = &results[0]; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalNumLock); + EXPECT_EQ(event->logical, kLogicalNumLock); + EXPECT_STREQ(event->character, ""); + EXPECT_EQ(event->synthesized, true); + EXPECT_EQ(event->callback, nullptr); + + event = &results[1]; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalNumLock); + EXPECT_EQ(event->logical, kLogicalNumLock); + EXPECT_STREQ(event->character, ""); + EXPECT_EQ(event->synthesized, true); + EXPECT_EQ(event->callback, nullptr); + + event = &results[2]; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalNumLock); + EXPECT_EQ(event->logical, kLogicalNumLock); + EXPECT_STREQ(event->character, ""); + EXPECT_EQ(event->synthesized, false); + + last_handled = false; + event->callback(true, event->user_data); + EXPECT_EQ(last_handled, true); +} + TEST(KeyboardKeyEmbedderHandlerTest, SynthesizeWithInitialTogglingState) { TestKeystate key_state; std::vector results; @@ -947,5 +1006,51 @@ TEST(KeyboardKeyEmbedderHandlerTest, SynthesizeWithInitialTogglingState) { results.clear(); } +TEST(KeyboardKeyEmbedderHandlerTest, SysKeyPress) { + TestKeystate key_state; + std::vector results; + TestFlutterKeyEvent* event; + bool last_handled = false; + + std::unique_ptr handler = + std::make_unique( + [&results](const FlutterKeyEvent& event, + FlutterKeyEventCallback callback, void* user_data) { + results.emplace_back(event, callback, user_data); + }, + key_state.Getter()); + + // Press KeyAltLeft. + handler->KeyboardHook( + kVirtualAltLeft, kScanCodeAltLeft, WM_SYSKEYDOWN, 0, false, false, + [&last_handled](bool handled) { last_handled = handled; }); + EXPECT_EQ(last_handled, false); + EXPECT_EQ(results.size(), 1); + event = results.data(); + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalAltLeft); + EXPECT_EQ(event->logical, kLogicalAltLeft); + EXPECT_STREQ(event->character, ""); + EXPECT_EQ(event->synthesized, false); + + event->callback(true, event->user_data); + EXPECT_EQ(last_handled, true); + results.clear(); + key_state.Set(kVirtualAltLeft, true); + + // Release KeyAltLeft. + handler->KeyboardHook( + kVirtualAltLeft, kScanCodeAltLeft, WM_SYSKEYUP, 0, false, true, + [&last_handled](bool handled) { last_handled = handled; }); + EXPECT_EQ(results.size(), 1); + event = results.data(); + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalAltLeft); + EXPECT_EQ(event->logical, kLogicalAltLeft); + EXPECT_STREQ(event->character, ""); + EXPECT_EQ(event->synthesized, false); + event->callback(false, event->user_data); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/keyboard_key_handler.cc b/shell/platform/windows/keyboard_key_handler.cc index 6c626774d1209..60149f3617fb9 100644 --- a/shell/platform/windows/keyboard_key_handler.cc +++ b/shell/platform/windows/keyboard_key_handler.cc @@ -67,7 +67,8 @@ static bool IsKeyDownAltRight(int action, int virtual_key, bool extended) { #ifdef WINUWP return false; #else - return virtual_key == VK_RMENU && extended && action == WM_KEYDOWN; + return virtual_key == VK_RMENU && extended && + (action == WM_KEYDOWN || action == WM_SYSKEYDOWN); #endif } @@ -78,7 +79,8 @@ static bool IsKeyUpAltRight(int action, int virtual_key, bool extended) { #ifdef WINUWP return false; #else - return virtual_key == VK_RMENU && extended && action == WM_KEYUP; + return virtual_key == VK_RMENU && extended && + (action == WM_KEYUP || action == WM_SYSKEYUP); #endif } @@ -89,7 +91,8 @@ static bool IsKeyDownCtrlLeft(int action, int virtual_key) { #ifdef WINUWP return false; #else - return virtual_key == VK_LCONTROL && action == WM_KEYDOWN; + return virtual_key == VK_LCONTROL && + (action == WM_KEYDOWN || action == WM_SYSKEYDOWN); #endif } @@ -126,6 +129,10 @@ void KeyboardKeyHandler::DispatchEvent(const PendingEvent& event) { #else char32_t character = event.character; + if (event.action == WM_SYSKEYDOWN || event.action == WM_SYSKEYUP) { + return; + } + INPUT input_event{ .type = INPUT_KEYBOARD, .ki = diff --git a/shell/platform/windows/keyboard_unittests.cc b/shell/platform/windows/keyboard_unittests.cc index 12927351282fa..a58704acee74d 100644 --- a/shell/platform/windows/keyboard_unittests.cc +++ b/shell/platform/windows/keyboard_unittests.cc @@ -766,9 +766,11 @@ TEST(KeyboardTest, AltGrModifiedKey) { EXPECT_EQ(key_calls.size(), 0); // Release AltGr. Win32 doesn't dispatch ControlLeft up. Instead Flutter will - // dispatch one. + // dispatch one. The AltGr is a system key, so will be handled by Win32's + // default WndProc. tester.InjectMessages( - 1, WmSysKeyUpInfo{VK_MENU, kScanCodeAlt, kExtended}.Build(kWmResultZero)); + 1, + WmSysKeyUpInfo{VK_MENU, kScanCodeAlt, kExtended}.Build(kWmResultDefault)); EXPECT_EQ(key_calls.size(), 1); EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalAltRight, diff --git a/shell/platform/windows/sequential_id_generator.h b/shell/platform/windows/sequential_id_generator.h index a7850b499d8e9..5efd6d78fe2af 100644 --- a/shell/platform/windows/sequential_id_generator.h +++ b/shell/platform/windows/sequential_id_generator.h @@ -14,7 +14,7 @@ namespace flutter { // new ID is always the lowest possible ID in the sequence. // // based on -// https://source.chromium.org/chromium/chromium/src/+/master:ui/gfx/sequential_id_generator.h +// https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/sequential_id_generator.h class SequentialIdGenerator { public: // Creates a new generator with the specified lower bound and uppoer bound for diff --git a/shell/platform/windows/task_runner.cc b/shell/platform/windows/task_runner.cc new file mode 100644 index 0000000000000..fa8b1348c9edd --- /dev/null +++ b/shell/platform/windows/task_runner.cc @@ -0,0 +1,104 @@ +// 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/windows/task_runner.h" + +#include +#include + +namespace flutter { + +TaskRunner::TaskRunner(CurrentTimeProc get_current_time, + const TaskExpiredCallback& on_task_expired) + : get_current_time_(get_current_time), + on_task_expired_(std::move(on_task_expired)) {} + +std::chrono::nanoseconds TaskRunner::ProcessTasks() { + const TaskTimePoint now = TaskTimePoint::clock::now(); + + std::vector expired_tasks; + + // Process expired tasks. + { + std::lock_guard lock(task_queue_mutex_); + while (!task_queue_.empty()) { + const auto& top = task_queue_.top(); + // If this task (and all tasks after this) has not yet expired, there is + // nothing more to do. Quit iterating. + if (top.fire_time > now) { + break; + } + + // Make a record of the expired task. Do NOT service the task here + // because we are still holding onto the task queue mutex. We don't want + // other threads to block on posting tasks onto this thread till we are + // done processing expired tasks. + expired_tasks.push_back(task_queue_.top()); + + // Remove the tasks from the delayed tasks queue. + task_queue_.pop(); + } + } + + // Fire expired tasks. + { + // Flushing tasks here without holing onto the task queue mutex. + for (const auto& task : expired_tasks) { + if (auto flutter_task = std::get_if(&task.variant)) { + on_task_expired_(flutter_task); + } else if (auto closure = std::get_if(&task.variant)) + (*closure)(); + } + } + + // Calculate duration to sleep for on next iteration. + { + std::lock_guard lock(task_queue_mutex_); + const auto next_wake = task_queue_.empty() ? TaskTimePoint::max() + : task_queue_.top().fire_time; + + return std::min(next_wake - now, std::chrono::nanoseconds::max()); + } +} + +TaskRunner::TaskTimePoint TaskRunner::TimePointFromFlutterTime( + uint64_t flutter_target_time_nanos) const { + const auto now = TaskTimePoint::clock::now(); + const auto flutter_duration = flutter_target_time_nanos - get_current_time_(); + return now + std::chrono::nanoseconds(flutter_duration); +} + +void TaskRunner::PostFlutterTask(FlutterTask flutter_task, + uint64_t flutter_target_time_nanos) { + Task task; + task.fire_time = TimePointFromFlutterTime(flutter_target_time_nanos); + task.variant = flutter_task; + EnqueueTask(std::move(task)); +} + +void TaskRunner::PostTask(TaskClosure closure) { + Task task; + task.fire_time = TaskTimePoint::clock::now(); + task.variant = std::move(closure); + EnqueueTask(std::move(task)); +} + +void TaskRunner::EnqueueTask(Task task) { + static std::atomic_uint64_t sGlobalTaskOrder(0); + + task.order = ++sGlobalTaskOrder; + { + std::lock_guard lock(task_queue_mutex_); + task_queue_.push(task); + + // Make sure the queue mutex is unlocked before waking up the loop. In case + // the wake causes this thread to be descheduled for the primary thread to + // process tasks, the acquisition of the lock on that thread while holding + // the lock here momentarily till the end of the scope is a pessimization. + } + + WakeUp(); +} + +} // namespace flutter diff --git a/shell/platform/windows/task_runner.h b/shell/platform/windows/task_runner.h index 0e3d44450281e..7f181d0758975 100644 --- a/shell/platform/windows/task_runner.h +++ b/shell/platform/windows/task_runner.h @@ -5,10 +5,13 @@ #ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_TASK_RUNNER_H_ #define FLUTTER_SHELL_PLATFORM_WINDOWS_TASK_RUNNER_H_ -#include - #include +#include +#include #include +#include +#include +#include #include "flutter/shell/platform/embedder/embedder.h" @@ -16,7 +19,6 @@ namespace flutter { typedef uint64_t (*CurrentTimeProc)(); -// Abstract custom task runner for scheduling custom tasks. class TaskRunner { public: using TaskTimePoint = std::chrono::steady_clock::time_point; @@ -25,18 +27,18 @@ class TaskRunner { virtual ~TaskRunner() = default; - // Returns if the current thread is the UI thread. + // Returns `true` if the current thread is this runner's thread. virtual bool RunsTasksOnCurrentThread() const = 0; // Post a Flutter engine task to the event loop for delayed execution. - virtual void PostFlutterTask(FlutterTask flutter_task, - uint64_t flutter_target_time_nanos) = 0; + void PostFlutterTask(FlutterTask flutter_task, + uint64_t flutter_target_time_nanos); // Post a task to the event loop. - virtual void PostTask(TaskClosure task) = 0; + void PostTask(TaskClosure task); // Post a task to the event loop or run it immediately if this is being called - // from the main thread. + // from the runner's thread. void RunNowOrPostTask(TaskClosure task) { if (RunsTasksOnCurrentThread()) { task(); @@ -45,12 +47,57 @@ class TaskRunner { } } - // Creates a new task runner with the given main thread ID, current time + // Creates a new task runner with the current thread, current time // provider, and callback for tasks that are ready to be run. static std::unique_ptr Create( - DWORD main_thread_id, CurrentTimeProc get_current_time, const TaskExpiredCallback& on_task_expired); + + protected: + TaskRunner(CurrentTimeProc get_current_time, + const TaskExpiredCallback& on_task_expired); + + // Schedules timers to call `ProcessTasks()` at the runner's thread. + virtual void WakeUp() = 0; + + // Executes expired task, and returns the duration until the next task + // deadline if exists, otherwise returns `std::chrono::nanoseconds::max()`. + // + // Each platform implementations must call this to schedule the tasks. + std::chrono::nanoseconds ProcessTasks(); + + private: + typedef std::variant TaskVariant; + + struct Task { + uint64_t order; + TaskTimePoint fire_time; + TaskVariant variant; + + struct Comparer { + bool operator()(const Task& a, const Task& b) { + if (a.fire_time == b.fire_time) { + return a.order > b.order; + } + return a.fire_time > b.fire_time; + } + }; + }; + + // Enqueues the given task. + void EnqueueTask(Task task); + + // Returns a TaskTimePoint computed from the given target time from Flutter. + TaskTimePoint TimePointFromFlutterTime( + uint64_t flutter_target_time_nanos) const; + + CurrentTimeProc get_current_time_; + TaskExpiredCallback on_task_expired_; + std::mutex task_queue_mutex_; + std::priority_queue, Task::Comparer> task_queue_; + + TaskRunner(const TaskRunner&) = delete; + TaskRunner& operator=(const TaskRunner&) = delete; }; } // namespace flutter diff --git a/shell/platform/windows/task_runner_unittests.cc b/shell/platform/windows/task_runner_unittests.cc new file mode 100644 index 0000000000000..8bf0b8abb1b09 --- /dev/null +++ b/shell/platform/windows/task_runner_unittests.cc @@ -0,0 +1,82 @@ +// 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/windows/task_runner.h" + +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +namespace { +class MockTaskRunner : public TaskRunner { + public: + MockTaskRunner(CurrentTimeProc get_current_time, + const TaskExpiredCallback& on_task_expired) + : TaskRunner(get_current_time, on_task_expired) {} + + virtual bool RunsTasksOnCurrentThread() const override { return true; } + + void SimulateTimerAwake() { ProcessTasks(); } + + protected: + virtual void WakeUp() override { + // Do nothing to avoid processing tasks immediately after the tasks is + // posted. + } +}; + +uint64_t MockGetCurrentTime() { + return 10000; +} +} // namespace + +TEST(TaskRunnerTest, MaybeExecuteTaskWithExactOrder) { + std::vector executed_task_order; + auto runner = + MockTaskRunner(MockGetCurrentTime, + [&executed_task_order](const FlutterTask* expired_task) { + executed_task_order.push_back(expired_task->task); + }); + + uint64_t time_now = MockGetCurrentTime(); + + runner.PostFlutterTask(FlutterTask{nullptr, 1}, time_now); + runner.PostFlutterTask(FlutterTask{nullptr, 2}, time_now); + runner.PostTask( + [&executed_task_order]() { executed_task_order.push_back(3); }); + runner.PostTask( + [&executed_task_order]() { executed_task_order.push_back(4); }); + + runner.SimulateTimerAwake(); + + std::vector posted_task_order{1, 2, 3, 4}; + EXPECT_EQ(executed_task_order, posted_task_order); +} + +TEST(TaskRunnerTest, MaybeExecuteTaskOnlyExpired) { + std::set executed_task; + auto runner = MockTaskRunner( + MockGetCurrentTime, [&executed_task](const FlutterTask* expired_task) { + executed_task.insert(expired_task->task); + }); + + uint64_t task_expired_before_now = 1; + uint64_t time_before_now = 0; + runner.PostFlutterTask(FlutterTask{nullptr, task_expired_before_now}, + time_before_now); + + uint64_t task_expired_after_now = 2; + uint64_t time_after_now = MockGetCurrentTime() * 2; + runner.PostFlutterTask(FlutterTask{nullptr, task_expired_after_now}, + time_after_now); + + runner.SimulateTimerAwake(); + + std::set only_task_expired_before_now{task_expired_before_now}; + EXPECT_EQ(executed_task, only_task_expired_before_now); +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/task_runner_win32.cc b/shell/platform/windows/task_runner_win32.cc index 62de4a1567d16..01c7179c792f5 100644 --- a/shell/platform/windows/task_runner_win32.cc +++ b/shell/platform/windows/task_runner_win32.cc @@ -4,27 +4,19 @@ #include "flutter/shell/platform/windows/task_runner_win32.h" -#include -#include -#include - namespace flutter { // static std::unique_ptr TaskRunner::Create( - DWORD main_thread_id, CurrentTimeProc get_current_time, const TaskExpiredCallback& on_task_expired) { - return std::make_unique(main_thread_id, get_current_time, - on_task_expired); + return std::make_unique(get_current_time, on_task_expired); } -TaskRunnerWin32::TaskRunnerWin32(DWORD main_thread_id, - CurrentTimeProc get_current_time, +TaskRunnerWin32::TaskRunnerWin32(CurrentTimeProc get_current_time, const TaskExpiredCallback& on_task_expired) - : main_thread_id_(main_thread_id), - get_current_time_(get_current_time), - on_task_expired_(std::move(on_task_expired)) { + : TaskRunner(get_current_time, on_task_expired) { + main_thread_id_ = GetCurrentThreadId(); task_runner_window_ = TaskRunnerWin32Window::GetSharedInstance(); task_runner_window_->AddDelegate(this); } @@ -38,89 +30,10 @@ bool TaskRunnerWin32::RunsTasksOnCurrentThread() const { } std::chrono::nanoseconds TaskRunnerWin32::ProcessTasks() { - const TaskTimePoint now = TaskTimePoint::clock::now(); - - std::vector expired_tasks; - - // Process expired tasks. - { - std::lock_guard lock(task_queue_mutex_); - while (!task_queue_.empty()) { - const auto& top = task_queue_.top(); - // If this task (and all tasks after this) has not yet expired, there is - // nothing more to do. Quit iterating. - if (top.fire_time > now) { - break; - } - - // Make a record of the expired task. Do NOT service the task here - // because we are still holding onto the task queue mutex. We don't want - // other threads to block on posting tasks onto this thread till we are - // done processing expired tasks. - expired_tasks.push_back(task_queue_.top()); - - // Remove the tasks from the delayed tasks queue. - task_queue_.pop(); - } - } - - // Fire expired tasks. - { - // Flushing tasks here without holing onto the task queue mutex. - for (const auto& task : expired_tasks) { - if (auto flutter_task = std::get_if(&task.variant)) { - on_task_expired_(flutter_task); - } else if (auto closure = std::get_if(&task.variant)) - (*closure)(); - } - } - - // Calculate duration to sleep for on next iteration. - { - std::lock_guard lock(task_queue_mutex_); - const auto next_wake = task_queue_.empty() ? TaskTimePoint::max() - : task_queue_.top().fire_time; - - return std::min(next_wake - now, std::chrono::nanoseconds::max()); - } + return TaskRunner::ProcessTasks(); } -TaskRunnerWin32::TaskTimePoint TaskRunnerWin32::TimePointFromFlutterTime( - uint64_t flutter_target_time_nanos) const { - const auto now = TaskTimePoint::clock::now(); - const auto flutter_duration = flutter_target_time_nanos - get_current_time_(); - return now + std::chrono::nanoseconds(flutter_duration); -} - -void TaskRunnerWin32::PostFlutterTask(FlutterTask flutter_task, - uint64_t flutter_target_time_nanos) { - Task task; - task.fire_time = TimePointFromFlutterTime(flutter_target_time_nanos); - task.variant = flutter_task; - EnqueueTask(std::move(task)); -} - -void TaskRunnerWin32::PostTask(TaskClosure closure) { - Task task; - task.fire_time = TaskTimePoint::clock::now(); - task.variant = std::move(closure); - EnqueueTask(std::move(task)); -} - -void TaskRunnerWin32::EnqueueTask(Task task) { - static std::atomic_uint64_t sGlobalTaskOrder(0); - - task.order = ++sGlobalTaskOrder; - { - std::lock_guard lock(task_queue_mutex_); - task_queue_.push(task); - - // Make sure the queue mutex is unlocked before waking up the loop. In case - // the wake causes this thread to be descheduled for the primary thread to - // process tasks, the acquisition of the lock on that thread while holding - // the lock here momentarily till the end of the scope is a pessimization. - } - +void TaskRunnerWin32::WakeUp() { task_runner_window_->WakeUp(); } diff --git a/shell/platform/windows/task_runner_win32.h b/shell/platform/windows/task_runner_win32.h index 149e9acd6a67e..6e3ceb8476f63 100644 --- a/shell/platform/windows/task_runner_win32.h +++ b/shell/platform/windows/task_runner_win32.h @@ -7,14 +7,6 @@ #include -#include -#include -#include -#include -#include -#include -#include - #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/windows/task_runner.h" #include "flutter/shell/platform/windows/task_runner_win32_window.h" @@ -27,61 +19,25 @@ namespace flutter { class TaskRunnerWin32 : public TaskRunner, public TaskRunnerWin32Window::Delegate { public: - // Creates a new task runner with the given main thread ID, current time - // provider, and callback for tasks that are ready to be run. - TaskRunnerWin32(DWORD main_thread_id, - CurrentTimeProc get_current_time, + TaskRunnerWin32(CurrentTimeProc get_current_time, const TaskExpiredCallback& on_task_expired); - virtual ~TaskRunnerWin32(); // |TaskRunner| bool RunsTasksOnCurrentThread() const override; - // |TaskRunner| - void PostFlutterTask(FlutterTask flutter_task, - uint64_t flutter_target_time_nanos) override; - - // |TaskRunner| - void PostTask(TaskClosure task) override; - // |TaskRunnerWin32Window::Delegate| std::chrono::nanoseconds ProcessTasks() override; - private: - typedef std::variant TaskVariant; - - struct Task { - uint64_t order; - TaskTimePoint fire_time; - TaskVariant variant; - - struct Comparer { - bool operator()(const Task& a, const Task& b) { - if (a.fire_time == b.fire_time) { - return a.order > b.order; - } - return a.fire_time > b.fire_time; - } - }; - }; - - // Enqueues the given task. - void EnqueueTask(Task task); - - // Returns a TaskTimePoint computed from the given target time from Flutter. - TaskTimePoint TimePointFromFlutterTime( - uint64_t flutter_target_time_nanos) const; + protected: + // |TaskRunner| + void WakeUp() override; + private: DWORD main_thread_id_; - CurrentTimeProc get_current_time_; - TaskExpiredCallback on_task_expired_; - std::mutex task_queue_mutex_; - std::priority_queue, Task::Comparer> task_queue_; std::shared_ptr task_runner_window_; TaskRunnerWin32(const TaskRunnerWin32&) = delete; - TaskRunnerWin32& operator=(const TaskRunnerWin32&) = delete; }; diff --git a/shell/platform/windows/task_runner_winuwp.cc b/shell/platform/windows/task_runner_winuwp.cc index d6c5c6bc70d93..8bba42ed741bf 100644 --- a/shell/platform/windows/task_runner_winuwp.cc +++ b/shell/platform/windows/task_runner_winuwp.cc @@ -1,51 +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. - -#include "flutter/shell/platform/windows/task_runner_winuwp.h" - -#include -#include - -namespace flutter { - -// static -std::unique_ptr TaskRunner::Create( - DWORD main_thread_id, - CurrentTimeProc get_current_time, - const TaskExpiredCallback& on_task_expired) { - return std::make_unique(main_thread_id, on_task_expired); -} - -TaskRunnerWinUwp::TaskRunnerWinUwp(DWORD main_thread_id, - const TaskExpiredCallback& on_task_expired) - : main_thread_id_(main_thread_id), - on_task_expired_(std::move(on_task_expired)) { - dispatcher_ = - winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread().Dispatcher(); -} - -TaskRunnerWinUwp::~TaskRunnerWinUwp() = default; - -bool TaskRunnerWinUwp::RunsTasksOnCurrentThread() const { - return GetCurrentThreadId() == main_thread_id_; -} - -void TaskRunnerWinUwp::PostFlutterTask(FlutterTask flutter_task, - uint64_t flutter_target_time_nanos) { - // TODO: Handle the target time. See - // https://github.com/flutter/flutter/issues/70890. - - dispatcher_.RunAsync( - winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, - [this, flutter_task]() { on_task_expired_(&flutter_task); }); -} - -void TaskRunnerWinUwp::PostTask(TaskClosure task) { - // TODO: Handle the target time. See PostFlutterTask() - - dispatcher_.RunAsync(winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, - [task]() { task(); }); -} - -} // namespace flutter +// 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/windows/task_runner_winuwp.h" + +namespace flutter { + +// static +std::unique_ptr TaskRunner::Create( + CurrentTimeProc get_current_time, + const TaskExpiredCallback& on_task_expired) { + return std::make_unique(get_current_time, on_task_expired); +} + +TaskRunnerWinUwp::TaskRunnerWinUwp(CurrentTimeProc get_current_time, + const TaskExpiredCallback& on_task_expired) + : TaskRunner(get_current_time, on_task_expired) { + dispatcher_queue_ = + winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); + dispatcher_queue_timer_ = dispatcher_queue_.CreateTimer(); + dispatcher_queue_timer_.Tick({this, &TaskRunnerWinUwp::OnTick}); +} + +TaskRunnerWinUwp::~TaskRunnerWinUwp() = default; + +bool TaskRunnerWinUwp::RunsTasksOnCurrentThread() const { + return dispatcher_queue_.HasThreadAccess(); +} + +void TaskRunnerWinUwp::WakeUp() { + dispatcher_queue_.TryEnqueue([this]() { ProcessTasksAndScheduleNext(); }); +} + +void TaskRunnerWinUwp::OnTick( + winrt::Windows::System::DispatcherQueueTimer const&, + winrt::Windows::Foundation::IInspectable const&) { + ProcessTasks(); +} + +void TaskRunnerWinUwp::ProcessTasksAndScheduleNext() { + auto next = ProcessTasks(); + + if (next == std::chrono::nanoseconds::max()) { + dispatcher_queue_timer_.Stop(); + } else { + dispatcher_queue_timer_.Interval( + std::chrono::duration_cast(next)); + dispatcher_queue_timer_.Start(); + } +} + +} // namespace flutter diff --git a/shell/platform/windows/task_runner_winuwp.h b/shell/platform/windows/task_runner_winuwp.h index d411a4c2c383a..d92ce80a46230 100644 --- a/shell/platform/windows/task_runner_winuwp.h +++ b/shell/platform/windows/task_runner_winuwp.h @@ -1,52 +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. - -#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_WINRT_TASK_RUNNER_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_WINRT_TASK_RUNNER_H_ - -#include - -#include - -#include -#include -#include - -#include "flutter/shell/platform/embedder/embedder.h" -#include "flutter/shell/platform/windows/task_runner.h" - -namespace flutter { - -// A custom task runner that uses a CoreDispatcher to schedule -// flutter tasks. -class TaskRunnerWinUwp : public TaskRunner { - public: - TaskRunnerWinUwp(DWORD main_thread_id, - const TaskExpiredCallback& on_task_expired); - - ~TaskRunnerWinUwp(); - - TaskRunnerWinUwp(const TaskRunnerWinUwp&) = delete; - TaskRunnerWinUwp& operator=(const TaskRunnerWinUwp&) = delete; - - // |TaskRunner| - bool RunsTasksOnCurrentThread() const override; - - // |TaskRunner| - void PostFlutterTask(FlutterTask flutter_task, - uint64_t flutter_target_time_nanos) override; - - // |TaskRunner| - void PostTask(TaskClosure task) override; - - private: - DWORD main_thread_id_; - TaskExpiredCallback on_task_expired_; - - winrt::Windows::UI::Core::CoreDispatcher dispatcher_{nullptr}; -}; - -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_WINRT_TASK_RUNNER_H_ +// 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_TASK_RUNNER_WINUWP_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_TASK_RUNNER_WINUWP_H_ + +#include +#include + +#include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/windows/task_runner.h" + +namespace flutter { + +// A custom task runner that uses a DispatcherQueue. +class TaskRunnerWinUwp : public TaskRunner { + public: + TaskRunnerWinUwp(CurrentTimeProc get_current_time, + const TaskExpiredCallback& on_task_expired); + virtual ~TaskRunnerWinUwp(); + + // |TaskRunner| + bool RunsTasksOnCurrentThread() const override; + + protected: + // |TaskRunner| + void WakeUp() override; + + private: + void OnTick(winrt::Windows::System::DispatcherQueueTimer const&, + winrt::Windows::Foundation::IInspectable const&); + + void ProcessTasksAndScheduleNext(); + + winrt::Windows::System::DispatcherQueue dispatcher_queue_{nullptr}; + winrt::Windows::System::DispatcherQueueTimer dispatcher_queue_timer_{nullptr}; + + TaskRunnerWinUwp(const TaskRunnerWinUwp&) = delete; + TaskRunnerWinUwp& operator=(const TaskRunnerWinUwp&) = delete; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TASK_RUNNER_WINUWP_H_ diff --git a/shell/platform/windows/testing/mock_text_input_manager_win32.cc b/shell/platform/windows/testing/mock_text_input_manager_win32.cc new file mode 100644 index 0000000000000..0d4d4cc2eb544 --- /dev/null +++ b/shell/platform/windows/testing/mock_text_input_manager_win32.cc @@ -0,0 +1,16 @@ +// 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/windows/testing/mock_text_input_manager_win32.h" + +namespace flutter { +namespace testing { + +MockTextInputManagerWin32::MockTextInputManagerWin32() + : TextInputManagerWin32(){}; + +MockTextInputManagerWin32::~MockTextInputManagerWin32() = default; + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/testing/mock_text_input_manager_win32.h b/shell/platform/windows/testing/mock_text_input_manager_win32.h new file mode 100644 index 0000000000000..eb900c00f5bd9 --- /dev/null +++ b/shell/platform/windows/testing/mock_text_input_manager_win32.h @@ -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. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_TEXT_INPUT_MANAGER_WIN32_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_TEXT_INPUT_MANAGER_WIN32_H_ + +#include +#include +#include + +#include "flutter/shell/platform/windows/text_input_manager_win32.h" +#include "gmock/gmock.h" + +namespace flutter { +namespace testing { + +/// Mock for the |WindowWin32| base class. +class MockTextInputManagerWin32 : public TextInputManagerWin32 { + public: + MockTextInputManagerWin32(); + virtual ~MockTextInputManagerWin32(); + + // Prevent copying. + MockTextInputManagerWin32(MockTextInputManagerWin32 const&) = delete; + MockTextInputManagerWin32& operator=(MockTextInputManagerWin32 const&) = + delete; + + MOCK_CONST_METHOD0(GetComposingString, std::optional()); + MOCK_CONST_METHOD0(GetResultString, std::optional()); + MOCK_CONST_METHOD0(GetComposingCursorPosition, long()); +}; + +} // namespace testing +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_TEXT_INPUT_MANAGER_WIN32_H_ diff --git a/shell/platform/windows/testing/mock_window_binding_handler.cc b/shell/platform/windows/testing/mock_window_binding_handler.cc index 7b7f0e17fdae7..c0449152beb72 100644 --- a/shell/platform/windows/testing/mock_window_binding_handler.cc +++ b/shell/platform/windows/testing/mock_window_binding_handler.cc @@ -1,15 +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. - -#include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h" - -namespace flutter { -namespace testing { - -MockWindowBindingHandler::MockWindowBindingHandler() : WindowBindingHandler(){}; - -MockWindowBindingHandler::~MockWindowBindingHandler() = default; - -} // namespace testing -} // namespace flutter +// 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/windows/testing/mock_window_binding_handler.h" + +namespace flutter { +namespace testing { + +MockWindowBindingHandler::MockWindowBindingHandler() : WindowBindingHandler(){}; + +MockWindowBindingHandler::~MockWindowBindingHandler() = default; + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/testing/mock_window_binding_handler.h b/shell/platform/windows/testing/mock_window_binding_handler.h index c6be8e18c8089..a5395c96cca02 100644 --- a/shell/platform/windows/testing/mock_window_binding_handler.h +++ b/shell/platform/windows/testing/mock_window_binding_handler.h @@ -1,42 +1,43 @@ -// 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_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_ -#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_ - -#include - -#include "flutter/shell/platform/windows/window_binding_handler.h" -#include "gmock/gmock.h" - -namespace flutter { -namespace testing { - -/// Mock for the |WindowWin32| base class. -class MockWindowBindingHandler : public WindowBindingHandler { - public: - MockWindowBindingHandler(); - virtual ~MockWindowBindingHandler(); - - // Prevent copying. - MockWindowBindingHandler(MockWindowBindingHandler const&) = delete; - MockWindowBindingHandler& operator=(MockWindowBindingHandler const&) = delete; - - MOCK_METHOD1(SetView, void(WindowBindingHandlerDelegate* view)); - MOCK_METHOD0(GetRenderTarget, WindowsRenderTarget()); - MOCK_METHOD0(GetPlatformWindow, PlatformWindow()); - MOCK_METHOD0(GetDpiScale, float()); - MOCK_METHOD0(IsVisible, bool()); - MOCK_METHOD0(OnWindowResized, void()); - MOCK_METHOD0(GetPhysicalWindowBounds, PhysicalWindowBounds()); - MOCK_METHOD1(UpdateFlutterCursor, void(const std::string& cursor_name)); - MOCK_METHOD1(OnCursorRectUpdated, void(const Rect& rect)); - MOCK_METHOD3(OnBitmapSurfaceUpdated, - bool(const void* allocation, size_t row_bytes, size_t height)); -}; - -} // namespace testing -} // namespace flutter - -#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_ +// 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_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_ + +#include + +#include "flutter/shell/platform/windows/window_binding_handler.h" +#include "gmock/gmock.h" + +namespace flutter { +namespace testing { + +/// Mock for the |WindowWin32| base class. +class MockWindowBindingHandler : public WindowBindingHandler { + public: + MockWindowBindingHandler(); + virtual ~MockWindowBindingHandler(); + + // Prevent copying. + MockWindowBindingHandler(MockWindowBindingHandler const&) = delete; + MockWindowBindingHandler& operator=(MockWindowBindingHandler const&) = delete; + + MOCK_METHOD1(SetView, void(WindowBindingHandlerDelegate* view)); + MOCK_METHOD0(GetRenderTarget, WindowsRenderTarget()); + MOCK_METHOD0(GetPlatformWindow, PlatformWindow()); + MOCK_METHOD0(GetDpiScale, float()); + MOCK_METHOD0(IsVisible, bool()); + MOCK_METHOD0(OnWindowResized, void()); + MOCK_METHOD0(GetPhysicalWindowBounds, PhysicalWindowBounds()); + MOCK_METHOD1(UpdateFlutterCursor, void(const std::string& cursor_name)); + MOCK_METHOD1(OnCursorRectUpdated, void(const Rect& rect)); + MOCK_METHOD0(OnResetImeComposing, void()); + MOCK_METHOD3(OnBitmapSurfaceUpdated, + bool(const void* allocation, size_t row_bytes, size_t height)); +}; + +} // namespace testing +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_ diff --git a/shell/platform/windows/testing/mock_window_win32.cc b/shell/platform/windows/testing/mock_window_win32.cc index 99cc67898fb37..4376c277db1e5 100644 --- a/shell/platform/windows/testing/mock_window_win32.cc +++ b/shell/platform/windows/testing/mock_window_win32.cc @@ -6,8 +6,10 @@ namespace flutter { namespace testing { - MockWin32Window::MockWin32Window() : WindowWin32(){}; +MockWin32Window::MockWin32Window( + std::unique_ptr text_input_manager) + : WindowWin32(std::move(text_input_manager)){}; MockWin32Window::~MockWin32Window() = default; @@ -35,5 +37,11 @@ LRESULT MockWin32Window::Win32SendMessage(HWND hWnd, return HandleMessage(message, wparam, lparam); } +void MockWin32Window::CallOnImeComposition(UINT const message, + WPARAM const wparam, + LPARAM const lparam) { + WindowWin32::OnImeComposition(message, wparam, lparam); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/testing/mock_window_win32.h b/shell/platform/windows/testing/mock_window_win32.h index b53e9f7c6900a..406cf1b9b04a7 100644 --- a/shell/platform/windows/testing/mock_window_win32.h +++ b/shell/platform/windows/testing/mock_window_win32.h @@ -18,6 +18,7 @@ namespace testing { class MockWin32Window : public WindowWin32, public MockMessageQueue { public: MockWin32Window(); + MockWin32Window(std::unique_ptr text_input_manager); virtual ~MockWin32Window(); // Prevent copying. @@ -44,12 +45,18 @@ class MockWin32Window : public WindowWin32, public MockMessageQueue { MOCK_METHOD0(OnSetCursor, void()); MOCK_METHOD1(OnText, void(const std::u16string&)); MOCK_METHOD6(OnKey, bool(int, int, int, char32_t, bool, bool)); + MOCK_METHOD1(OnUpdateSemanticsEnabled, void(bool)); MOCK_METHOD4(OnScroll, void(double, double, FlutterPointerDeviceKind, int32_t)); MOCK_METHOD0(OnComposeBegin, void()); MOCK_METHOD0(OnComposeCommit, void()); MOCK_METHOD0(OnComposeEnd, void()); MOCK_METHOD2(OnComposeChange, void(const std::u16string&, int)); + MOCK_METHOD3(OnImeComposition, void(UINT const, WPARAM const, LPARAM const)); + + void CallOnImeComposition(UINT const message, + WPARAM const wparam, + LPARAM const lparam); protected: LRESULT Win32DefWindowProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); diff --git a/shell/platform/windows/testing/wm_builders.h b/shell/platform/windows/testing/wm_builders.h index 56a0b3e29858c..3ada4465c3db9 100644 --- a/shell/platform/windows/testing/wm_builders.h +++ b/shell/platform/windows/testing/wm_builders.h @@ -26,22 +26,22 @@ struct Win32Message { HWND hWnd; }; -typedef enum { +typedef enum WmFieldExtended { kNotExtended = 0, kExtended = 1, } WmFieldExtended; -typedef enum { +typedef enum WmFieldContext { kNoContext = 0, kAltHeld = 1, } WmFieldContext; -typedef enum { +typedef enum WmFieldPrevState { kWasUp = 0, kWasDown = 1, } WmFieldPrevState; -typedef enum { +typedef enum WmFieldTransitionState { kBeingReleased = 0, kBeingPressed = 1, } WmFieldTransitionState; @@ -49,7 +49,7 @@ typedef enum { // WM_KEYDOWN messages. // // See https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown. -typedef struct { +typedef struct WmKeyDownInfo { uint32_t key; uint8_t scan_code; @@ -74,7 +74,7 @@ typedef struct { // WM_KEYUP messages. // // See https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keyup. -typedef struct { +typedef struct WmKeyUpInfo { uint32_t key; uint8_t scan_code; @@ -96,7 +96,7 @@ typedef struct { // WM_CHAR messages. // // See https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-char. -typedef struct { +typedef struct WmCharInfo { uint32_t char_code; uint8_t scan_code; @@ -118,7 +118,7 @@ typedef struct { // WM_SYSKEYUP messages. // // See https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-syskeyup. -typedef struct { +typedef struct WmSysKeyUpInfo { uint32_t key; uint8_t scan_code; @@ -140,7 +140,7 @@ typedef struct { // WM_DEADCHAR messages. // // See https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-deadchar. -typedef struct { +typedef struct WmDeadCharInfo { uint32_t char_code; uint8_t scan_code; diff --git a/shell/platform/windows/text_input_manager_win32.cc b/shell/platform/windows/text_input_manager_win32.cc index 7c0ed0850e212..e3900c1b511ed 100644 --- a/shell/platform/windows/text_input_manager_win32.cc +++ b/shell/platform/windows/text_input_manager_win32.cc @@ -128,6 +128,25 @@ std::optional TextInputManagerWin32::GetResultString() const { return GetString(GCS_RESULTSTR); } +void TextInputManagerWin32::AbortComposing() { + if (window_handle_ == nullptr || !ime_active_) { + return; + } + + ImmContext imm_context(window_handle_); + if (imm_context.IsValid()) { + // Cancel composing and close the candidates window. + ::ImmNotifyIME(imm_context.get(), NI_COMPOSITIONSTR, CPS_CANCEL, 0); + ::ImmNotifyIME(imm_context.get(), NI_CLOSECANDIDATE, 0, 0); + + // Clear the composing string. + wchar_t composition_str[] = L""; + wchar_t reading_str[] = L""; + ::ImmSetCompositionStringW(imm_context.get(), SCS_SETSTR, composition_str, + sizeof(wchar_t), reading_str, sizeof(wchar_t)); + } +} + std::optional TextInputManagerWin32::GetString(int type) const { if (window_handle_ == nullptr || !ime_active_) { return std::nullopt; @@ -138,7 +157,7 @@ std::optional TextInputManagerWin32::GetString(int type) const { const long compose_bytes = ::ImmGetCompositionString(imm_context.get(), type, nullptr, 0); const long compose_length = compose_bytes / sizeof(wchar_t); - if (compose_length <= 0) { + if (compose_length < 0) { return std::nullopt; } diff --git a/shell/platform/windows/text_input_manager_win32.h b/shell/platform/windows/text_input_manager_win32.h index d7c0319412edf..3ca6d09e48cf0 100644 --- a/shell/platform/windows/text_input_manager_win32.h +++ b/shell/platform/windows/text_input_manager_win32.h @@ -28,7 +28,7 @@ namespace flutter { class TextInputManagerWin32 { public: TextInputManagerWin32() noexcept = default; - ~TextInputManagerWin32() = default; + virtual ~TextInputManagerWin32() = default; TextInputManagerWin32(const TextInputManagerWin32&) = delete; TextInputManagerWin32& operator=(const TextInputManagerWin32&) = delete; @@ -60,7 +60,7 @@ class TextInputManagerWin32 { void UpdateCaretRect(const Rect& rect); // Returns the cursor position relative to the start of the composing range. - long GetComposingCursorPosition() const; + virtual long GetComposingCursorPosition() const; // Returns the contents of the composing string. // @@ -68,14 +68,20 @@ class TextInputManagerWin32 { // GCS_COMPSTR flag is set in the lparam. In some IMEs, this string may also // be set in events where the GCS_RESULTSTR flag is set. This contains the // in-progress composing string. - std::optional GetComposingString() const; + virtual std::optional GetComposingString() const; // Returns the contents of the result string. // // This may be called in response to WM_IME_COMPOSITION events where the // GCS_RESULTSTR flag is set in the lparam. This contains the final string to // be committed in the composing region when composition is ended. - std::optional GetResultString() const; + virtual std::optional GetResultString() const; + + /// Aborts IME composing. + /// + /// Aborts composing, closes the candidates window, and clears the contents + /// of the composing string. + void AbortComposing(); private: // Returns either the composing string or result string based on the value of diff --git a/shell/platform/windows/text_input_plugin.cc b/shell/platform/windows/text_input_plugin.cc index f898b30b813c6..e5cf28f655e35 100644 --- a/shell/platform/windows/text_input_plugin.cc +++ b/shell/platform/windows/text_input_plugin.cc @@ -7,7 +7,6 @@ #include #include -#include #include "flutter/shell/platform/common/json_method_codec.h" #include "flutter/shell/platform/windows/flutter_windows_view.h" @@ -71,7 +70,7 @@ bool TextInputPlugin::KeyboardHook(FlutterWindowsView* view, if (active_model_ == nullptr) { return false; } - if (action == WM_KEYDOWN) { + if (action == WM_KEYDOWN || action == WM_SYSKEYDOWN) { // Most editing keys (arrow keys, backspace, delete, etc.) are handled in // the framework, so don't need to be handled at this layer. switch (key) { @@ -148,6 +147,12 @@ void TextInputPlugin::HandleMethodCall( if (method.compare(kShowMethod) == 0 || method.compare(kHideMethod) == 0) { // These methods are no-ops. } else if (method.compare(kClearClientMethod) == 0) { + if (active_model_ != nullptr && active_model_->composing()) { + active_model_->CommitComposing(); + active_model_->EndComposing(); + SendStateUpdate(*active_model_); + } + delegate_->OnResetImeComposing(); active_model_ = nullptr; } else if (method.compare(kSetClientMethod) == 0) { if (!method_call.arguments() || method_call.arguments()->IsNull()) { diff --git a/shell/platform/windows/text_input_plugin_delegate.h b/shell/platform/windows/text_input_plugin_delegate.h index 3975f3a0f342f..0d41ff4bdc3a0 100644 --- a/shell/platform/windows/text_input_plugin_delegate.h +++ b/shell/platform/windows/text_input_plugin_delegate.h @@ -15,6 +15,9 @@ class TextInputPluginDelegate { // Notifies the delegate of the updated the cursor rect in Flutter root view // coordinates. virtual void OnCursorRectUpdated(const Rect& rect) = 0; + + // Notifies the delegate that the system IME composing state should be reset. + virtual void OnResetImeComposing() = 0; }; } // namespace flutter diff --git a/shell/platform/windows/text_input_plugin_unittest.cc b/shell/platform/windows/text_input_plugin_unittest.cc index aac581e50b4f3..51440a8ff2e9e 100644 --- a/shell/platform/windows/text_input_plugin_unittest.cc +++ b/shell/platform/windows/text_input_plugin_unittest.cc @@ -7,6 +7,7 @@ #include #include "flutter/shell/platform/common/json_message_codec.h" +#include "flutter/shell/platform/common/json_method_codec.h" #include "flutter/shell/platform/windows/flutter_windows_view.h" #include "flutter/shell/platform/windows/testing/test_binary_messenger.h" #include "gmock/gmock.h" @@ -30,8 +31,13 @@ static std::unique_ptr> CreateResponse(bool handled) { class EmptyTextInputPluginDelegate : public TextInputPluginDelegate { public: - // Notifies delegate that the cursor position has changed. - void OnCursorRectUpdated(const Rect& rect) {} + void OnCursorRectUpdated(const Rect& rect) override {} + void OnResetImeComposing() override { ime_was_reset_ = true; } + + bool ime_was_reset() const { return ime_was_reset_; } + + private: + bool ime_was_reset_ = false; }; } // namespace @@ -59,5 +65,21 @@ TEST(TextInputPluginTest, TextMethodsWorksWithEmptyModel) { // Passes if it did not crash } +TEST(TextInputPluginTest, ClearClientResetsComposing) { + TestBinaryMessenger messenger([](const std::string& channel, + const uint8_t* message, size_t message_size, + BinaryReply reply) {}); + BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {}; + + EmptyTextInputPluginDelegate delegate; + TextInputPlugin handler(&messenger, &delegate); + + auto& codec = JsonMethodCodec::GetInstance(); + auto message = codec.EncodeMethodCall({"TextInput.clearClient", nullptr}); + messenger.SimulateEngineMessage("flutter/textinput", message->data(), + message->size(), reply_handler); + EXPECT_TRUE(delegate.ime_was_reset()); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/window_binding_handler.h b/shell/platform/windows/window_binding_handler.h index 60f93ca8aee66..6f310217aa890 100644 --- a/shell/platform/windows/window_binding_handler.h +++ b/shell/platform/windows/window_binding_handler.h @@ -91,6 +91,10 @@ class WindowBindingHandler { virtual bool OnBitmapSurfaceUpdated(const void* allocation, size_t row_bytes, size_t height) = 0; + + // Invoked when the app ends IME composing, such when the active text input + // client is cleared. + virtual void OnResetImeComposing() = 0; }; } // namespace flutter diff --git a/shell/platform/windows/window_binding_handler_delegate.h b/shell/platform/windows/window_binding_handler_delegate.h index 7bdedc9d4dbf0..3b58e4aed93e8 100644 --- a/shell/platform/windows/window_binding_handler_delegate.h +++ b/shell/platform/windows/window_binding_handler_delegate.h @@ -98,6 +98,10 @@ class WindowBindingHandlerDelegate { // Notifies delegate that backing window has received brightness change event. virtual void OnPlatformBrightnessChanged() = 0; + + // Notifies delegate that the Flutter semantics tree should be enabled or + // disabled. + virtual void OnUpdateSemanticsEnabled(bool enabled) = 0; }; } // namespace flutter diff --git a/shell/platform/windows/window_win32.cc b/shell/platform/windows/window_win32.cc index 20f7ebbc49acc..c1cebf8514bc1 100644 --- a/shell/platform/windows/window_win32.cc +++ b/shell/platform/windows/window_win32.cc @@ -44,12 +44,19 @@ static const int kMaxTouchDeviceId = 128; } // namespace -WindowWin32::WindowWin32() - : touch_id_generator_(kMinTouchDeviceId, kMaxTouchDeviceId) { +WindowWin32::WindowWin32() : WindowWin32(nullptr) {} + +WindowWin32::WindowWin32( + std::unique_ptr text_input_manager) + : touch_id_generator_(kMinTouchDeviceId, kMaxTouchDeviceId), + text_input_manager_(std::move(text_input_manager)) { // Get the DPI of the primary monitor as the initial DPI. If Per-Monitor V2 is // supported, |current_dpi_| should be updated in the // kWmDpiChangedBeforeParent message. current_dpi_ = GetDpiForHWND(nullptr); + if (text_input_manager_ == nullptr) { + text_input_manager_ = std::make_unique(); + } } WindowWin32::~WindowWin32() { @@ -119,7 +126,7 @@ LRESULT CALLBACK WindowWin32::WndProc(HWND const window, auto that = static_cast(cs->lpCreateParams); that->window_handle_ = window; - that->text_input_manager_.SetWindowHandle(window); + that->text_input_manager_->SetWindowHandle(window); RegisterTouchWindow(window, 0); } else if (WindowWin32* that = GetThisFromHandle(window)) { return that->HandleMessage(message, wparam, lparam); @@ -139,18 +146,45 @@ void WindowWin32::TrackMouseLeaveEvent(HWND hwnd) { } } +void WindowWin32::OnGetObject(UINT const message, + WPARAM const wparam, + LPARAM const lparam) { + LRESULT reference_result = static_cast(0L); + + // Only the lower 32 bits of lparam are valid when checking the object id + // because it sometimes gets sign-extended incorrectly (but not always). + DWORD obj_id = static_cast(static_cast(lparam)); + + bool is_msaa_request = static_cast(OBJID_CLIENT) == obj_id; + if (is_msaa_request) { + // On Windows, we don't get a notification that the screen reader has been + // enabled or disabled. There is an API to query for screen reader state, + // but that state isn't set by all screen readers, including by Narrator, + // the screen reader that ships with Windows: + // https://docs.microsoft.com/en-us/windows/win32/winauto/screen-reader-parameter + // + // Instead, we enable semantics in Flutter if Windows issues queries for + // Microsoft Active Accessibility (MSAA) COM objects. + OnUpdateSemanticsEnabled(true); + + // TODO(cbracken): https://github.com/flutter/flutter/issues/77838 + // Once AccessibilityBridge is wired up, look up the IAccessible + // representing the root view and call LresultFromObject. + } +} + void WindowWin32::OnImeSetContext(UINT const message, WPARAM const wparam, LPARAM const lparam) { if (wparam != 0) { - text_input_manager_.CreateImeWindow(); + text_input_manager_->CreateImeWindow(); } } void WindowWin32::OnImeStartComposition(UINT const message, WPARAM const wparam, LPARAM const lparam) { - text_input_manager_.CreateImeWindow(); + text_input_manager_->CreateImeWindow(); OnComposeBegin(); } @@ -158,21 +192,22 @@ void WindowWin32::OnImeComposition(UINT const message, WPARAM const wparam, LPARAM const lparam) { // Update the IME window position. - text_input_manager_.UpdateImeWindow(); + text_input_manager_->UpdateImeWindow(); if (lparam & GCS_COMPSTR) { // Read the in-progress composing string. - long pos = text_input_manager_.GetComposingCursorPosition(); + long pos = text_input_manager_->GetComposingCursorPosition(); std::optional text = - text_input_manager_.GetComposingString(); + text_input_manager_->GetComposingString(); if (text) { OnComposeChange(text.value(), pos); } - } else if (lparam & GCS_RESULTSTR) { + } + if (lparam & GCS_RESULTSTR) { // Commit but don't end composing. // Read the committed composing string. - long pos = text_input_manager_.GetComposingCursorPosition(); - std::optional text = text_input_manager_.GetResultString(); + long pos = text_input_manager_->GetComposingCursorPosition(); + std::optional text = text_input_manager_->GetResultString(); if (text) { OnComposeChange(text.value(), pos); OnComposeCommit(); @@ -183,7 +218,7 @@ void WindowWin32::OnImeComposition(UINT const message, void WindowWin32::OnImeEndComposition(UINT const message, WPARAM const wparam, LPARAM const lparam) { - text_input_manager_.DestroyImeWindow(); + text_input_manager_->DestroyImeWindow(); OnComposeEnd(); } @@ -195,8 +230,12 @@ void WindowWin32::OnImeRequest(UINT const message, // https://github.com/flutter/flutter/issues/74547 } +void WindowWin32::AbortImeComposing() { + text_input_manager_->AbortComposing(); +} + void WindowWin32::UpdateCursorRect(const Rect& rect) { - text_input_manager_.UpdateCaretRect(rect); + text_input_manager_->UpdateCaretRect(rect); } static uint16_t ResolveKeyCode(uint16_t original, @@ -373,6 +412,9 @@ WindowWin32::HandleMessage(UINT const message, static_cast(WHEEL_DELTA)), 0.0, kFlutterPointerDeviceKindMouse, kDefaultPointerDeviceId); break; + case WM_GETOBJECT: + OnGetObject(message, wparam, lparam); + break; case WM_INPUTLANGCHANGE: // TODO(cbracken): pass this to TextInputManager to aid with // language-specific issues. @@ -457,7 +499,8 @@ WindowWin32::HandleMessage(UINT const message, ? Win32MapVkToChar(keycode_for_char_message_) : IsPrintable(code_point) ? code_point : 0; - bool handled = OnKey(keycode_for_char_message_, scancode, WM_KEYDOWN, + bool handled = OnKey(keycode_for_char_message_, scancode, + message == WM_SYSCHAR ? WM_SYSKEYDOWN : WM_KEYDOWN, event_character, extended, was_down); keycode_for_char_message_ = 0; if (handled) { @@ -474,6 +517,12 @@ WindowWin32::HandleMessage(UINT const message, if (message == WM_CHAR) { text_for_scancode_on_redispatch_[scancode] = text; } + + // For system characters, always pass them to the default WndProc so + // that system keys like the ALT-TAB are processed correctly. + if (message == WM_SYSCHAR) { + break; + } return 0; } } @@ -525,9 +574,17 @@ WindowWin32::HandleMessage(UINT const message, const bool extended = ((lparam >> 24) & 0x01) == 0x01; // If the key is a modifier, get its side. keyCode = ResolveKeyCode(keyCode, extended, scancode); - const int action = is_keydown_message ? WM_KEYDOWN : WM_KEYUP; const bool was_down = lparam & 0x40000000; + bool is_syskey = message == WM_SYSKEYDOWN || message == WM_SYSKEYUP; + const int action = is_keydown_message + ? (is_syskey ? WM_SYSKEYDOWN : WM_KEYDOWN) + : (is_syskey ? WM_SYSKEYUP : WM_KEYUP); if (OnKey(keyCode, scancode, action, 0, extended, was_down)) { + // For system keys, always pass them to the default WndProc so that keys + // like the ALT-TAB or Kanji switches are processed correctly. + if (is_syskey) { + break; + } return 0; } break; @@ -554,6 +611,7 @@ HWND WindowWin32::GetWindowHandle() { void WindowWin32::Destroy() { if (window_handle_) { + text_input_manager_->SetWindowHandle(nullptr); DestroyWindow(window_handle_); window_handle_ = nullptr; } diff --git a/shell/platform/windows/window_win32.h b/shell/platform/windows/window_win32.h index 4e14faa4680c8..c106b82819eb7 100644 --- a/shell/platform/windows/window_win32.h +++ b/shell/platform/windows/window_win32.h @@ -25,6 +25,7 @@ namespace flutter { class WindowWin32 { public: WindowWin32(); + WindowWin32(std::unique_ptr text_input_manager); virtual ~WindowWin32(); // Initializes as a child window with size using |width| and |height| and @@ -121,6 +122,14 @@ class WindowWin32 { bool extended, bool was_down) = 0; + // Called when the OS requests a COM object. + // + // The primary use of this function is to supply Windows with wrapped + // semantics objects for use by Windows accessibility. + void OnGetObject(UINT const message, + WPARAM const wparam, + LPARAM const lparam); + // Called when IME composing begins. virtual void OnComposeBegin() = 0; @@ -135,37 +144,44 @@ class WindowWin32 { // Called when a window is activated in order to configure IME support for // multi-step text input. - void OnImeSetContext(UINT const message, - WPARAM const wparam, - LPARAM const lparam); + virtual void OnImeSetContext(UINT const message, + WPARAM const wparam, + LPARAM const lparam); // Called when multi-step text input begins when using an IME. - void OnImeStartComposition(UINT const message, - WPARAM const wparam, - LPARAM const lparam); + virtual void OnImeStartComposition(UINT const message, + WPARAM const wparam, + LPARAM const lparam); // Called when edits/commit of multi-step text input occurs when using an IME. - void OnImeComposition(UINT const message, - WPARAM const wparam, - LPARAM const lparam); + virtual void OnImeComposition(UINT const message, + WPARAM const wparam, + LPARAM const lparam); // Called when multi-step text input ends when using an IME. - void OnImeEndComposition(UINT const message, - WPARAM const wparam, - LPARAM const lparam); + virtual void OnImeEndComposition(UINT const message, + WPARAM const wparam, + LPARAM const lparam); // Called when the user triggers an IME-specific request such as input // reconversion, where an existing input sequence is returned to composing // mode to select an alternative candidate conversion. - void OnImeRequest(UINT const message, - WPARAM const wparam, - LPARAM const lparam); + virtual void OnImeRequest(UINT const message, + WPARAM const wparam, + LPARAM const lparam); + + // Called when the app ends IME composing, such as when the text input client + // is cleared or changed. + virtual void AbortImeComposing(); // Called when the cursor rect has been updated. // // |rect| is in Win32 window coordinates. virtual void UpdateCursorRect(const Rect& rect); + // Called when accessibility support is enabled or disabled. + virtual void OnUpdateSemanticsEnabled(bool enabled) = 0; + // Called when mouse scrollwheel input occurs. virtual void OnScroll(double delta_x, double delta_y, @@ -249,7 +265,7 @@ class WindowWin32 { std::map text_for_scancode_on_redispatch_; // Manages IME state. - TextInputManagerWin32 text_input_manager_; + std::unique_ptr text_input_manager_; // Used for temporarily storing the WM_TOUCH-provided touch points. std::vector touch_points_; diff --git a/shell/platform/windows/window_win32_unittests.cc b/shell/platform/windows/window_win32_unittests.cc index 92fbf009fd646..d494a0ff16e43 100644 --- a/shell/platform/windows/window_win32_unittests.cc +++ b/shell/platform/windows/window_win32_unittests.cc @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "flutter/shell/platform/windows/testing/mock_text_input_manager_win32.h" #include "flutter/shell/platform/windows/testing/mock_window_win32.h" #include "gtest/gtest.h" using testing::_; +using testing::Invoke; +using testing::Return; namespace flutter { namespace testing { @@ -32,6 +35,86 @@ TEST(MockWin32Window, VerticalScroll) { window.InjectWindowMessage(WM_MOUSEWHEEL, MAKEWPARAM(0, scroll_amount), 0); } +TEST(MockWin32Window, OnImeCompositionCompose) { + MockTextInputManagerWin32* text_input_manager = + new MockTextInputManagerWin32(); + std::unique_ptr text_input_manager_ptr( + text_input_manager); + MockWin32Window window(std::move(text_input_manager_ptr)); + EXPECT_CALL(*text_input_manager, GetComposingString()) + .WillRepeatedly( + Return(std::optional(std::u16string(u"nihao")))); + EXPECT_CALL(*text_input_manager, GetResultString()) + .WillRepeatedly( + Return(std::optional(std::u16string(u"`}")))); + EXPECT_CALL(*text_input_manager, GetComposingCursorPosition()) + .WillRepeatedly(Return((int)0)); + + EXPECT_CALL(window, OnComposeChange(std::u16string(u"nihao"), 0)).Times(1); + EXPECT_CALL(window, OnComposeChange(std::u16string(u"`}"), 0)).Times(0); + EXPECT_CALL(window, OnComposeCommit()).Times(0); + ON_CALL(window, OnImeComposition) + .WillByDefault(Invoke(&window, &MockWin32Window::CallOnImeComposition)); + EXPECT_CALL(window, OnImeComposition(_, _, _)).Times(1); + + // Send an IME_COMPOSITION event that contains just the composition string. + window.InjectWindowMessage(WM_IME_COMPOSITION, 0, GCS_COMPSTR); +} + +TEST(MockWin32Window, OnImeCompositionResult) { + MockTextInputManagerWin32* text_input_manager = + new MockTextInputManagerWin32(); + std::unique_ptr text_input_manager_ptr( + text_input_manager); + MockWin32Window window(std::move(text_input_manager_ptr)); + EXPECT_CALL(*text_input_manager, GetComposingString()) + .WillRepeatedly( + Return(std::optional(std::u16string(u"nihao")))); + EXPECT_CALL(*text_input_manager, GetResultString()) + .WillRepeatedly( + Return(std::optional(std::u16string(u"`}")))); + EXPECT_CALL(*text_input_manager, GetComposingCursorPosition()) + .WillRepeatedly(Return((int)0)); + + EXPECT_CALL(window, OnComposeChange(std::u16string(u"nihao"), 0)).Times(0); + EXPECT_CALL(window, OnComposeChange(std::u16string(u"`}"), 0)).Times(1); + EXPECT_CALL(window, OnComposeCommit()).Times(1); + ON_CALL(window, OnImeComposition) + .WillByDefault(Invoke(&window, &MockWin32Window::CallOnImeComposition)); + EXPECT_CALL(window, OnImeComposition(_, _, _)).Times(1); + + // Send an IME_COMPOSITION event that contains just the result string. + window.InjectWindowMessage(WM_IME_COMPOSITION, 0, GCS_RESULTSTR); +} + +TEST(MockWin32Window, OnImeCompositionComposeAndResult) { + MockTextInputManagerWin32* text_input_manager = + new MockTextInputManagerWin32(); + std::unique_ptr text_input_manager_ptr( + text_input_manager); + MockWin32Window window(std::move(text_input_manager_ptr)); + EXPECT_CALL(*text_input_manager, GetComposingString()) + .WillRepeatedly( + Return(std::optional(std::u16string(u"nihao")))); + EXPECT_CALL(*text_input_manager, GetResultString()) + .WillRepeatedly( + Return(std::optional(std::u16string(u"`}")))); + EXPECT_CALL(*text_input_manager, GetComposingCursorPosition()) + .WillRepeatedly(Return((int)0)); + + EXPECT_CALL(window, OnComposeChange(std::u16string(u"nihao"), 0)).Times(1); + EXPECT_CALL(window, OnComposeChange(std::u16string(u"`}"), 0)).Times(1); + EXPECT_CALL(window, OnComposeCommit()).Times(1); + ON_CALL(window, OnImeComposition) + .WillByDefault(Invoke(&window, &MockWin32Window::CallOnImeComposition)); + EXPECT_CALL(window, OnImeComposition(_, _, _)).Times(1); + + // send an IME_COMPOSITION event that contains both the result string and the + // composition string. + window.InjectWindowMessage(WM_IME_COMPOSITION, 0, + GCS_COMPSTR | GCS_RESULTSTR); +} + TEST(MockWin32Window, HorizontalScroll) { MockWin32Window window; const int scroll_amount = 10; @@ -59,6 +142,22 @@ TEST(MockWin32Window, KeyUp) { window.InjectWindowMessage(WM_KEYUP, 16, lparam); } +TEST(MockWin32Window, SysKeyDown) { + MockWin32Window window; + EXPECT_CALL(window, OnKey(_, _, _, _, _, _)).Times(1); + LPARAM lparam = CreateKeyEventLparam(42, false, false); + // send a "Shift" key down event. + window.InjectWindowMessage(WM_SYSKEYDOWN, 16, lparam); +} + +TEST(MockWin32Window, SysKeyUp) { + MockWin32Window window; + EXPECT_CALL(window, OnKey(_, _, _, _, _, _)).Times(1); + LPARAM lparam = CreateKeyEventLparam(42, false, true); + // send a "Shift" key up event. + window.InjectWindowMessage(WM_SYSKEYUP, 16, lparam); +} + TEST(MockWin32Window, KeyDownPrintable) { MockWin32Window window; LPARAM lparam = CreateKeyEventLparam(30, false, false); diff --git a/shell/profiling/sampling_profiler.h b/shell/profiling/sampling_profiler.h index 8b4b06c007622..d68f88a643d3e 100644 --- a/shell/profiling/sampling_profiler.h +++ b/shell/profiling/sampling_profiler.h @@ -31,10 +31,10 @@ struct CpuUsageInfo { }; /** - * @brief Memory usage stats. `dirty_memory_usage` is the the memory usage (in + * @brief Memory usage stats. `dirty_memory_usage` is the memory usage (in * MB) such that the app uses its physical memory for dirty memory. Dirty memory * is the memory data that cannot be paged to disk. `owned_shared_memory_usage` - * is the memory usage (in MB) such that the app uses its physicaal memory for + * is the memory usage (in MB) such that the app uses its physical memory for * shared memory, including loaded frameworks and executables. On iOS, it's * `physical memory - dirty memory`. */ diff --git a/shell/vmservice/BUILD.gn b/shell/vmservice/BUILD.gn new file mode 100644 index 0000000000000..becc886597fb4 --- /dev/null +++ b/shell/vmservice/BUILD.gn @@ -0,0 +1,12 @@ +# 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("//flutter/build/dart/rules.gni") + +# Build a minimal snapshot that can be used to launch the VM service isolate. +flutter_snapshot("vmservice_snapshot") { + main_dart = "empty.dart" + package_config = ".dart_tool/package_config.json" + output_aot_lib = "libvmservice_snapshot.so" +} diff --git a/shell/vmservice/empty.dart b/shell/vmservice/empty.dart new file mode 100644 index 0000000000000..120fa0bae202f --- /dev/null +++ b/shell/vmservice/empty.dart @@ -0,0 +1,6 @@ +// 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. + +// This is used to build an empty snapshot that can be used to start the VM service isolate. +void main(List args) {} diff --git a/shell/vmservice/pubspec.yaml b/shell/vmservice/pubspec.yaml new file mode 100644 index 0000000000000..7c1f613b34059 --- /dev/null +++ b/shell/vmservice/pubspec.yaml @@ -0,0 +1,8 @@ +# 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. + +name: vmservice_snapshot +publish_to: none +environment: + sdk: '>=2.12.0 <3.0.0' diff --git a/sky/packages/sky_engine/LICENSE b/sky/packages/sky_engine/LICENSE index a3bcef59a80fe..96f7ad356af58 100644 --- a/sky/packages/sky_engine/LICENSE +++ b/sky/packages/sky_engine/LICENSE @@ -5943,33 +5943,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- fuchsia_sdk -Copyright 2015 The Fuchsia 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. - -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. --------------------------------------------------------------------------------- -fuchsia_sdk - Copyright 2016 The Fuchsia Authors. All rights reserved. Redistribution and use in source and binary forms, with or without @@ -11390,6 +11363,36 @@ 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. -------------------------------------------------------------------------------- +musl_compat + +Copyright 2021 Google LLC + +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. +-------------------------------------------------------------------------------- pkg Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file diff --git a/testing/BUILD.gn b/testing/BUILD.gn index e77968c536bdf..31b0edec486ad 100644 --- a/testing/BUILD.gn +++ b/testing/BUILD.gn @@ -88,6 +88,18 @@ source_set("skia") { ] } +dart_snapshot_kernel("vmservice_kernel") { + dart_main = + rebase_path("//flutter/shell/vmservice/empty.dart", root_build_dir) + dart_kernel = "$target_gen_dir/assets/vmservice_kernel.bin" +} + +dart_snapshot_aot("vmservice_snapshot") { + dart_kernel = "$target_gen_dir/assets/vmservice_kernel.bin" + dart_elf_filename = "libvmservice_snapshot.so" + deps = [ ":vmservice_kernel" ] +} + source_set("fixture_test") { testonly = true @@ -103,25 +115,37 @@ source_set("fixture_test") { "//flutter/common", "//flutter/runtime", ] + + if (flutter_runtime_mode == "profile") { + public_deps += [ ":vmservice_snapshot" ] + } } if (enable_unittests) { source_set("vulkan") { testonly = true + sources = [ + "test_vulkan_context.cc", + "test_vulkan_context.h", + ] + + defines = [ "TEST_VULKAN_PROCS" ] + deps = [ ":skia", "//flutter/fml", + "//flutter/vulkan", ] - # SwiftShader only supports x86/x64_64 + # SwiftShader only supports x86/x86_64 if (target_cpu == "x86" || target_cpu == "x64") { configs += [ "//third_party/swiftshader_flutter:swiftshader_config" ] deps += [ "//third_party/swiftshader_flutter:swiftshader_vulkan" ] } } - # SwiftShader only supports x86/x64_64 + # SwiftShader only supports x86/x86_64 if (target_cpu == "x86" || target_cpu == "x64") { source_set("opengl") { testonly = true @@ -158,9 +182,12 @@ if (enable_unittests) { "test_metal_surface_impl.mm", ] + # Skia's Vulkan support is enabled for all platforms, and so parts of + # Skia's graphics context reference Vulkan symbols. deps = [ ":skia", "//flutter/fml", + "//flutter/vulkan", ] } @@ -183,6 +210,7 @@ if (enable_unittests) { ":testing", ":testing_fixtures", "//flutter/runtime:libdart", + "//flutter/vulkan", ] if (shell_enable_metal) { diff --git a/testing/android_background_image/android/BUILD.gn b/testing/android_background_image/android/BUILD.gn index ac5b93bc01ef0..f9e90d1cdcd6f 100644 --- a/testing/android_background_image/android/BUILD.gn +++ b/testing/android_background_image/android/BUILD.gn @@ -16,7 +16,8 @@ gradle_task("android_lint") { task = "lint" gradle_project_dir = rebase_path(".") sources = _android_sources - outputs = [ "$root_out_dir/android_background_image/build/reports" ] + outputs = + [ "$root_out_dir/android_background_image/reports/lint-results.xml" ] deps = [ "//flutter/testing/android_background_image:android_background_image_snapshot" ] } diff --git a/testing/android_background_image/android/app/build.gradle b/testing/android_background_image/android/app/build.gradle index 362f23c0563b8..68a3b1dd1fae1 100644 --- a/testing/android_background_image/android/app/build.gradle +++ b/testing/android_background_image/android/app/build.gradle @@ -7,6 +7,9 @@ android { showAll true warningsAsErrors true checkTestSources true + htmlReport false + xmlReport true + xmlOutput file("${rootProject.buildDir}/reports/lint-results.xml") // UnpackedNativeCode can break stack unwinding - see b/193408481 // NewerVersionAvailable and GradleDependency need to be taken care of // by a roller rather than as part of CI. @@ -45,4 +48,5 @@ dependencies { implementation files(project.property('flutter_jar')) implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.lifecycle:lifecycle-common-java8:2.2.0-alpha01' + implementation 'androidx.tracing:tracing:1.0.0' } diff --git a/testing/benchmark/bin/parse_and_send.dart b/testing/benchmark/bin/parse_and_send.dart index 0a5e04b63bb46..57e999afb7167 100644 --- a/testing/benchmark/bin/parse_and_send.dart +++ b/testing/benchmark/bin/parse_and_send.dart @@ -121,5 +121,6 @@ Future main(List args) async { int.parse(pointsAndDate.date) * 1000, isUtc: true, ), + 'flutter_engine_benchmark', ); } diff --git a/testing/dart/compile_test.gni b/testing/dart/compile_test.gni index c13b2d1e264a0..fbdf76ea849da 100644 --- a/testing/dart/compile_test.gni +++ b/testing/dart/compile_test.gni @@ -57,7 +57,7 @@ template("compile_flutter_dart_test") { if (flutter_prebuilt_dart_sdk) { action(target_name) { testonly = true - deps = common_deps + [ "//flutter:dart_sdk" ] + deps = common_deps pool = "//flutter/build/dart:dart_pool" script = "//build/gn_run_binary.py" inputs = [ invoker.dart_file ] diff --git a/testing/dart/fragment_shader_test.dart b/testing/dart/fragment_shader_test.dart index e9099c34205d5..f2e6fce7a55c1 100644 --- a/testing/dart/fragment_shader_test.dart +++ b/testing/dart/fragment_shader_test.dart @@ -14,34 +14,57 @@ import 'package:path/path.dart' as path; import 'shader_test_file_utils.dart'; void main() { - test('throws exception for invalid shader', () { + test('throws exception for invalid shader', () async { final ByteBuffer invalidBytes = Uint8List.fromList([1, 2, 3, 4, 5]).buffer; - expect(() => FragmentShader(spirv: invalidBytes), throws); + try { + await FragmentProgram.compile(spirv: invalidBytes); + fail('expected compile to throw an exception'); + } catch (_) { + } }); test('simple shader renders correctly', () async { final Uint8List shaderBytes = await spvFile('general_shaders', 'functions.spv').readAsBytes(); - final FragmentShader shader = FragmentShader( + final FragmentProgram program = await FragmentProgram.compile( spirv: shaderBytes.buffer, + ); + final Shader shader = program.shader( floatUniforms: Float32List.fromList([1]), ); _expectShaderRendersGreen(shader); }); - test('shader with functions renders green', () { + test('shader with functions renders green', () async { final ByteBuffer spirv = spvFile('general_shaders', 'functions.spv').readAsBytesSync().buffer; - final FragmentShader shader = FragmentShader( + final FragmentProgram program = await FragmentProgram.compile( spirv: spirv, + ); + final Shader shader = program.shader( floatUniforms: Float32List.fromList([1]), ); _expectShaderRendersGreen(shader); }); - test('shader with uniforms renders and updates correctly', () async { + test('blue-green image renders green', () async { + final ByteBuffer spirv = spvFile('general_shaders', 'blue_green_sampler.spv').readAsBytesSync().buffer; + final FragmentProgram program = await FragmentProgram.compile( + debugPrint: true, + spirv: spirv, + ); + final Image blueGreenImage = await _createBlueGreenImage(); + final ImageShader imageShader = ImageShader( + blueGreenImage, TileMode.clamp, TileMode.clamp, _identityMatrix); + final Shader shader = program.shader( + samplerUniforms: [imageShader], + ); + await _expectShaderRendersGreen(shader); + }); + + test('shader with uniforms renders correctly', () async { final Uint8List shaderBytes = await spvFile('general_shaders', 'uniforms.spv').readAsBytes(); - final FragmentShader shader = FragmentShader(spirv: shaderBytes.buffer); + final FragmentProgram program = await FragmentProgram.compile(spirv: shaderBytes.buffer); - shader.update( + final Shader shader = program.shader( floatUniforms: Float32List.fromList([ 0.0, // iFloatUniform 0.25, // iVec2Uniform.x @@ -75,6 +98,42 @@ void main() { expect(supportedOpShaders.isNotEmpty, true); _expectShadersRenderGreen(supportedOpShaders); _expectShadersHaveOp(supportedOpShaders, false /* glsl ops */); + + test('equality depends on floatUniforms', () async { + final ByteBuffer spirv = spvFile('general_shaders', 'simple.spv') + .readAsBytesSync().buffer; + final FragmentProgram program = await FragmentProgram.compile(spirv: spirv); + final Float32List ones = Float32List.fromList([1]); + final Float32List zeroes = Float32List.fromList([0]); + + { + final a = program.shader(floatUniforms: ones); + final b = program.shader(floatUniforms: ones); + expect(a, b); + expect(a.hashCode, b.hashCode); + } + + { + final a = program.shader(floatUniforms: ones); + final b = program.shader(floatUniforms: zeroes); + expect(a, notEquals(b)); + expect(a.hashCode, notEquals(b.hashCode)); + } + }); + + test('equality depends on spirv', () async { + final ByteBuffer spirvA = spvFile('general_shaders', 'simple.spv') + .readAsBytesSync().buffer; + final ByteBuffer spirvB = spvFile('general_shaders', 'uniforms.spv') + .readAsBytesSync().buffer; + final FragmentProgram programA = await FragmentProgram.compile(spirv: spirvA); + final FragmentProgram programB = await FragmentProgram.compile(spirv: spirvB); + final a = programA.shader(); + final b = programB.shader(); + + expect(a, notEquals(b)); + expect(a.hashCode, notEquals(b.hashCode)); + }); } // Expect that all of the spirv shaders in this folder render green. @@ -82,9 +141,11 @@ void main() { // of the file name within the test case. void _expectShadersRenderGreen(Map shaders) { for (final String key in shaders.keys) { - test('$key renders green', () { - final FragmentShader shader = FragmentShader( + test('$key renders green', () async { + final FragmentProgram program = await FragmentProgram.compile( spirv: shaders[key]!, + ); + final Shader shader = program.shader( floatUniforms: Float32List.fromList([1]), ); _expectShaderRendersGreen(shader); @@ -138,7 +199,7 @@ void _expectShaderHasOp(ByteBuffer spirv, String filename, bool glsl) { } // Expects that a spirv shader only outputs the color green. -Future _expectShaderRendersGreen(FragmentShader shader) async { +Future _expectShaderRendersGreen(Shader shader) async { final ByteData renderedBytes = (await _imageByteDataFromShader( shader: shader, imageDimension: _shaderImageDimension, @@ -149,7 +210,7 @@ Future _expectShaderRendersGreen(FragmentShader shader) async { } Future _imageByteDataFromShader({ - required FragmentShader shader, + required Shader shader, int imageDimension = 100, }) async { final PictureRecorder recorder = PictureRecorder(); @@ -190,6 +251,7 @@ Map _loadSpv(String leafFolderName) { const int _shaderImageDimension = 4; const Color _greenColor = Color(0xFF00FF00); +const Color _blueColor = Color(0xFF0000FF); // Precision for checking uniform values. const double epsilon = 0.5 / 255.0; @@ -198,3 +260,43 @@ const double epsilon = 0.5 / 255.0; double toFloat(int v) => v.toDouble() / 255.0; String toHexString(int color) => '#${color.toRadixString(16)}'; + +// 10x10 image where the left half is blue and the right half is +// green. +Future _createBlueGreenImage() async { + final int length = 10; + final int bytesPerPixel = 4; + final Uint8List pixels = Uint8List(length * length * bytesPerPixel); + int i = 0; + for (int y = 0; y < length; y++) { + for (int x = 0; x < length; x++) { + if (x < length/2) { + pixels[i+2] = 0xFF; // blue channel + } else { + pixels[i+1] = 0xFF; // green channel + } + pixels[i+3] = 0xFF; // alpha channel + i += bytesPerPixel; + } + } + final ImageDescriptor descriptor = ImageDescriptor.raw( + await ImmutableBuffer.fromUint8List(pixels), + width: length, + height: length, + pixelFormat: PixelFormat.rgba8888, + ); + final Codec codec = await descriptor.instantiateCodec(); + final FrameInfo frame = await codec.getNextFrame(); + return frame.image; +} + +// A single uniform with value 1. +final Float32List _singleUniform = Float32List.fromList([1]); + +final Float64List _identityMatrix = Float64List.fromList([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, +]); + diff --git a/testing/dart/observatory/BUILD.gn b/testing/dart/observatory/BUILD.gn index ea5d285e0fd2c..e2acc5af47800 100644 --- a/testing/dart/observatory/BUILD.gn +++ b/testing/dart/observatory/BUILD.gn @@ -4,7 +4,10 @@ import("//flutter/testing/dart/compile_test.gni") -tests = [ "skp_test.dart" ] +tests = [ + "skp_test.dart", + "tracing_test.dart", +] foreach(test, tests) { compile_flutter_dart_test("compile_$test") { diff --git a/testing/dart/observatory/tracing_test.dart b/testing/dart/observatory/tracing_test.dart new file mode 100644 index 0000000000000..597d436d2eb16 --- /dev/null +++ b/testing/dart/observatory/tracing_test.dart @@ -0,0 +1,68 @@ +// 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:async'; +import 'dart:developer' as developer; +import 'dart:ui'; + +import 'package:litetest/litetest.dart'; +import 'package:vm_service/vm_service.dart' as vms; +import 'package:vm_service/vm_service_io.dart'; + +void main() { + test('Canvas.saveLayer emits tracing', () async { + final developer.ServiceProtocolInfo info = await developer.Service.getInfo(); + + if (info.serverUri == null) { + fail('This test must not be run with --disable-observatory.'); + } + + final vms.VmService vmService = await vmServiceConnectUri( + 'ws://localhost:${info.serverUri!.port}${info.serverUri!.path}ws', + ); + + final Completer completer = Completer(); + window.onBeginFrame = (Duration timeStamp) async { + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawColor(const Color(0xff0000ff), BlendMode.srcOut); + canvas.drawPaint(Paint()..imageFilter = ImageFilter.blur(sigmaX: 2, sigmaY: 3)); + canvas.saveLayer(null, Paint()); + canvas.saveLayer(const Rect.fromLTWH(0, 0, 100, 100), Paint()); + canvas.drawRect(const Rect.fromLTRB(10, 10, 20, 20), Paint()); + canvas.restore(); + canvas.restore(); + final Picture picture = recorder.endRecording(); + + final SceneBuilder builder = SceneBuilder(); + builder.addPicture(Offset.zero, picture); + final Scene scene = builder.build(); + + await scene.toImage(100, 100); + scene.dispose(); + completer.complete(); + }; + window.scheduleFrame(); + await completer.future; + + final vms.Timeline timeline = await vmService.getVMTimeline(); + await vmService.dispose(); + + int saveLayerRecordCount = 0; + int saveLayerCount = 0; + for (final vms.TimelineEvent event in timeline.traceEvents!) { + final Map json = event.json!; + if (json['ph'] == 'B') { + if (json['name'] == 'ui.Canvas::saveLayer (Recorded)') { + saveLayerRecordCount += 1; + } + if (json['name'] == 'Canvas::saveLayer') { + saveLayerCount += 1; + } + } + } + expect(saveLayerRecordCount, 3); + expect(saveLayerCount, 3); + }); +} diff --git a/testing/dart/semantics_test.dart b/testing/dart/semantics_test.dart index 103722b0b9517..e3ecb3dae701a 100644 --- a/testing/dart/semantics_test.dart +++ b/testing/dart/semantics_test.dart @@ -6,7 +6,9 @@ import 'dart:ui'; import 'package:litetest/litetest.dart'; -/// Verifies Semantics flags and actions. +// The body of this file is the same as ../../lib/web_ui/test/engine/semantics/semantics_api_test.dart +// Please keep them in sync. + void main() { // This must match the number of flags in lib/ui/semantics.dart const int numSemanticsFlags = 25; @@ -29,4 +31,12 @@ void main() { expect(SemanticsAction.values[flag].toString(), startsWith('SemanticsAction.')); } }); + + test('SpellOutStringAttribute.toString', () async { + expect(SpellOutStringAttribute(range: const TextRange(start: 2, end: 5)).toString(), 'SpellOutStringAttribute(TextRange(start: 2, end: 5))'); + }); + + test('LocaleStringAttribute.toString', () async { + expect(LocaleStringAttribute(range: const TextRange(start: 2, end: 5), locale: const Locale('test')).toString(), 'LocaleStringAttribute(TextRange(start: 2, end: 5), test)'); + }); } diff --git a/testing/dart_fixture.cc b/testing/dart_fixture.cc index 4e86a33e33c7f..2852f4165fb54 100644 --- a/testing/dart_fixture.cc +++ b/testing/dart_fixture.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "flutter/testing/dart_fixture.h" +#include "flutter/fml/paths.h" namespace flutter::testing { @@ -48,6 +49,10 @@ void DartFixture::SetSnapshotsAndAssets(Settings& settings) { // snapshots will be present in the application AOT dylib. if (DartVM::IsRunningPrecompiledCode()) { FML_CHECK(PrepareSettingsForAOTWithSymbols(settings, aot_symbols_)); +#if OS_LINUX + settings.vmservice_snapshot_library_path.emplace_back(fml::paths::JoinPaths( + {GetTestingAssetsPath(), "libvmservice_snapshot.so"})); +#endif // OS_LINUX } else { settings.application_kernels = [this]() -> Mappings { std::vector> kernel_mappings; diff --git a/testing/dart_isolate_runner.cc b/testing/dart_isolate_runner.cc index cfe0aa1a9c035..760764b93f3b3 100644 --- a/testing/dart_isolate_runner.cc +++ b/testing/dart_isolate_runner.cc @@ -130,10 +130,12 @@ std::unique_ptr RunDartCodeInIsolateOnUITaskRunner( vm_data->GetIsolateSnapshot(), // isolate snapshot nullptr, // platform configuration DartIsolate::Flags{}, // flags + nullptr, // root isolate create callback settings.isolate_create_callback, // isolate create callback settings.isolate_shutdown_callback, // isolate shutdown callback entrypoint, // entrypoint std::nullopt, // library + {}, // args std::move(isolate_configuration), // isolate configuration context // engine context ) diff --git a/testing/fuchsia/meta/test_suite.cml b/testing/fuchsia/meta/test_suite.cml index 498503ae51fac..126f37a46d4aa 100644 --- a/testing/fuchsia/meta/test_suite.cml +++ b/testing/fuchsia/meta/test_suite.cml @@ -1,4 +1,7 @@ { + facets: { + "fuchsia.test": { type: "system" }, + }, program: { // TODO(fxbug.dev/80338): Switch to gtest_runner after the filters in // `gtest_filters.yaml` are supported by `--test-filter` and diff --git a/testing/fuchsia/test_suites.yaml b/testing/fuchsia/test_suites.yaml index 60ab48cb6fa07..95d0a9e8fd949 100644 --- a/testing/fuchsia/test_suites.yaml +++ b/testing/fuchsia/test_suites.yaml @@ -4,14 +4,12 @@ # Legacy Component Framework v1 components. - test_command: run-test-component fuchsia-pkg://fuchsia.com/flutter_runner_tests#meta/flutter_runner_tests.cmx package: flutter_runner_tests-0.far -# TODO(richkadel): Enable this test once the CI fuchsia image for femu tests has been updated to -# meet the test requirements. Changes are in progress to support this test. -# - test_command: run-test-component fuchsia-pkg://fuchsia.com/flutter-embedder-test2#meta/flutter-embedder-test2.cmx -# packages: -# - flutter-embedder-test2-0.far -# - gen/flutter/shell/platform/fuchsia/flutter/integration_flutter_tests/embedder/child-view2/child-view2/child-view2.far -# - gen/flutter/shell/platform/fuchsia/flutter/integration_flutter_tests/embedder/parent-view2/parent-view2/parent-view2.far -# - flutter_jit_runner-0.far +- test_command: run-test-component fuchsia-pkg://fuchsia.com/flutter-embedder-test2#meta/flutter-embedder-test2.cmx + packages: + - flutter-embedder-test2-0.far + - gen/flutter/shell/platform/fuchsia/flutter/integration_flutter_tests/embedder/child-view2/child-view2/child-view2.far + - gen/flutter/shell/platform/fuchsia/flutter/integration_flutter_tests/embedder/parent-view2/parent-view2/parent-view2.far + - flutter_jit_runner-0.far # v2 components. - test_command: run-test-suite fuchsia-pkg://fuchsia.com/flutter_runner_tzdata_tests#meta/flutter_runner_tzdata_tests.cm @@ -30,7 +28,8 @@ package: txt_tests-0.far - test_command: run-test-suite fuchsia-pkg://fuchsia.com/ui_tests#meta/ui_tests.cm package: ui_tests-0.far -- test_command: run-test-suite fuchsia-pkg://fuchsia.com/embedder_tests#meta/embedder_tests.cm + # TODO(fxb/87493): re-enable when this doesn't crash. +- test_command: run-test-suite fuchsia-pkg://fuchsia.com/embedder_tests#meta/embedder_tests.cm -- --gtest_filter=-Embedder11yTest.A11yTreeIsConsistent package: embedder_tests-0.far - test_command: run-test-suite fuchsia-pkg://fuchsia.com/dart_utils_tests#meta/dart_utils_tests.cm package: dart_utils_tests-0.far diff --git a/testing/run_all_unittests.cc b/testing/run_all_unittests.cc index eb1ae28256bc3..5e6c934185912 100644 --- a/testing/run_all_unittests.cc +++ b/testing/run_all_unittests.cc @@ -22,8 +22,8 @@ std::optional GetTestTimeoutFromArgs(int argc, char** argv) { std::string timeout_seconds; if (!command_line.GetOptionValue("timeout", &timeout_seconds)) { - // No timeout specified. Default to 30s. - return fml::TimeDelta::FromSeconds(30u); + // No timeout specified. Default to 120s. + return fml::TimeDelta::FromSeconds(120u); } const auto seconds = std::stoi(timeout_seconds); diff --git a/testing/run_tests.py b/testing/run_tests.py index 1c12894c0ded8..15940a9d8584c 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -42,7 +42,7 @@ def RunCmd(cmd, forbidden_output=[], expect_failure=False, env=None, **kwargs): start_time = time.time() stdout_pipe = sys.stdout if not forbidden_output else subprocess.PIPE stderr_pipe = sys.stderr if not forbidden_output else subprocess.PIPE - process = subprocess.Popen(cmd, stdout=stdout_pipe, stderr=stderr_pipe, env=env, **kwargs) + process = subprocess.Popen(cmd, stdout=stdout_pipe, stderr=stderr_pipe, env=env, universal_newlines=True, **kwargs) stdout, stderr = process.communicate() end_time = time.time() @@ -179,11 +179,11 @@ def RunCCTests(build_dir, filter, coverage, capture_core_dump): RunEngineExecutable(build_dir, 'client_wrapper_unittests', filter, shuffle_flags, coverage=coverage) - # https://github.com/flutter/flutter/issues/36294 - if not IsWindows(): - RunEngineExecutable(build_dir, 'embedder_unittests', filter, shuffle_flags, coverage=coverage) - RunEngineExecutable(build_dir, 'embedder_proctable_unittests', filter, shuffle_flags, coverage=coverage) - else: + RunEngineExecutable(build_dir, 'embedder_unittests', filter, shuffle_flags, coverage=coverage) + + RunEngineExecutable(build_dir, 'embedder_proctable_unittests', filter, shuffle_flags, coverage=coverage) + + if IsWindows(): RunEngineExecutable(build_dir, 'flutter_windows_unittests', filter, shuffle_flags, coverage=coverage) RunEngineExecutable(build_dir, 'client_wrapper_windows_unittests', filter, shuffle_flags, coverage=coverage) @@ -297,12 +297,13 @@ def EnsureDebugUnoptSkyPackagesAreBuilt(): def EnsureIosTestsAreBuilt(ios_out_dir): """Builds the engine variant and the test dylib containing the XCTests""" tmp_out_dir = os.path.join(out_dir, ios_out_dir) + ios_test_lib = os.path.join(tmp_out_dir, 'libios_test_flutter.dylib') message = [] message.append('gn --ios --unoptimized --runtime-mode=debug --no-lto --simulator') message.append('autoninja -C %s ios_test_flutter' % ios_out_dir) - final_message = '%s doesn\'t exist. Please run the following commands: \n%s' % ( - ios_out_dir, '\n'.join(message)) - assert os.path.exists(tmp_out_dir), final_message + final_message = '%s or %s doesn\'t exist. Please run the following commands: \n%s' % ( + ios_out_dir, ios_test_lib, '\n'.join(message)) + assert os.path.exists(tmp_out_dir) and os.path.exists(ios_test_lib), final_message def AssertExpectedXcodeVersion(): diff --git a/testing/scenario_app/android/app/build.gradle b/testing/scenario_app/android/app/build.gradle index 858b9196a1c3d..3e1271235aa5f 100644 --- a/testing/scenario_app/android/app/build.gradle +++ b/testing/scenario_app/android/app/build.gradle @@ -63,6 +63,7 @@ dependencies { implementation 'com.facebook.testing.screenshot:layout-hierarchy-common:0.12.0' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.tracing:tracing:1.0.0' implementation 'com.google.android.material:material:1.0.0' implementation 'androidx.lifecycle:lifecycle-common-java8:2.2.0-alpha01' implementation "com.squareup.leakcanary:leakcanary-android:$leakcanary_version" diff --git a/testing/scenario_app/android/app/gradle.lockfile b/testing/scenario_app/android/app/gradle.lockfile index 613f074088401..305ad847baea5 100644 --- a/testing/scenario_app/android/app/gradle.lockfile +++ b/testing/scenario_app/android/app/gradle.lockfile @@ -41,6 +41,7 @@ androidx.test.espresso:espresso-idling-resource:3.2.0=androidTestImplementationD androidx.test:monitor:1.2.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAndroidTestRuntimeClasspath androidx.test:rules:1.2.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAndroidTestRuntimeClasspath androidx.test:runner:1.2.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAndroidTestRuntimeClasspath +androidx.tracing:tracing:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.transition:transition:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.vectordrawable:vectordrawable-animated:1.1.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.vectordrawable:vectordrawable:1.1.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m index b806e34541a6d..00892f9b830c8 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -91,7 +91,7 @@ - (FlutterEngine*)engineForTest:(NSString*)scenarioIdentifier { FlutterEngine* spawner = [[FlutterEngine alloc] initWithName:@"FlutterControllerTest" project:nil]; [spawner run]; - return [spawner spawnWithEntrypoint:nil libraryURI:nil initialRoute:nil]; + return [spawner spawnWithEntrypoint:nil libraryURI:nil initialRoute:nil entrypointArgs:nil]; } else { FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"FlutterControllerTest" project:nil]; diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.h b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.h index 80ad6c8fcc558..9438dd3774ea4 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.h +++ b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.h @@ -10,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN withCompletion:(nullable void (^)(void))engineRunCompletion; - (FlutterEngine*)spawnWithEntrypoint:(nullable NSString*)entrypoint libraryURI:(nullable NSString*)libraryURI - initialRoute:(nullable NSString*)initialRoute; + initialRoute:(nullable NSString*)initialRoute + entrypointArgs:(nullable NSArray*)entrypointArgs; @end NS_ASSUME_NONNULL_END diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m index 49677ee38f0a3..50f70f2a4635a 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m @@ -78,8 +78,8 @@ - (void)testSettingInitialRoute { [self waitForExpectationsWithTimeout:30.0 handler:nil]; - [binaryMessenger cleanupConnection:waitingForStatusConnection]; - [binaryMessenger cleanupConnection:initialRoutTestChannelConnection]; + [binaryMessenger cleanUpConnection:waitingForStatusConnection]; + [binaryMessenger cleanUpConnection:initialRoutTestChannelConnection]; } @end diff --git a/testing/test_gl_surface.h b/testing/test_gl_surface.h index ee339eb45347b..b5c7632b58e43 100644 --- a/testing/test_gl_surface.h +++ b/testing/test_gl_surface.h @@ -15,7 +15,7 @@ namespace testing { class TestGLSurface { public: - TestGLSurface(SkISize surface_size); + explicit TestGLSurface(SkISize surface_size); ~TestGLSurface(); diff --git a/testing/test_timeout_listener.h b/testing/test_timeout_listener.h index 37e2c23a2f40e..e6f466d92df2a 100644 --- a/testing/test_timeout_listener.h +++ b/testing/test_timeout_listener.h @@ -19,7 +19,7 @@ class PendingTests; class TestTimeoutListener : public ::testing::EmptyTestEventListener { public: - TestTimeoutListener(fml::TimeDelta timeout); + explicit TestTimeoutListener(fml::TimeDelta timeout); ~TestTimeoutListener(); diff --git a/testing/test_vulkan_context.cc b/testing/test_vulkan_context.cc new file mode 100644 index 0000000000000..ab83100982055 --- /dev/null +++ b/testing/test_vulkan_context.cc @@ -0,0 +1,52 @@ +// 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 "test_vulkan_context.h" + +#include "flutter/vulkan/vulkan_proc_table.h" + +#ifdef OS_MACOSX +#define VULKAN_SO_PATH "libvk_swiftshader.dylib" +#elif OS_WIN +#define VULKAN_SO_PATH "vk_swiftshader.dll" +#else +#define VULKAN_SO_PATH "libvk_swiftshader.so" +#endif + +namespace flutter { + +TestVulkanContext::TestVulkanContext() : valid_(false) { + vk_ = fml::MakeRefCounted(VULKAN_SO_PATH); + if (!vk_ || !vk_->HasAcquiredMandatoryProcAddresses()) { + FML_DLOG(ERROR) << "Proc table has not acquired mandatory proc addresses."; + return; + } + + application_ = std::unique_ptr( + new vulkan::VulkanApplication(*vk_, "Flutter Unittests", {})); + if (!application_->IsValid()) { + FML_DLOG(ERROR) << "Failed to initialize basic Vulkan state."; + return; + } + if (!vk_->AreInstanceProcsSetup()) { + FML_DLOG(ERROR) << "Failed to acquire full proc table."; + return; + } + + logical_device_ = application_->AcquireFirstCompatibleLogicalDevice(); + if (!logical_device_ || !logical_device_->IsValid()) { + FML_DLOG(ERROR) << "Failed to create compatible logical device."; + return; + } + + valid_ = true; +} + +TestVulkanContext::~TestVulkanContext() = default; + +bool TestVulkanContext::IsValid() { + return valid_; +} + +} // namespace flutter diff --git a/testing/test_vulkan_context.h b/testing/test_vulkan_context.h new file mode 100644 index 0000000000000..5d79971809dd3 --- /dev/null +++ b/testing/test_vulkan_context.h @@ -0,0 +1,33 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_TESTING_TEST_VULKAN_CONTEXT_H_ +#define FLUTTER_TESTING_TEST_VULKAN_CONTEXT_H_ + +#include "flutter/vulkan/vulkan_application.h" +#include "flutter/vulkan/vulkan_device.h" +#include "flutter/vulkan/vulkan_proc_table.h" + +namespace flutter { + +/// @brief Utility class to create a Vulkan device context, a corresponding +/// Skia context, and device resources. +class TestVulkanContext { + public: + TestVulkanContext(); + ~TestVulkanContext(); + bool IsValid(); + + private: + bool valid_ = false; + fml::RefPtr vk_; + std::unique_ptr application_; + std::unique_ptr logical_device_; + + FML_DISALLOW_COPY_AND_ASSIGN(TestVulkanContext); +}; + +} // namespace flutter + +#endif // FLUTTER_TESTING_TEST_VULKAN_CONTEXT_H_ diff --git a/testing/testing.gni b/testing/testing.gni index e457b0ffb7796..e0184d3a306ae 100644 --- a/testing/testing.gni +++ b/testing/testing.gni @@ -26,10 +26,16 @@ template("fixtures_location") { assert(defined(invoker.assets_dir), "The assets directory.") location_path = rebase_path(invoker.assets_dir) + testing_assets_path = rebase_path("$root_out_dir/gen/flutter/testing/assets") # Array of source lines. We use a list to ensure a trailing newline is # emitted by write_file() to comply with -Wnewline-eof. - location_source = [ "namespace flutter {namespace testing {const char* GetFixturesPath() {return \"$location_path\";}}}" ] + location_source = [ + "namespace flutter { namespace testing { ", + "const char* GetFixturesPath() {return \"$location_path\";} ", + "const char* GetTestingAssetsPath() {return \"$testing_assets_path\";} ", + "}}", + ] location_source_path = "$target_gen_dir/_fl_$target_name.cc" write_file(location_source_path, location_source) @@ -52,7 +58,7 @@ template("_frontend_server") { if (flutter_prebuilt_dart_sdk) { action(target_name) { testonly = invoker.testonly - deps = invoker.deps + [ "//flutter:dart_sdk" ] + deps = invoker.deps script = "//build/gn_run_binary.py" inputs = invoker.inputs outputs = invoker.outputs @@ -65,7 +71,7 @@ template("_frontend_server") { } dart = rebase_path("$host_prebuilt_dart_sdk/bin/dart$ext", root_out_dir) frontend_server = rebase_path( - "$root_out_dir/dart-sdk/bin/snapshots/frontend_server.dart.snapshot") + "$host_prebuilt_dart_sdk/bin/snapshots/frontend_server.dart.snapshot") args = [ dart, diff --git a/testing/testing.h b/testing/testing.h index 99a8e1bbe7e0d..386763a0b2717 100644 --- a/testing/testing.h +++ b/testing/testing.h @@ -27,6 +27,13 @@ namespace testing { /// const char* GetFixturesPath(); +//------------------------------------------------------------------------------ +/// @brief Returns the directory containing assets shared across all tests. +/// +/// @return The testing assets path. +/// +const char* GetTestingAssetsPath(); + //------------------------------------------------------------------------------ /// @brief Returns the default path to kernel_blob.bin. This file is within /// the directory returned by `GetFixturesPath()`. diff --git a/third_party/accessibility/BUILD.gn b/third_party/accessibility/BUILD.gn index efdc96c7d5b38..1295f7aed3412 100644 --- a/third_party/accessibility/BUILD.gn +++ b/third_party/accessibility/BUILD.gn @@ -7,11 +7,11 @@ import("//flutter/testing/testing.gni") config("accessibility_config") { visibility = [ - ":*", "//flutter/shell/platform/common:common_cpp_accessibility", + "//flutter/third_party/accessibility/*", ] if (is_win) { - # TODO: std::iterator class template is deprecated in C++17 + # TODO(cbracken): https://github.com/flutter/flutter/issues/92229 defines = [ "_SILENCE_CXX17_ITERATOR_BASE_CLASS_DEPRECATION_WARNING" ] } include_dirs = [ "//flutter/third_party/accessibility" ] diff --git a/third_party/accessibility/README.md b/third_party/accessibility/README.md index d8c51892fca3c..5b4b760949885 100644 --- a/third_party/accessibility/README.md +++ b/third_party/accessibility/README.md @@ -14,7 +14,7 @@ For the main ax code, the following parts were not imported: Update to this Library ============== -Bug fixes to the forked files in the the four directories should proceed as usual. +Bug fixes to the forked files in the four directories should proceed as usual. New features or changes that change the behaviors of these classes are discouraged. If you do need to make such change, please log the change at the end of this file. diff --git a/third_party/accessibility/ax/BUILD.gn b/third_party/accessibility/ax/BUILD.gn index 8f9f87a5f55f5..3c234b72344a7 100644 --- a/third_party/accessibility/ax/BUILD.gn +++ b/third_party/accessibility/ax/BUILD.gn @@ -6,7 +6,8 @@ import("//flutter/common/config.gni") source_set("ax") { visibility = [ "//flutter/third_party/accessibility/*" ] - include_dirs = [ "//flutter/third_party/accessibility" ] + public_configs = + [ "//flutter/third_party/accessibility:accessibility_config" ] sources = [ "platform/ax_platform_node.cc", diff --git a/third_party/tonic/dart_args.h b/third_party/tonic/dart_args.h index ec1f62358941c..cb9d0e3d273c2 100644 --- a/third_party/tonic/dart_args.h +++ b/third_party/tonic/dart_args.h @@ -16,7 +16,7 @@ namespace tonic { class DartArgIterator { public: - DartArgIterator(Dart_NativeArguments args, int start_index = 1) + explicit DartArgIterator(Dart_NativeArguments args, int start_index = 1) : args_(args), index_(start_index), had_exception_(false) {} template diff --git a/third_party/tonic/dart_state.h b/third_party/tonic/dart_state.h index 19dd9a5aee3c2..bc859fb69fb7b 100644 --- a/third_party/tonic/dart_state.h +++ b/third_party/tonic/dart_state.h @@ -38,8 +38,9 @@ class DartState : public std::enable_shared_from_this { DartApiScope api_scope_; }; - DartState(int dirfd = -1, - std::function message_epilogue = nullptr); + explicit DartState( + int dirfd = -1, + std::function message_epilogue = nullptr); virtual ~DartState(); static DartState* From(Dart_Isolate isolate); diff --git a/third_party/tonic/file_loader/file_loader.h b/third_party/tonic/file_loader/file_loader.h index 978d370fb8347..8b887416b9860 100644 --- a/third_party/tonic/file_loader/file_loader.h +++ b/third_party/tonic/file_loader/file_loader.h @@ -18,7 +18,7 @@ namespace tonic { class FileLoader { public: - FileLoader(int dirfd = -1); + explicit FileLoader(int dirfd = -1); ~FileLoader(); bool LoadPackagesMap(const std::string& packages); diff --git a/third_party/tonic/tests/dart_state_unittest.cc b/third_party/tonic/tests/dart_state_unittest.cc index dd0c15f26fcdc..8ce1dfa15af7a 100644 --- a/third_party/tonic/tests/dart_state_unittest.cc +++ b/third_party/tonic/tests/dart_state_unittest.cc @@ -36,10 +36,12 @@ TEST_F(DartState, IsShuttingDown) { vm_data->GetIsolateSnapshot(), // isolate snapshot nullptr, // platform configuration DartIsolate::Flags{}, // flags + nullptr, // root_isolate_create_callback settings.isolate_create_callback, // isolate create callback settings.isolate_shutdown_callback, // isolate shutdown callback "main", // dart entrypoint std::nullopt, // dart entrypoint library + {}, // dart entrypoint arguments std::move(isolate_configuration), // isolate configuration std::move(context) // engine context ); diff --git a/third_party/txt/benchmarks/paint_record_benchmarks.cc b/third_party/txt/benchmarks/paint_record_benchmarks.cc index c042dfcdcc951..52366031c9060 100644 --- a/third_party/txt/benchmarks/paint_record_benchmarks.cc +++ b/third_party/txt/benchmarks/paint_record_benchmarks.cc @@ -17,7 +17,7 @@ #include "flutter/fml/command_line.h" #include "flutter/fml/logging.h" #include "flutter/third_party/txt/tests/txt_test_utils.h" -#include "third_party/benchmark/include/benchmark/benchmark_api.h" +#include "third_party/benchmark/include/benchmark/benchmark.h" #include "txt/paint_record.h" #include "txt/text_style.h" diff --git a/third_party/txt/benchmarks/paragraph_benchmarks.cc b/third_party/txt/benchmarks/paragraph_benchmarks.cc index c0a6573dd739f..babd637093e04 100644 --- a/third_party/txt/benchmarks/paragraph_benchmarks.cc +++ b/third_party/txt/benchmarks/paragraph_benchmarks.cc @@ -22,7 +22,7 @@ #include "flutter/fml/logging.h" #include "flutter/third_party/txt/tests/txt_test_utils.h" #include "minikin/LayoutUtils.h" -#include "third_party/benchmark/include/benchmark/benchmark_api.h" +#include "third_party/benchmark/include/benchmark/benchmark.h" #include "third_party/icu/source/common/unicode/unistr.h" #include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/core/SkCanvas.h" diff --git a/third_party/txt/benchmarks/paragraph_builder_benchmarks.cc b/third_party/txt/benchmarks/paragraph_builder_benchmarks.cc index 7e378f365c6e1..b507db32e13b9 100644 --- a/third_party/txt/benchmarks/paragraph_builder_benchmarks.cc +++ b/third_party/txt/benchmarks/paragraph_builder_benchmarks.cc @@ -16,7 +16,7 @@ #include "flutter/fml/logging.h" #include "flutter/third_party/txt/tests/txt_test_utils.h" -#include "third_party/benchmark/include/benchmark/benchmark_api.h" +#include "third_party/benchmark/include/benchmark/benchmark.h" #include "third_party/icu/source/common/unicode/unistr.h" #include "third_party/skia/include/core/SkColor.h" #include "txt/font_collection.h" diff --git a/third_party/txt/benchmarks/skparagraph_benchmarks.cc b/third_party/txt/benchmarks/skparagraph_benchmarks.cc index 556478297b099..cb4775441cf57 100644 --- a/third_party/txt/benchmarks/skparagraph_benchmarks.cc +++ b/third_party/txt/benchmarks/skparagraph_benchmarks.cc @@ -19,7 +19,7 @@ #include "flutter/fml/command_line.h" #include "flutter/fml/logging.h" #include "flutter/third_party/txt/tests/txt_test_utils.h" -#include "third_party/benchmark/include/benchmark/benchmark_api.h" +#include "third_party/benchmark/include/benchmark/benchmark.h" #include "third_party/icu/source/common/unicode/unistr.h" #include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/core/SkCanvas.h" diff --git a/third_party/txt/benchmarks/txt_run_all_benchmarks.cc b/third_party/txt/benchmarks/txt_run_all_benchmarks.cc index 51f9a5902f2c7..6e7f2d8fb0c7b 100644 --- a/third_party/txt/benchmarks/txt_run_all_benchmarks.cc +++ b/third_party/txt/benchmarks/txt_run_all_benchmarks.cc @@ -18,7 +18,7 @@ #include "flutter/fml/logging.h" #include "flutter/testing/testing.h" #include "flutter/third_party/txt/tests/txt_test_utils.h" -#include "third_party/benchmark/include/benchmark/benchmark_api.h" +#include "third_party/benchmark/include/benchmark/benchmark.h" // We will use a custom main to allow custom font directories for consistency. int main(int argc, char** argv) { diff --git a/third_party/txt/src/minikin/FontFamily.h b/third_party/txt/src/minikin/FontFamily.h index aac7a0d62bd12..b82e80ea222da 100644 --- a/third_party/txt/src/minikin/FontFamily.h +++ b/third_party/txt/src/minikin/FontFamily.h @@ -41,7 +41,7 @@ class FontStyle { : FontStyle(0 /* variant */, 4 /* weight */, false /* italic */) {} FontStyle(int weight, bool italic) : FontStyle(0 /* variant */, weight, italic) {} - FontStyle(uint32_t langListId) // NOLINT(implicit) + FontStyle(uint32_t langListId) // NOLINT(google-explicit-constructor) : FontStyle(langListId, 0 /* variant */, 4 /* weight */, diff --git a/third_party/txt/src/minikin/Hyphenator.h b/third_party/txt/src/minikin/Hyphenator.h index 637c35e20f956..26b898ed24fa6 100644 --- a/third_party/txt/src/minikin/Hyphenator.h +++ b/third_party/txt/src/minikin/Hyphenator.h @@ -110,7 +110,8 @@ class HyphenEdit { static uint32_t editForNextLine(HyphenationType type); HyphenEdit() : hyphen(NO_EDIT) {} - HyphenEdit(uint32_t hyphenInt) : hyphen(hyphenInt) {} // NOLINT(implicit) + HyphenEdit(uint32_t hyphenInt) // NOLINT(google-explicit-constructor) + : hyphen(hyphenInt) {} uint32_t getHyphen() const { return hyphen; } bool operator==(const HyphenEdit& other) const { return hyphen == other.hyphen; diff --git a/third_party/txt/src/txt/asset_font_manager.h b/third_party/txt/src/txt/asset_font_manager.h index 68f9eca316c23..41648a0a497f3 100644 --- a/third_party/txt/src/txt/asset_font_manager.h +++ b/third_party/txt/src/txt/asset_font_manager.h @@ -29,7 +29,7 @@ namespace txt { class AssetFontManager : public SkFontMgr { public: - AssetFontManager(std::unique_ptr font_provider); + explicit AssetFontManager(std::unique_ptr font_provider); ~AssetFontManager() override; diff --git a/third_party/txt/src/txt/run_metrics.h b/third_party/txt/src/txt/run_metrics.h index 934bff3f10153..aa437932081ec 100644 --- a/third_party/txt/src/txt/run_metrics.h +++ b/third_party/txt/src/txt/run_metrics.h @@ -25,7 +25,7 @@ namespace txt { // Contains the font metrics and TextStyle of a unique run. class RunMetrics { public: - RunMetrics(const TextStyle* style) : text_style(style) {} + explicit RunMetrics(const TextStyle* style) : text_style(style) {} RunMetrics(const TextStyle* style, const SkFontMetrics& metrics) : text_style(style), font_metrics(metrics) {} diff --git a/tools/android_lint/project.xml b/tools/android_lint/project.xml index a5d3805d734ce..916da6b37a313 100644 --- a/tools/android_lint/project.xml +++ b/tools/android_lint/project.xml @@ -56,6 +56,7 @@ + diff --git a/tools/androidx/files.json b/tools/androidx/files.json index e8939614a32b1..f39538de3cc71 100644 --- a/tools/androidx/files.json +++ b/tools/androidx/files.json @@ -49,5 +49,21 @@ "androidx.annotation.UiThread", "androidx.annotation.VisibleForTesting" ] + }, + { + "url": "https://maven.google.com/androidx/tracing/tracing/1.0.0/tracing-1.0.0.aar", + "out_file_name": "androidx_tracing.aar", + "maven_dependency": "androidx.tracing:tracing:1.0.0", + "provides": [ + "androidx.tracing.Trace" + ] + }, + { + "url": "https://dl.google.com/android/maven2/androidx/core/core/1.6.0/core-1.6.0.aar", + "out_file_name": "androidx_core.aar", + "maven_dependency": "androidx.core:core:1.6.0", + "provides": [ + "androidx.core.view.WindowInsetsControllerCompat" + ] } ] diff --git a/tools/cipd/android_embedding_bundle/README.md b/tools/cipd/android_embedding_bundle/README.md index 28c538824ae22..01d5fb7551132 100644 --- a/tools/cipd/android_embedding_bundle/README.md +++ b/tools/cipd/android_embedding_bundle/README.md @@ -12,7 +12,7 @@ modify the dependencies in `build.gradle` and run `gradle updateDependencies`. Once you have updated the dependencies, you can upload a new version by running `cipd create --pkg-def cipd.yaml`. For more, see the Chromium instructions on ["Updating a CIPD -dependency"](https://chromium.googlesource.com/chromium/src/+/master/docs/cipd.md#Updating-a-CIPD-dependency) for how to upload a package update to CIPD. +dependency"](https://chromium.googlesource.com/chromium/src/+/main/docs/cipd_and_3pp.md#Updating-a-CIPD-dependency) for how to upload a package update to CIPD. On successful upload, you will receive a hash for the upload such as diff --git a/tools/cipd/android_embedding_bundle/build.gradle b/tools/cipd/android_embedding_bundle/build.gradle index 6c255757f9b96..4b1392cefbc43 100644 --- a/tools/cipd/android_embedding_bundle/build.gradle +++ b/tools/cipd/android_embedding_bundle/build.gradle @@ -41,7 +41,9 @@ android { dependencies { embedding "androidx.annotation:annotation:1.1.0" + embedding "androidx.core:core:1.6.0" embedding "androidx.fragment:fragment:1.1.0" + embedding "androidx.tracing:tracing:1.0.0" def lifecycle_version = "2.2.0" embedding "androidx.lifecycle:lifecycle-runtime:$lifecycle_version" @@ -53,17 +55,6 @@ android { // added to the user's app gradle in order to opt into using split AOT // dynamic features. embedding "com.google.android.play:core:1.8.0" - - // Testing - // TODO(xster): remove these android-all compile time dependencies. - // Use https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/plugins/LegacyDependencyResolver.java#L24 - // and specify them as runtime dependencies. - embeddingTesting "org.robolectric:android-all:9-robolectric-4913185-2" - embeddingTesting_v16 "org.robolectric:android-all:4.1.2_r1-robolectric-r1" - embeddingTesting "org.robolectric:robolectric:4.4" - embeddingTesting "junit:junit:4.13" - embeddingTesting "androidx.test:core:1.0.0" - embeddingTesting "org.mockito:mockito-core:3.11.2" } } diff --git a/tools/clang_tidy/README.md b/tools/clang_tidy/README.md index 2de22f4b56ce9..460ecbbfb7959 100644 --- a/tools/clang_tidy/README.md +++ b/tools/clang_tidy/README.md @@ -1,9 +1,15 @@ # clang_tidy -This is a Dart program/library that runs clang_tidy over modified files. It -takes two mandatory arguments that point at a compile_commands.json command -and the root of the Flutter engine repo: +This is a Dart program/library that runs clang_tidy over modified files in the Flutter engine repo. + +By default the linter runs on the repo files changed contained in `src/out/host_debug/compile_commands.json` command. +To check files other than in `host_debug` use `--target-variant android_debug_unopt`, +`--target-variant ios_debug_sim_unopt`, etc. + +Alternatively, use `--compile-commands` to specify a path to a `compile_commands.json` file. ``` -$ bin/main.dart --compile-commands --repo +$ bin/main.dart --target-variant +$ bin/main.dart --compile-commands +$ bin/main.dart --help ``` diff --git a/tools/clang_tidy/bin/main.dart b/tools/clang_tidy/bin/main.dart index 481f3b08ff3c7..4e3bd27ec9f6e 100644 --- a/tools/clang_tidy/bin/main.dart +++ b/tools/clang_tidy/bin/main.dart @@ -7,13 +7,19 @@ // Runs clang-tidy on files with changes. // // Basic Usage: -// dart bin/main.dart --compile-commands \ -// --repo \ +// dart bin/main.dart --compile-commands +// dart bin/main.dart --target-variant // // User environment variable FLUTTER_LINT_ALL to run on all files. +import 'dart:io' as io; + import 'package:clang_tidy/clang_tidy.dart'; Future main(List arguments) async { - return ClangTidy.fromCommandLine(arguments).run(); + final int result = await ClangTidy.fromCommandLine(arguments).run(); + if (result != 0) { + io.exit(result); + } + return result; } diff --git a/tools/clang_tidy/lib/clang_tidy.dart b/tools/clang_tidy/lib/clang_tidy.dart index d5bc169471305..77650d226d599 100644 --- a/tools/clang_tidy/lib/clang_tidy.dart +++ b/tools/clang_tidy/lib/clang_tidy.dart @@ -45,17 +45,17 @@ class ClangTidy { /// will otherwise go to stderr. ClangTidy({ required io.File buildCommandsPath, - required io.Directory repoPath, String checksArg = '', bool lintAll = false, + bool fix = false, StringSink? outSink, StringSink? errSink, }) : options = Options( buildCommandsPath: buildCommandsPath, - repoPath: repoPath, checksArg: checksArg, lintAll: lintAll, + fix: fix, errSink: errSink, ), _outSink = outSink ?? io.stdout, @@ -110,7 +110,7 @@ class ClangTidy { final List buildCommandsData = jsonDecode( options.buildCommandsPath.readAsStringSync(), ) as List; - final List changedFileBuildCommands = getLintCommandsForChangedFiles( + final List changedFileBuildCommands = await getLintCommandsForChangedFiles( buildCommandsData, changedFiles, ); @@ -165,26 +165,26 @@ class ClangTidy { /// Given a build commands json file, and the files with local changes, /// compute the lint commands to run. @visibleForTesting - List getLintCommandsForChangedFiles( + Future> getLintCommandsForChangedFiles( List buildCommandsData, List changedFiles, - ) { - final List buildCommands = [ - for (final dynamic c in buildCommandsData) - Command.fromMap(c as Map), - ]; - - return [ - for (final Command c in buildCommands) - if (c.containsAny(changedFiles)) - c, - ]; + ) async { + final List buildCommands = []; + for (final dynamic data in buildCommandsData) { + final Command command = Command.fromMap(data as Map); + final LintAction lintAction = await command.lintAction; + // Short-circuit the expensive containsAny call for the many third_party files. + if (lintAction != LintAction.skipThirdParty && command.containsAny(changedFiles)) { + buildCommands.add(command); + } + } + return buildCommands; } Future<_ComputeJobsResult> _computeJobs( List commands, io.Directory repoPath, - String checks, + String? checks, ) async { bool sawMalformed = false; final List jobs = []; @@ -207,11 +207,14 @@ class ClangTidy { break; case LintAction.lint: _outSink.writeln('🔶 linting $relativePath'); - jobs.add(command.createLintJob(checks)); + jobs.add(command.createLintJob(checks, options.fix)); break; case LintAction.skipThirdParty: _outSink.writeln('🔷 ignoring $relativePath (third_party)'); break; + case LintAction.skipMissing: + _outSink.writeln('🔷 ignoring $relativePath (missing)'); + break; } } return _ComputeJobsResult(jobs, sawMalformed); @@ -224,20 +227,10 @@ class ClangTidy { if (job.result.exitCode == 0) { continue; } - if (job.exception != null) { - _errSink.writeln( - '\n❗ A clang-tidy job failed to run, aborting:\n${job.exception}', - ); - result = 1; - break; - } else { - _errSink.writeln('❌ Failures for ${job.name}:'); - _errSink.writeln(job.result.stdout); - } + _errSink.writeln('❌ Failures for ${job.name}:'); + _errSink.writeln(job.result.stdout); result = 1; } return result; } } - - diff --git a/tools/clang_tidy/lib/src/command.dart b/tools/clang_tidy/lib/src/command.dart index 9fedf287682f9..1bc87d07be766 100644 --- a/tools/clang_tidy/lib/src/command.dart +++ b/tools/clang_tidy/lib/src/command.dart @@ -26,6 +26,9 @@ enum LintAction { /// Fail due to a malformed FLUTTER_NOLINT comment. failMalformedNoLint, + + /// Ignore because the file doesn't exist locally. + skipMissing, } /// A compilation command and methods to generate the lint command and job for @@ -86,20 +89,20 @@ class Command { r'//\s*FLUTTER_NOLINT(: https://github.com/flutter/flutter/issues/\d+)?', ); - LintAction? _lintAction; - /// The type of lint that is appropriate for this command. - Future get lintAction async => - _lintAction ??= await getLintAction(filePath); + late final Future lintAction = getLintAction(filePath); /// Determine the lint action for the file at `path`. @visibleForTesting - static Future getLintAction(String filePath) { + static Future getLintAction(String filePath) async { if (path.split(filePath).contains('third_party')) { - return Future.value(LintAction.skipThirdParty); + return LintAction.skipThirdParty; } final io.File file = io.File(filePath); + if (!file.existsSync()) { + return LintAction.skipMissing; + } final Stream lines = file.openRead() .transform(utf8.decoder) .transform(const LineSplitter()); @@ -126,8 +129,17 @@ class Command { } /// The job for the process runner for the lint needed for this command. - WorkerJob createLintJob(String checks) { - final List args = [filePath, checks, '--']; + WorkerJob createLintJob(String? checks, bool fix) { + final List args = [ + filePath, + if (checks != null) + checks, + if (fix) ...[ + '--fix', + '--format-style=file', + ], + '--', + ]; args.addAll(tidyArgs.split(' ')); return WorkerJob( [tidyPath, ...args], diff --git a/tools/clang_tidy/lib/src/git_repo.dart b/tools/clang_tidy/lib/src/git_repo.dart index 39621ab4fc7df..8367b736b3305 100644 --- a/tools/clang_tidy/lib/src/git_repo.dart +++ b/tools/clang_tidy/lib/src/git_repo.dart @@ -18,7 +18,7 @@ class GitRepo { List? _changedFiles; /// Returns a list of all non-deleted files which differ from the nearest - /// merge-base with `master`. If it can't find a fork point, uses the default + /// merge-base with `main`. If it can't find a fork point, uses the default /// merge-base. /// /// This is only computed once and cached. Subsequent invocations of the @@ -31,7 +31,7 @@ class GitRepo { defaultWorkingDirectory: root, ); final ProcessRunnerResult fetchResult = await processRunner.runProcess( - ['git', 'fetch', 'upstream', 'master'], + ['git', 'fetch', 'upstream', 'main'], failOk: true, ); if (fetchResult.exitCode != 0) { @@ -39,7 +39,7 @@ class GitRepo { 'git', 'fetch', 'origin', - 'master', + 'main', ]); } final Set result = {}; diff --git a/tools/clang_tidy/lib/src/options.dart b/tools/clang_tidy/lib/src/options.dart index 2af805d52f506..264fa37bdb6ff 100644 --- a/tools/clang_tidy/lib/src/options.dart +++ b/tools/clang_tidy/lib/src/options.dart @@ -5,6 +5,10 @@ import 'dart:io' as io show Directory, File, Platform, stderr; import 'package:args/args.dart'; +import 'package:path/path.dart' as path; + +// Path to root of the flutter/engine repository containing this script. +final String _engineRoot = path.dirname(path.dirname(path.dirname(path.dirname(path.fromUri(io.Platform.script))))); /// A class for organizing the options to the Engine linter, and the files /// that it operates on. @@ -12,17 +16,15 @@ class Options { /// Builds an instance of [Options] from the arguments. Options({ required this.buildCommandsPath, - required this.repoPath, this.help = false, this.verbose = false, this.checksArg = '', this.lintAll = false, + this.fix = false, this.errorMessage, StringSink? errSink, - }) { - checks = checksArg.isNotEmpty ? '--checks=$checksArg' : '--config='; - _errSink = errSink ?? io.stderr; - } + }) : checks = checksArg.isNotEmpty ? '--checks=$checksArg' : null, + _errSink = errSink ?? io.stderr; factory Options._error( String message, { @@ -31,7 +33,6 @@ class Options { return Options( errorMessage: message, buildCommandsPath: io.File('none'), - repoPath: io.Directory('none'), errSink: errSink, ); } @@ -42,7 +43,6 @@ class Options { return Options( help: true, buildCommandsPath: io.File('none'), - repoPath: io.Directory('none'), errSink: errSink, ); } @@ -50,16 +50,17 @@ class Options { /// Builds an [Options] instance with an [ArgResults] instance. factory Options._fromArgResults( ArgResults options, { + required io.File buildCommandsPath, StringSink? errSink, }) { return Options( help: options['help'] as bool, verbose: options['verbose'] as bool, - buildCommandsPath: io.File(options['compile-commands'] as String), - repoPath: io.Directory(options['repo'] as String), + buildCommandsPath: buildCommandsPath, checksArg: options.wasParsed('checks') ? options['checks'] as String : '', lintAll: io.Platform.environment['FLUTTER_LINT_ALL'] != null || options['lint-all'] as bool, + fix: options['fix'] as bool, errSink: errSink, ); } @@ -70,7 +71,17 @@ class Options { StringSink? errSink, }) { final ArgResults argResults = _argParser.parse(arguments); - final String? message = _checkArguments(argResults); + + String? buildCommandsPath = argResults['compile-commands'] as String?; + // path/to/engine/src/out/variant/compile_commands.json + buildCommandsPath ??= path.join( + argResults['src-dir'] as String, + 'out', + argResults['target-variant'] as String, + 'compile_commands.json', + ); + final io.File buildCommands = io.File(buildCommandsPath); + final String? message = _checkArguments(argResults, buildCommands); if (message != null) { return Options._error(message, errSink: errSink); } @@ -79,6 +90,7 @@ class Options { } return Options._fromArgResults( argResults, + buildCommandsPath: buildCommands, errSink: errSink, ); } @@ -86,26 +98,44 @@ class Options { static final ArgParser _argParser = ArgParser() ..addFlag( 'help', + abbr: 'h', help: 'Print help.', + negatable: false, ) ..addFlag( 'lint-all', help: 'lint all of the sources, regardless of FLUTTER_NOLINT.', defaultsTo: false, ) + ..addFlag( + 'fix', + help: 'Apply suggested fixes.', + defaultsTo: false, + ) ..addFlag( 'verbose', help: 'Print verbose output.', defaultsTo: false, ) - ..addOption( - 'repo', - help: 'Use the given path as the repo path', - ) ..addOption( 'compile-commands', help: 'Use the given path as the source of compile_commands.json. This ' - 'file is created by running tools/gn', + 'file is created by running "tools/gn". Cannot be used with --target-variant ' + 'or --src-dir.', + ) + ..addOption( + 'target-variant', + aliases: ['variant'], + help: 'The engine variant directory containing compile_commands.json ' + 'created by running "tools/gn". Cannot be used with --compile-commands.', + valueHelp: 'host_debug|android_debug_unopt|ios_debug|ios_debug_sim_unopt', + defaultsTo: 'host_debug', + ) + ..addOption( + 'src-dir', + help: 'Path to the engine src directory. Cannot be used with --compile-commands.', + valueHelp: 'path/to/engine/src', + defaultsTo: path.dirname(_engineRoot), ) ..addOption( 'checks', @@ -124,23 +154,26 @@ class Options { final io.File buildCommandsPath; /// The root of the flutter/engine repository. - final io.Directory repoPath; + final io.Directory repoPath = io.Directory(_engineRoot); /// Arguments to plumb through to clang-tidy formatted as a command line /// argument. final String checksArg; /// Check arguments to plumb through to clang-tidy. - late final String checks; + final String? checks; /// Whether all files should be linted. final bool lintAll; + /// Whether checks should apply available fix-ups to the working copy. + final bool fix; + /// If there was a problem with the command line arguments, this string /// contains the error message. final String? errorMessage; - late final StringSink _errSink; + final StringSink _errSink; /// Print command usage with an additional message. void printUsage({String? message}) { @@ -148,35 +181,30 @@ class Options { _errSink.writeln(message); } _errSink.writeln( - 'Usage: bin/main.dart [--help] [--lint-all] [--verbose] [--diff-branch]', + 'Usage: bin/main.dart [--help] [--lint-all] [--fix] [--verbose] [--diff-branch] [--target-variant variant] [--src-dir path/to/engine/src]', ); _errSink.writeln(_argParser.usage); } /// Command line argument validation. - static String? _checkArguments(ArgResults argResults) { + static String? _checkArguments(ArgResults argResults, io.File buildCommandsPath) { if (argResults.wasParsed('help')) { return null; } - if (!argResults.wasParsed('compile-commands')) { - return 'ERROR: The --compile-commands argument is required.'; + final bool compileCommandsParsed = argResults.wasParsed('compile-commands'); + if (compileCommandsParsed && argResults.wasParsed('target-variant')) { + return 'ERROR: --compile-commands option cannot be used with --target-variant.'; } - if (!argResults.wasParsed('repo')) { - return 'ERROR: The --repo argument is required.'; + if (compileCommandsParsed && argResults.wasParsed('src-dir')) { + return 'ERROR: --compile-commands option cannot be used with --src-dir.'; } - final io.File buildCommandsPath = io.File(argResults['compile-commands'] as String); if (!buildCommandsPath.existsSync()) { return "ERROR: Build commands path ${buildCommandsPath.absolute.path} doesn't exist."; } - final io.Directory repoPath = io.Directory(argResults['repo'] as String); - if (!repoPath.existsSync()) { - return "ERROR: Repo path ${repoPath.absolute.path} doesn't exist."; - } - return null; } } diff --git a/tools/clang_tidy/test/clang_tidy_test.dart b/tools/clang_tidy/test/clang_tidy_test.dart index 6dc15762bde93..76e0e7c775f13 100644 --- a/tools/clang_tidy/test/clang_tidy_test.dart +++ b/tools/clang_tidy/test/clang_tidy_test.dart @@ -2,21 +2,21 @@ // 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 show Directory, File, Platform, stderr; +import 'dart:io' as io show File, Platform, stderr; import 'package:clang_tidy/clang_tidy.dart'; import 'package:clang_tidy/src/command.dart'; import 'package:litetest/litetest.dart'; +import 'package:process_runner/process_runner.dart'; Future main(List args) async { - if (args.length < 2) { + if (args.isEmpty) { io.stderr.writeln( - 'Usage: clang_tidy_test.dart [build commands] [repo root]', + 'Usage: clang_tidy_test.dart [path/to/compile_commands.json]', ); return 1; } final String buildCommands = args[0]; - final String repoRoot = args[1]; test('--help gives help', () async { final StringBuffer outBuffer = StringBuffer(); @@ -36,11 +36,16 @@ Future main(List args) async { expect(errBuffer.toString(), contains('Usage: ')); }); - test('Error when --compile-commands is missing', () async { + test('Error when --compile-commands and --target-variant are used together', () async { final StringBuffer outBuffer = StringBuffer(); final StringBuffer errBuffer = StringBuffer(); final ClangTidy clangTidy = ClangTidy.fromCommandLine( - [], + [ + '--compile-commands', + '/unused', + '--target-variant', + 'unused' + ], outSink: outBuffer, errSink: errBuffer, ); @@ -50,17 +55,19 @@ Future main(List args) async { expect(clangTidy.options.help, isFalse); expect(result, equals(1)); expect(errBuffer.toString(), contains( - 'ERROR: The --compile-commands argument is required.', + 'ERROR: --compile-commands option cannot be used with --target-variant.', )); }); - test('Error when --repo is missing', () async { + test('Error when --compile-commands and --src-dir are used together', () async { final StringBuffer outBuffer = StringBuffer(); final StringBuffer errBuffer = StringBuffer(); final ClangTidy clangTidy = ClangTidy.fromCommandLine( [ '--compile-commands', '/unused', + '--src-dir', + '/unused', ], outSink: outBuffer, errSink: errBuffer, @@ -71,7 +78,7 @@ Future main(List args) async { expect(clangTidy.options.help, isFalse); expect(result, equals(1)); expect(errBuffer.toString(), contains( - 'ERROR: The --repo argument is required.', + 'ERROR: --compile-commands option cannot be used with --src-dir.', )); }); @@ -82,8 +89,6 @@ Future main(List args) async { [ '--compile-commands', '/does/not/exist', - '--repo', - '/unused', ], outSink: outBuffer, errSink: errBuffer, @@ -98,16 +103,15 @@ Future main(List args) async { )); }); - test('Error when --repo path does not exist', () async { + test('Error when --src-dir path does not exist, uses target variant in path', () async { final StringBuffer outBuffer = StringBuffer(); final StringBuffer errBuffer = StringBuffer(); final ClangTidy clangTidy = ClangTidy.fromCommandLine( [ - '--compile-commands', - // This just has to exist. - io.Platform.executable, - '--repo', + '--src-dir', '/does/not/exist', + '--target-variant', + 'ios_debug_unopt', ], outSink: outBuffer, errSink: errBuffer, @@ -118,7 +122,7 @@ Future main(List args) async { expect(clangTidy.options.help, isFalse); expect(result, equals(1)); expect(errBuffer.toString(), contains( - "ERROR: Repo path /does/not/exist doesn't exist.", + "ERROR: Build commands path /does/not/exist/out/ios_debug_unopt/compile_commands.json doesn't exist.", )); }); @@ -127,7 +131,6 @@ Future main(List args) async { final StringBuffer errBuffer = StringBuffer(); final ClangTidy clangTidy = ClangTidy( buildCommandsPath: io.File(buildCommands), - repoPath: io.Directory(repoRoot), lintAll: true, outSink: outBuffer, errSink: errBuffer, @@ -141,7 +144,6 @@ Future main(List args) async { final StringBuffer errBuffer = StringBuffer(); final ClangTidy clangTidy = ClangTidy( buildCommandsPath: io.File(buildCommands), - repoPath: io.Directory(repoRoot), outSink: outBuffer, errSink: errBuffer, ); @@ -154,7 +156,6 @@ Future main(List args) async { final StringBuffer errBuffer = StringBuffer(); final ClangTidy clangTidy = ClangTidy( buildCommandsPath: io.File(buildCommands), - repoPath: io.Directory(repoRoot), lintAll: true, outSink: outBuffer, errSink: errBuffer, @@ -167,7 +168,7 @@ Future main(List args) async { 'file': filePath, }, ]; - final List commands = clangTidy.getLintCommandsForChangedFiles( + final List commands = await clangTidy.getLintCommandsForChangedFiles( buildCommandsData, [], ); @@ -180,12 +181,13 @@ Future main(List args) async { final StringBuffer errBuffer = StringBuffer(); final ClangTidy clangTidy = ClangTidy( buildCommandsPath: io.File(buildCommands), - repoPath: io.Directory(repoRoot), lintAll: true, outSink: outBuffer, errSink: errBuffer, ); - const String filePath = '/path/to/a/source_file.cc'; + + // This file needs to exist, and be UTF8 line-parsable. + final String filePath = io.Platform.script.path; final List buildCommandsData = >[ { 'directory': '/unused', @@ -193,13 +195,33 @@ Future main(List args) async { 'file': filePath, }, ]; - final List commands = clangTidy.getLintCommandsForChangedFiles( + final List commands = await clangTidy.getLintCommandsForChangedFiles( buildCommandsData, [io.File(filePath)], ); expect(commands, isNotEmpty); - expect(commands.first.tidyPath, contains('clang/bin/clang-tidy')); + final Command command = commands.first; + expect(command.tidyPath, contains('clang/bin/clang-tidy')); + final WorkerJob jobNoFix = command.createLintJob(null, false); + expect(jobNoFix.command, [ + '../../buildtools/mac-x64/clang/bin/clang-tidy', + filePath, + '--', + '', + filePath, + ]); + + final WorkerJob jobWithFix = command.createLintJob(null, true); + expect(jobWithFix.command, [ + '../../buildtools/mac-x64/clang/bin/clang-tidy', + filePath, + '--fix', + '--format-style=file', + '--', + '', + filePath, + ]); }); test('Command getLintAction flags third_party files', () async { @@ -210,6 +232,14 @@ Future main(List args) async { expect(lintAction, equals(LintAction.skipThirdParty)); }); + test('Command getLintAction flags missing files', () async { + final LintAction lintAction = await Command.getLintAction( + '/does/not/exist', + ); + + expect(lintAction, equals(LintAction.skipMissing)); + }); + test('Command getLintActionFromContents flags FLUTTER_NOLINT', () async { final LintAction lintAction = await Command.lintActionFromContents( Stream.fromIterable([ diff --git a/tools/const_finder/test/fixtures/lib/target.dart b/tools/const_finder/test/fixtures/lib/target.dart index bdaee2d3615cc..c4a87d7f680a7 100644 --- a/tools/const_finder/test/fixtures/lib/target.dart +++ b/tools/const_finder/test/fixtures/lib/target.dart @@ -48,4 +48,4 @@ class MixedInTarget with MixableTarget { @override final String val; -} \ No newline at end of file +} diff --git a/tools/dia_dll.py b/tools/dia_dll.py index 010ed4f1eeb2c..cbfbf08bb0c97 100644 --- a/tools/dia_dll.py +++ b/tools/dia_dll.py @@ -5,7 +5,7 @@ # found in the LICENSE file. """ -This script is based on chromium/chromium/master/tools/clang/scripts/update.py. +This script is based on chromium/chromium/main/tools/clang/scripts/update.py. It is used on Windows platforms to copy the correct msdia*.dll to the clang folder, as a "gclient hook". @@ -19,8 +19,8 @@ # Path constants. (All of these should be absolute paths.) THIS_DIR = os.path.abspath(os.path.dirname(__file__)) -LLVM_BUILD_DIR = os.path.abspath(os.path.join(THIS_DIR, '..', '..', 'third_party', - 'llvm-build', 'Release+Asserts')) +LLVM_BUILD_DIR = os.path.abspath(os.path.join(THIS_DIR, '..', '..', 'buildtools', + 'windows-x64', 'clang')) def GetDiaDll(): diff --git a/tools/fuchsia/build_fuchsia_artifacts.py b/tools/fuchsia/build_fuchsia_artifacts.py index 861b6eef819d0..afac9dce45d46 100755 --- a/tools/fuchsia/build_fuchsia_artifacts.py +++ b/tools/fuchsia/build_fuchsia_artifacts.py @@ -35,7 +35,7 @@ def IsMac(): def GetFuchsiaSDKPath(): # host_os references the gn host_os - # https://gn.googlesource.com/gn/+/master/docs/reference.md#var_host_os + # https://gn.googlesource.com/gn/+/main/docs/reference.md#var_host_os host_os = '' if IsLinux(): host_os = 'linux' diff --git a/tools/fuchsia/dart/config.gni b/tools/fuchsia/dart/config.gni index 8b66a75892678..5b14c9ebfaa67 100644 --- a/tools/fuchsia/dart/config.gni +++ b/tools/fuchsia/dart/config.gni @@ -5,20 +5,19 @@ import("//flutter/tools/fuchsia/dart/dart_build_config.gni") declare_args() { - # Forces all Dart apps to build in product mode which is a - # stripped down version of the VM running in AOT mode. - dart_force_product = false - - # TODO(fxbug.dev/64153) renable aot builds - # if (dart_force_product) { - # Product AOT - # dart_default_build_cfg = dart_release_build_cfg - # } else if (is_debug) { + # TODO(fxbug.dev/86941) enable dart_runner_integration_tests + # TODO(fxbug.dev/64153) renable dart runner aot builds + # if (flutter_runtime_mode == "release") { + # # Product AOT + # dart_default_build_cfg = dart_release_build_cfg + # } else if (flutter_runtime_mode == "jit_release") { + # # Product JIT + # dart_default_build_cfg = dart_jit_release_build_cfg + # } else if (flutter_runtime_mode == "debug") { # Non-product JIT dart_default_build_cfg = dart_debug_build_cfg - - # } else { - # Non-product AOT - # dart_default_build_cfg = dart_profile_build_cfg - # } + # } else { # "profile" + # # Non-product AOT + # dart_default_build_cfg = dart_profile_build_cfg + # } } diff --git a/tools/fuchsia/dart/dart_build_config.gni b/tools/fuchsia/dart/dart_build_config.gni index 76e4cc702ed6e..d9678ba9bfc46 100644 --- a/tools/fuchsia/dart/dart_build_config.gni +++ b/tools/fuchsia/dart/dart_build_config.gni @@ -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/common/config.gni") + # Builds the component in a non-product JIT build. This will # launch the vm service in the runner. dart_debug_build_cfg = { @@ -13,42 +15,52 @@ dart_debug_build_cfg = { enable_asserts = true } -# TODO(richkadel): Don't confuse these settings with those in dart_build_config.gni, -# in fact, do I really need both? Can I drop flutter_build_config.gni? +# Builds the component in a non-product AOT build. This will +# launch the vm service in the runner. +# This configuration is not compatible with a --release build since the +# profile aot runner is built without asserts. +dart_aot_debug_build_cfg = { + runtime_meta = "//flutter/shell/platform/fuchsia/dart_runner/meta/aot_runtime" + runner_dep = "//flutter/shell/platform/fuchsia/dart_runner:dart_aot_runner" + platform_name = "dart_runner" + is_aot = true + is_product = false + enable_asserts = true +} -# # Builds the component in a non-product AOT build. This will -# # launch the vm service in the runner. -# # This configuration is not compatible with a --release build since the -# # profile aot runner is built without asserts. -# dart_aot_debug_build_cfg = { -# runtime_meta = "//flutter/shell/platform/fuchsia/dart_runner/meta/aot_runtime" -# runner_dep = "//flutter/shell/platform/fuchsia/dart_runner:dart_aot_runner" -# platform_name = "dart_runner" -# is_aot = true -# is_product = false -# enable_asserts = true -# } +# Builds the component in a non-product AOT build. This will +# launch the vm service in the runner. +dart_profile_build_cfg = { + runtime_meta = "//flutter/shell/platform/fuchsia/dart_runner/meta/aot_runtime" + runner_dep = "//flutter/shell/platform/fuchsia/dart_runner:dart_aot_runner" + platform_name = "dart_runner" + is_aot = true + is_product = false + enable_asserts = false +} -# # Builds the component in a non-product AOT build. This will -# # launch the vm service in the runner. -# dart_profile_build_cfg = { -# runtime_meta = "//flutter/shell/platform/fuchsia/dart_runner/meta/aot_runtime" -# runner_dep = "//flutter/shell/platform/fuchsia/dart_runner:dart_aot_runner" -# platform_name = "dart_runner" -# is_aot = true -# is_product = false -# enable_asserts = false -# } +# Builds the component in a product JIT build. This will +# not launch the vm service in the runner. +dart_jit_release_build_cfg = { + runtime_meta = + "//flutter/shell/platform/fuchsia/dart_runner/meta/jit_product_runtime" + runner_dep = + "//flutter/shell/platform/fuchsia/dart_runner:dart_jit_product_runner" + platform_name = "dart_runner" + is_aot = true + is_product = true + enable_asserts = false +} -# # Builds the component in a product AOT build. This will -# # not launch the vm service in the runner. -# dart_release_build_cfg = { -# runtime_meta = -# "//flutter/shell/platform/fuchsia/dart_runner/meta/aot_product_runtime" -# runner_dep = -# "//flutter/shell/platform/fuchsia/dart_runner:dart_aot_product_runner" -# platform_name = "dart_runner" -# is_aot = true -# is_product = true -# enable_asserts = false -# } +# Builds the component in a product AOT build. This will +# not launch the vm service in the runner. +dart_release_build_cfg = { + runtime_meta = + "//flutter/shell/platform/fuchsia/dart_runner/meta/aot_product_runtime" + runner_dep = + "//flutter/shell/platform/fuchsia/dart_runner:dart_aot_product_runner" + platform_name = "dart_runner" + is_aot = true + is_product = true + enable_asserts = false +} diff --git a/tools/fuchsia/dart/kernel/dart_kernel.gni b/tools/fuchsia/dart/kernel/dart_kernel.gni index ae58d1583b751..318546f79d1d9 100644 --- a/tools/fuchsia/dart/kernel/dart_kernel.gni +++ b/tools/fuchsia/dart/kernel/dart_kernel.gni @@ -157,10 +157,9 @@ template("dart_kernel") { _kernel_deps += invoker.deps } - # TODO(richkadel): The manifest is currently used by flutter_dart_component, to populate the file - # it calls `_convert_kernel_manifest_file`, so I can't make this conditional, right? Or is it - # possible to build Dart AOT Fuchsia packages/components without the data in this file (in which - # case I should make it conditional there as well)? + # TODO(richkadel): The manifest (if not using AOT) is used by + # flutter_dart_component, to populate the file it calls + # `_convert_kernel_manifest_file`. _generate_manifest = false if (invoker.is_aot) { not_needed(invoker, [ "generate_manifest" ]) @@ -291,17 +290,11 @@ template("dart_kernel") { ] } - # TODO(richkadel): NEED TO VALIDATE AND/OR CLEAN UP `is_debug` and `is_aot`. - # `is_debug` is set by the flutter `gn` script to true if --unoptimized, - # but `--unoptimized` is broken for Fuchsia, according to an open bug in - # github flutter/flutter issues. But I have a feeling `is_debug` from - # Fuchsia scripts may have a different meaning for dart/flutter at least. - # if (is_debug) { - args += [ "--embed-sources" ] - - # } else { - # args += [ "--no-embed-sources" ] - # } + if (flutter_runtime_mode == "debug") { + args += [ "--embed-sources" ] + } else { + args += [ "--no-embed-sources" ] + } if (invoker.is_aot) { args += [ @@ -320,12 +313,10 @@ template("dart_kernel") { # This define excludes debugging and profiling code from Flutter. args += [ "-Ddart.vm.product=true" ] } else { - # TODO(richkadel): I'm pretty sure we want to assume is_debug even if that's - # not what's set by gn flags (--unoptimized = false because true is currentlyh broken for Fuchsia?) - # if (!is_debug) { - # # The following define excludes debugging code from Flutter. - # args += [ "-Ddart.vm.profile=true" ] - # } + if (flutter_runtime_mode == "profile") { + # The following define excludes debugging code from Flutter. + args += [ "-Ddart.vm.profile=true" ] + } } if (defined(invoker.main_dart)) { diff --git a/tools/fuchsia/dart_kernel.gni b/tools/fuchsia/dart_kernel.gni index 97f098fbf67dd..598113eee02e7 100644 --- a/tools/fuchsia/dart_kernel.gni +++ b/tools/fuchsia/dart_kernel.gni @@ -49,7 +49,7 @@ template("dart_kernel") { "--output=" + rebase_path(output), ] - if (is_debug) { + if (flutter_runtime_mode == "debug") { args += [ "--embed-sources" ] } else { args += [ "--no-embed-sources" ] @@ -70,12 +70,12 @@ template("dart_kernel") { # build) causes the vm service isolate code to be tree-shaken from an app. # See the pragma on the entrypoint here: # - # https://github.com/dart-lang/sdk/blob/master/sdk/lib/_internal/vm/bin/vmservice_io.dart#L240 + # https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/vm/bin/vmservice_io.dart#L240 # # Also, this define excludes debugging and profiling code from Flutter. args += [ "-Ddart.vm.product=true" ] } else { - if (!is_debug) { + if (flutter_runtime_mode == "profile") { # The following define excludes debugging code from Flutter. args += [ "-Ddart.vm.profile=true" ] } diff --git a/tools/fuchsia/fidl/gen_response_file.py b/tools/fuchsia/fidl/gen_response_file.py index beeeae4878fc0..8be6ebea3295d 100755 --- a/tools/fuchsia/fidl/gen_response_file.py +++ b/tools/fuchsia/fidl/gen_response_file.py @@ -30,11 +30,11 @@ def main(): description="Generate response file for FIDL frontend") parser.add_argument( "--out-response-file", - help="The path for for the response file to generate", + help="The path for the response file to generate", required=True) parser.add_argument( "--out-libraries", - help="The path for for the libraries file to generate", + help="The path for the libraries file to generate", required=True) parser.add_argument( "--json", help="The path for the JSON file to generate, if any") diff --git a/tools/fuchsia/flutter/config.gni b/tools/fuchsia/flutter/config.gni index a8ebc4414f863..c1e4285e10a76 100644 --- a/tools/fuchsia/flutter/config.gni +++ b/tools/fuchsia/flutter/config.gni @@ -5,27 +5,17 @@ import("//flutter/tools/fuchsia/flutter/flutter_build_config.gni") declare_args() { - # If set to true, will force the runners to be built in - # product mode which means they will not have an exposed vm service - flutter_force_product = false -} - -declare_args() { - # TODO(richkadel): Make sure we're using 'debug' (non-aot) consistently, - # across dart and flutter rules. - # Since we're not REALLY building flutter apps, can I just git rid of this - # setting, and the configs in flutter/tools/fuchsia/flutter/flutter_build_config.gni? - # I think it's confusing to have both that one and .../dart/dart_build_config.gni. - - # if (flutter_force_product) { - # # Product AOT - # flutter_default_build_cfg = flutter_release_build_cfg - # } else if (is_debug) { - # Non-product JIT - flutter_default_build_cfg = flutter_debug_build_cfg - - # } else { - # # Non-product AOT - # flutter_default_build_cfg = flutter_profile_build_cfg - # } + if (flutter_runtime_mode == "release") { + # Product AOT + flutter_default_build_cfg = flutter_release_build_cfg + } else if (flutter_runtime_mode == "jit_release") { + # Product JIT + flutter_default_build_cfg = flutter_jit_release_build_cfg + } else if (flutter_runtime_mode == "debug") { + # Non-product JIT + flutter_default_build_cfg = flutter_debug_build_cfg + } else { # "profile" + # Non-product AOT + flutter_default_build_cfg = flutter_profile_build_cfg + } } diff --git a/tools/fuchsia/flutter/flutter_build_config.gni b/tools/fuchsia/flutter/flutter_build_config.gni index dd36c0a4dd56f..b7c033a46fc2a 100644 --- a/tools/fuchsia/flutter/flutter_build_config.gni +++ b/tools/fuchsia/flutter/flutter_build_config.gni @@ -2,8 +2,11 @@ # 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") + # Non-product JIT is "debug". It launches the vm service. # Non-product AOT is "profile". It also launches the vm service, but lacks tools that rely on JIT. +# Product JIT is "release". It doesn't launch the vm service. # Product AOT is "release". It doesn't launch the vm service. # Builds the component in a non-product JIT build. This will @@ -17,43 +20,54 @@ flutter_debug_build_cfg = { enable_asserts = true } -# TODO(richkadel): Don't confuse these settings with those in dart_build_config.gni, -# in fact, do I really need both? Can we just use the settings in dart_build_config.gni? +# Builds the component in a non-product AOT build. This will +# launch the vm service in the runner. +# This configuration is not compatible with a --release build since the +# profile aot runner is built without asserts. +flutter_aot_debug_build_cfg = { + runtime_meta = "//flutter/shell/platform/fuchsia/flutter/meta/aot_runtime" + runner_dep = "//flutter/shell/platform/fuchsia/flutter:flutter_aot_runner" + platform_name = "flutter_runner" + is_aot = true + is_product = false + enable_asserts = true +} -# # Builds the component in a non-product AOT build. This will -# # launch the vm service in the runner. -# # This configuration is not compatible with a --release build since the -# # profile aot runner is built without asserts. -# flutter_aot_debug_build_cfg = { -# runtime_meta = "//flutter/shell/platform/fuchsia/flutter/meta/aot_runtime" -# runner_dep = "//flutter/shell/platform/fuchsia/flutter:flutter_aot_runner" -# platform_name = "flutter_runner" -# is_aot = true -# is_product = false -# enable_asserts = true -# } +# Builds the component in a non-product AOT build. This will +# launch the vm service in the runner. +flutter_profile_build_cfg = { + runtime_meta = + "//flutter/shell/platform/fuchsia/flutter/meta/aot_runtime" # profile + # runner + runner_dep = "//flutter/shell/platform/fuchsia/flutter:flutter_aot_runner" + platform_name = "flutter_runner" + is_aot = true + is_product = false + enable_asserts = false +} -# # Builds the component in a non-product AOT build. This will -# # launch the vm service in the runner. -# flutter_profile_build_cfg = { -# runtime_meta = -# "//flutter/shell/platform/fuchsia/flutter/meta/aot_runtime" # profile -# # runner -# runner_dep = "//flutter/shell/platform/fuchsia/flutter:flutter_aot_runner" -# platform_name = "flutter_runner" -# is_aot = true -# is_product = false -# enable_asserts = false -# } +# Builds the component in a product JIT build. This will +# not launch the vm service in the runner. +flutter_jit_release_build_cfg = { + runtime_meta = + "//flutter/shell/platform/fuchsia/flutter/meta/jit_product_runtime" + runner_dep = + "//flutter/shell/platform/fuchsia/flutter:flutter_jit_product_runner" + platform_name = "flutter_runner" + is_aot = false + is_product = true + enable_asserts = false +} -# # Builds the component in a product AOT build. This will -# # not launch the vm service in the runner. -# flutter_release_build_cfg = { -# runtime_meta = -# "//flutter/shell/platform/fuchsia/flutter/meta/aot_product_runtime" -# runner_dep = "//flutter/shell/platform/fuchsia/flutter:flutter_aot_product_runner" -# platform_name = "flutter_runner" -# is_aot = true -# is_product = true -# enable_asserts = false -# } +# Builds the component in a product AOT build. This will +# not launch the vm service in the runner. +flutter_release_build_cfg = { + runtime_meta = + "//flutter/shell/platform/fuchsia/flutter/meta/aot_product_runtime" + runner_dep = + "//flutter/shell/platform/fuchsia/flutter:flutter_aot_product_runner" + platform_name = "flutter_runner" + is_aot = true + is_product = true + enable_asserts = false +} diff --git a/tools/fuchsia/flutter/flutter_component.gni b/tools/fuchsia/flutter/flutter_component.gni index 4be1f1a28926c..3e43e1634a82a 100644 --- a/tools/fuchsia/flutter/flutter_component.gni +++ b/tools/fuchsia/flutter/flutter_component.gni @@ -22,17 +22,18 @@ import("//flutter/tools/fuchsia/flutter/internal/flutter_dart_component.gni") # # Once a library is defined a flutter component can be created which # depends on this package. If the component needs any other resources they may -# be defined using the resource target and added to the components deps. +# be defined in the `resources` variable. # # ``` -# resource("text-file") { -# sources = [ "text_file.txt" ] -# outputs = [ "data/text_file.txt" ] -# } -# # flutter_component("my-component") { # manifest = "meta/my-component.cmx" # main_package = "my_library" +# resources = [ +# { +# path = "text_file.txt" +# dest = "data/text_file.txt" +# }, +# ] # deps = [ # ":lib", # ":text-file", @@ -133,6 +134,7 @@ template("flutter_component") { "main_dart", "main_package", "component_name", + "resources", ]) deps = _component_deps diff --git a/tools/fuchsia/flutter/internal/flutter_dart_component.gni b/tools/fuchsia/flutter/internal/flutter_dart_component.gni index 63f134cb9f00e..49529d23c0158 100644 --- a/tools/fuchsia/flutter/internal/flutter_dart_component.gni +++ b/tools/fuchsia/flutter/internal/flutter_dart_component.gni @@ -7,7 +7,6 @@ import("//flutter/tools/fuchsia/dart/config.gni") import("//flutter/tools/fuchsia/dart/dart.gni") import("//flutter/tools/fuchsia/dart/dart_package_config.gni") import("//flutter/tools/fuchsia/dart/kernel/dart_kernel.gni") -import("//flutter/tools/fuchsia/dist/resource.gni") import("//flutter/tools/fuchsia/gn-sdk/cmc.gni") import("//flutter/tools/fuchsia/gn-sdk/component.gni") @@ -71,6 +70,11 @@ template("flutter_dart_component") { _component_name = target_name } + _resources = [] + if (defined(invoker.resources)) { + _resources = invoker.resources + } + # Flutter and Dart components need to run inside the runner which matches how # they were compiled, for example, a JIT component must run in the JIT runner. # We need to be able to dynamically convert the manifest files to include the @@ -154,11 +158,6 @@ template("flutter_dart_component") { # the kernel will ignore this variable. generate_manifest = true - # TODO(richkadel): verify the above statement about aot is true (AOT builds, - # using these new scripts derived from Fuchsia, have not been tested yet); - # because as of now, the Fuchsia package/component appears to require data - # from `_convert_kernel_manifest`. - platform_name = build_cfg.platform_name product = build_cfg.is_product is_aot = build_cfg.is_aot @@ -230,27 +229,13 @@ template("flutter_dart_component") { # No asserts in debug or release product. # No asserts in non-product release # Yes asserts in non-product debug. - # if (is_debug && !dart_force_product) if (build_cfg.enable_asserts) { args += [ "--enable_asserts" ] } args += [ rebase_path(_kernel_path, root_build_dir) ] } - # copy the snapshot as a resource - _snapshot_resource_target_name = "${target_name}_snapshot_resource" - - # TODO(richkadel): It looks like this is the _ONLY_ place resource() is used - # and maybe that logic should simply be embedded here, directly? - resource(_snapshot_resource_target_name) { - sources = [ _snapshot_path ] - outputs = [ "data/${_component_name}/app_aot_snapshot.so" ] - } - - _component_deps += [ - ":$_snapshot_resource_target_name", - ":$_snapshot_target_name", - ] + _component_deps += [ ":$_snapshot_target_name" ] } fuchsia_component(_component_name) { @@ -263,17 +248,32 @@ template("flutter_dart_component") { manifest = _manifest manifest_output_name = _manifest_output_name - _convert_kernel_target_name = - "${_kernel_target_name}_convert_kernel_manifest" - _convert_kernel_manifest_file = - # TODO(richkadel): This is prefixed by "dartlang/", which is not found. - # Is the current toolchain set incorrectly somewhere? - string_replace( - "${target_gen_dir}/${_convert_kernel_target_name}_kernel_manifest.json", - "dartlang/", - "") - - resources_in_json_files = [ rebase_path(_convert_kernel_manifest_file) ] + if (build_cfg.is_aot) { + _resources += [ + { + path = _snapshot_path + dest = "data/${_component_name}/app_aot_snapshot.so" + }, + ] + } else { + _convert_kernel_target_name = + "${_kernel_target_name}_convert_kernel_manifest" + _convert_kernel_manifest_file = + # TODO(richkadel): This is prefixed by "dartlang/", which is not found. + # Is the current toolchain set incorrectly somewhere? + string_replace( + "${target_gen_dir}/${_convert_kernel_target_name}_kernel_manifest.json", + "dartlang/", + "") + + # TODO(richkadel): Adds the json resource names in the manifest to the + # collection of `json_of_resources` files, read by + # `prepare_package_inputs.py`. These resources are computed (only known) + # at some point during the build/compile phase. + resources_in_json_files = [ rebase_path(_convert_kernel_manifest_file) ] + } + + resources = _resources } group(target_name) { diff --git a/tools/fuchsia/fuchsia_libs.gni b/tools/fuchsia/fuchsia_libs.gni index aadd693dbf0dc..08f34a71e622f 100644 --- a/tools/fuchsia/fuchsia_libs.gni +++ b/tools/fuchsia/fuchsia_libs.gni @@ -9,6 +9,24 @@ fuchsia_sdk_dist = "$fuchsia_sdk_base/dist" sysroot_lib = "$fuchsia_sdk_base/sysroot/lib" sysroot_dist_lib = "$fuchsia_sdk_base/sysroot/dist/lib" +ld_so_path = "" +libcxx_so_2_path = "${clang_manifest_json.md5_19df03aecdc9eb27bc8b4038352f2b27}" +libcxx_abi_so_1_path = + "${clang_manifest_json.md5_6aff1b5f218d4a9278d85d63d0695af8}" +libunwind_so_1_path = + "${clang_manifest_json.md5_fb2bd871885ef42c2cf3138655f901ed}" +if (is_asan) { + ld_so_path = "asan/" + libcxx_so_2_path = + "${clang_manifest_json.md5_07cbd4a04b921d2ca61badc1b15a3b0b}" + libcxx_abi_so_1_path = + "${clang_manifest_json.md5_18f605e8fd52a92c5f9c6fde9be8206c}" + libunwind_so_1_path = + "${clang_manifest_json.md5_85b659ccd825be02f123e3479f1bc280}" + libclang_rt_so_path = + "${clang_manifest_json.md5_b76b69d6ab2c13408b7a3608f925b0c6}" +} + common_libs = [ { name = "libasync-default.so" @@ -40,27 +58,35 @@ common_libs = [ }, { name = "ld.so.1" - path = rebase_path("$sysroot_dist_lib") + path = rebase_path("$sysroot_dist_lib/$ld_so_path") }, +] - # Note, we use the md5 hashes here because of gn limitations of json parsing. - # This is a hack, and we can migrate to a better way soon. +# Note, for clang libs we use the md5 hashes here because of gn limitations of +# json parsing. +# This is a hack, and we can migrate to a better way soon. +common_libs += [ { name = "libc++.so.2" - path = rebase_path( - "$clang_base/${clang_manifest_json.md5_19df03aecdc9eb27bc8b4038352f2b27}") + path = rebase_path("$clang_base/$libcxx_so_2_path") }, { name = "libc++abi.so.1" - path = rebase_path( - "$clang_base/${clang_manifest_json.md5_6aff1b5f218d4a9278d85d63d0695af8}") + path = rebase_path("$clang_base/$libcxx_abi_so_1_path") }, { name = "libunwind.so.1" - path = rebase_path( - "$clang_base/${clang_manifest_json.md5_fb2bd871885ef42c2cf3138655f901ed}") + path = rebase_path("$clang_base/$libunwind_so_1_path") }, ] +if (is_asan) { + common_libs += [ + { + name = "libclang_rt.asan.so" + path = rebase_path("$clang_base/$libclang_rt_so_path") + }, + ] +} vulkan_dist = "$fuchsia_sdk_base/dist" diff --git a/tools/fuchsia/gn-sdk/component.gni b/tools/fuchsia/gn-sdk/component.gni index e4af4e316e2d9..6c06b45286ebc 100644 --- a/tools/fuchsia/gn-sdk/component.gni +++ b/tools/fuchsia/gn-sdk/component.gni @@ -33,8 +33,10 @@ declare_args() { # fuchsia_component("my_component") { # manifest = "meta/component-manifest.cmx" # resources = [ -# path = "testdata/use_case1.json" -# dest = "data/testdata/use_case1.json" +# { +# path = "testdata/use_case1.json" +# dest = "data/testdata/use_case1.json" +# }, # ] # data_deps = [ ":hello_world_executable"] # } diff --git a/tools/fuchsia/sdk/sdk_targets.gni b/tools/fuchsia/sdk/sdk_targets.gni index ffe037fc896ae..0067e0d4fecff 100644 --- a/tools/fuchsia/sdk/sdk_targets.gni +++ b/tools/fuchsia/sdk/sdk_targets.gni @@ -107,16 +107,19 @@ template("sdk_targets") { part_meta_rebased = "$fuchsia_sdk_path/$part_meta" part_meta_json = read_file(part_meta_rebased, "json") - subtarget_name = part_meta_json.name - if (part.type == target_type) { - if (target_type == "dart_library") { - _fuchsia_dart_library(subtarget_name) { - meta = part_meta_json - } - } else if (target_type == "fidl_library") { - _fuchsia_fidl_library(subtarget_name) { - meta = part_meta_json + if (part_meta != "version_history.json") { + subtarget_name = part_meta_json.name + + if (part.type == target_type) { + if (target_type == "dart_library") { + _fuchsia_dart_library(subtarget_name) { + meta = part_meta_json + } + } else if (target_type == "fidl_library") { + _fuchsia_fidl_library(subtarget_name) { + meta = part_meta_json + } } } } diff --git a/tools/gen_objcdoc.sh b/tools/gen_objcdoc.sh index cfef2b5265aba..dd0583b939231 100755 --- a/tools/gen_objcdoc.sh +++ b/tools/gen_objcdoc.sh @@ -46,7 +46,7 @@ jazzy \ --author Flutter Team\ --author_url 'https://flutter.io'\ --github_url 'https://github.com/flutter'\ - --github-file-prefix 'http://github.com/flutter/engine/blob/master'\ + --github-file-prefix 'http://github.com/flutter/engine/blob/main'\ --module-version 1.0.0\ --xcodebuild-arguments --objc,"$FLUTTER_UMBRELLA_HEADER",--,-x,objective-c,-isysroot,"$(xcrun --show-sdk-path --sdk iphonesimulator)",-I,"$(pwd)"\ --module Flutter\ @@ -60,6 +60,7 @@ FlutterCallbackInformation.html FlutterDartProject.html FlutterEngine.html FlutterEngineGroup.html +FlutterEngineGroupOptions.html FlutterError.html FlutterEventChannel.html FlutterHeadlessDartRunner.html diff --git a/tools/githooks/lib/src/pre_push_command.dart b/tools/githooks/lib/src/pre_push_command.dart index 428c10e8307be..e1e36999f7eb0 100644 --- a/tools/githooks/lib/src/pre_push_command.dart +++ b/tools/githooks/lib/src/pre_push_command.dart @@ -60,7 +60,6 @@ class PrePushCommand extends Command { final StringBuffer errBuffer = StringBuffer(); final ClangTidy clangTidy = ClangTidy( buildCommandsPath: compileCommands, - repoPath: io.Directory(flutterRoot), outSink: outBuffer, errSink: errBuffer, ); diff --git a/tools/gn b/tools/gn index 6e0946093e19f..e674f0b698939 100755 --- a/tools/gn +++ b/tools/gn @@ -88,15 +88,19 @@ def is_host_build(args): # target_os='linux' and linux-cpu='arm64' # TODO(fujino): make host platform explicit # https://github.com/flutter/flutter/issues/79403 - return args.target_os is None or args.target_os == 'linux' + return args.target_os is None or (args.target_os == 'linux' and args.linux_cpu == 'arm64') # Determines whether a prebuilt Dart SDK can be used instead of building one. # We can use a prebuilt Dart SDK when: -# 1. It is a host build, or a build targeting desktop +# 1. It is a host build, a build targeting Fuchsia, or a build targeting desktop. # 2. The prebuilt SDK exists under //flutter/prebuilts/$OS-$ARCH. def can_use_prebuilt_dart(args): prebuilt = None - if args.target_os == None: + # In a Fuchsia build, we can use a prebuilt Dart SDK for the host to build + # platform agnostic artifacts (e.g. kernel snapshots), and a Dart SDK + # targeting Fuchsia is not needed. So, it is safe to say that the prebuilt + # Dart SDK in a Fuchsia build is the host prebuilt Dart SDK. + if args.target_os == None or args.target_os == 'fuchsia': if sys.platform.startswith(('cygwin', 'win')): prebuilt = 'windows-x64' elif sys.platform == 'darwin': @@ -117,8 +121,8 @@ def can_use_prebuilt_dart(args): def to_gn_args(args): if args.simulator: - if args.target_os == 'android': - raise Exception('--simulator is not supported on Android') + if args.target_os != 'ios': + raise Exception('--simulator is only supported for iOS') runtime_mode = args.runtime_mode @@ -131,6 +135,8 @@ def to_gn_args(args): gn_args['bitcode_marker'] = True gn_args['enable_bitcode'] = args.bitcode + if args.enable_unittests: + gn_args['enable_unittests'] = args.enable_unittests # Skia GN args. gn_args['skia_enable_flutter_defines'] = True # Enable Flutter API guards in Skia. @@ -187,6 +193,9 @@ def to_gn_args(args): elif args.target_os == 'ios': gn_args['target_os'] = 'ios' gn_args['use_ios_simulator'] = args.simulator + elif args.target_os == 'mac': + gn_args['target_os'] = 'mac' + gn_args['use_ios_simulator'] = False elif args.target_os == 'fuchsia': gn_args['target_os'] = 'fuchsia' elif args.target_os == 'winuwp': @@ -303,6 +312,17 @@ def to_gn_args(args): gn_args['skia_use_metal'] = True gn_args['shell_enable_metal'] = True + # Enable Vulkan on all platforms except for Android and iOS. This is just + # to save on mobile binary size, as there's no reason the Vulkan embedder + # features can't work on these platforms. + if args.target_os not in ['android', 'ios']: + gn_args['skia_use_vulkan'] = True + gn_args['shell_enable_vulkan'] = True + # Disable VMA's use of std::shared_mutex in environments where the + # standard library doesn't support it. + if args.target_os == 'ios' or sys.platform.startswith(('cygwin', 'win')): + gn_args['skia_disable_vma_stl_shared_mutex'] = True + if sys.platform.startswith(('cygwin', 'win')): # The buildroot currently isn't set up to support Vulkan in the # Windows ANGLE build, so disable it regardless of enable_vulkan's value. @@ -392,6 +412,8 @@ def parse_args(args): parser.add_argument('--unoptimized', default=False, action='store_true') + parser.add_argument('--enable-unittests', action='store_true', default=False) + parser.add_argument('--runtime-mode', type=str, choices=['debug', 'profile', 'release', 'jit_release'], default='debug') parser.add_argument('--interpreter', default=False, action='store_true') parser.add_argument('--dart-debug', default=False, action='store_true', help='Enables assertions in the Dart VM. ' + diff --git a/tools/javadoc/android_reference/package-list b/tools/javadoc/android_reference/package-list new file mode 100644 index 0000000000000..319c00a19110c --- /dev/null +++ b/tools/javadoc/android_reference/package-list @@ -0,0 +1,238 @@ +android +android.accessibilityservice +android.accounts +android.animation +android.annotation +android.app +android.app.admin +android.app.appsearch +android.app.appsearch.exceptions +android.app.assist +android.app.backup +android.app.blob +android.app.job +android.app.people +android.app.role +android.app.slice +android.app.usage +android.appwidget +android.bluetooth +android.bluetooth.le +android.companion +android.content +android.content.pm +android.content.pm.verify.domain +android.content.res +android.content.res.loader +android.database +android.database.sqlite +android.drm +android.gesture +android.graphics +android.graphics.drawable +android.graphics.drawable.shapes +android.graphics.fonts +android.graphics.pdf +android.graphics.text +android.hardware +android.hardware.biometrics +android.hardware.camera2 +android.hardware.camera2.params +android.hardware.display +android.hardware.fingerprint +android.hardware.input +android.hardware.lights +android.hardware.usb +android.icu.lang +android.icu.math +android.icu.number +android.icu.text +android.icu.util +android.inputmethodservice +android.location +android.location.provider +android.media +android.media.audiofx +android.media.browse +android.media.effect +android.media.metrics +android.media.midi +android.media.projection +android.media.session +android.media.tv +android.mtp +android.net +android.net.eap +android.net.http +android.net.ipsec.ike +android.net.ipsec.ike.exceptions +android.net.nsd +android.net.rtp +android.net.sip +android.net.ssl +android.net.vcn +android.net.wifi +android.net.wifi.aware +android.net.wifi.hotspot2 +android.net.wifi.hotspot2.omadm +android.net.wifi.hotspot2.pps +android.net.wifi.p2p +android.net.wifi.p2p.nsd +android.net.wifi.rtt +android.nfc +android.nfc.cardemulation +android.nfc.tech +android.opengl +android.os +android.os.health +android.os.storage +android.os.strictmode +android.preference +android.print +android.print.pdf +android.printservice +android.provider +android.renderscript +android.sax +android.se.omapi +android.security +android.security.identity +android.security.keystore +android.service.autofill +android.service.carrier +android.service.chooser +android.service.controls +android.service.controls.actions +android.service.controls.templates +android.service.dreams +android.service.media +android.service.notification +android.service.quickaccesswallet +android.service.quicksettings +android.service.restrictions +android.service.textservice +android.service.voice +android.service.vr +android.service.wallpaper +android.speech +android.speech.tts +android.system +android.telecom +android.telephony +android.telephony.cdma +android.telephony.data +android.telephony.emergency +android.telephony.euicc +android.telephony.gsm +android.telephony.ims +android.telephony.ims.feature +android.telephony.mbms +android.test +android.test.mock +android.test.suitebuilder +android.test.suitebuilder.annotation +android.text +android.text.format +android.text.method +android.text.style +android.text.util +android.transition +android.util +android.util.proto +android.view +android.view.accessibility +android.view.animation +android.view.autofill +android.view.contentcapture +android.view.displayhash +android.view.inputmethod +android.view.inspector +android.view.textclassifier +android.view.textservice +android.view.translation +android.webkit +android.widget +android.widget.inline +android.window +dalvik.annotation +dalvik.bytecode +dalvik.system +java.awt.font +java.beans +java.io +java.lang +java.lang.annotation +java.lang.invoke +java.lang.ref +java.lang.reflect +java.math +java.net +java.nio +java.nio.channels +java.nio.channels.spi +java.nio.charset +java.nio.charset.spi +java.nio.file +java.nio.file.attribute +java.nio.file.spi +java.security +java.security.acl +java.security.cert +java.security.interfaces +java.security.spec +java.sql +java.text +java.time +java.time.chrono +java.time.format +java.time.temporal +java.time.zone +java.util +java.util.concurrent +java.util.concurrent.atomic +java.util.concurrent.locks +java.util.function +java.util.jar +java.util.logging +java.util.prefs +java.util.regex +java.util.stream +java.util.zip +javax.crypto +javax.crypto.interfaces +javax.crypto.spec +javax.microedition.khronos.egl +javax.microedition.khronos.opengles +javax.net +javax.net.ssl +javax.security.auth +javax.security.auth.callback +javax.security.auth.login +javax.security.auth.x500 +javax.security.cert +javax.sql +javax.xml +javax.xml.datatype +javax.xml.namespace +javax.xml.parsers +javax.xml.transform +javax.xml.transform.dom +javax.xml.transform.sax +javax.xml.transform.stream +javax.xml.validation +javax.xml.xpath +junit.framework +junit.runner +org.apache.http.conn +org.apache.http.conn.scheme +org.apache.http.conn.ssl +org.apache.http.params +org.json +org.w3c.dom +org.w3c.dom.ls +org.xml.sax +org.xml.sax.ext +org.xml.sax.helpers +org.xmlpull.v1 +org.xmlpull.v1.sax2 + diff --git a/tools/javadoc/gen_javadoc.py b/tools/javadoc/gen_javadoc.py new file mode 100755 index 0000000000000..b1ed22ceb06c4 --- /dev/null +++ b/tools/javadoc/gen_javadoc.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# +# 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 argparse +import os +import subprocess +import sys + +ANDROID_SRC_ROOT = 'flutter/shell/platform/android' + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) + +def JavadocBin(): + if sys.platform == 'darwin': + return os.path.join(SCRIPT_DIR, '..', '..', '..', 'third_party', 'java', 'openjdk', 'Contents', 'Home', 'bin', 'javadoc') + elif sys.platform.startswith(('cygwin', 'win')): + return os.path.join(SCRIPT_DIR, '..', '..', '..', 'third_party', 'java', 'openjdk', 'bin', 'javadoc.exe') + else : + return os.path.join(SCRIPT_DIR, '..', '..', '..', 'third_party', 'java', 'openjdk', 'bin', 'javadoc') + + +def main(): + parser = argparse.ArgumentParser(description='Runs javadoc on Flutter Android libraries') + parser.add_argument('--out-dir', type=str, required=True) + parser.add_argument('--android-source-root', type=str, default=ANDROID_SRC_ROOT) + parser.add_argument('--build-config-path', type=str) + parser.add_argument('--third-party', type=str, default='third_party') + parser.add_argument('--quiet', default=False, action='store_true') + args = parser.parse_args() + + if not os.path.exists(args.android_source_root): + print('This script must be run at the root of the Flutter source tree, or ' + 'the --android-source-root must be set.') + return 1 + + if not os.path.exists(args.out_dir): + os.makedirs(args.out_dir) + + classpath = [ + args.android_source_root, + os.path.join(args.third_party, 'android_tools/sdk/platforms/android-31/android.jar'), + os.path.join(args.third_party, 'android_embedding_dependencies', 'lib', '*'), + ] + if args.build_config_path: + classpath.append(args.build_config_path) + + packages = [ + 'io.flutter.app', + 'io.flutter.embedding.android', + 'io.flutter.embedding.engine', + 'io.flutter.embedding.engine.dart', + 'io.flutter.embedding.engine.loader', + 'io.flutter.embedding.engine.mutatorsstack', + 'io.flutter.embedding.engine.plugins', + 'io.flutter.embedding.engine.plugins.activity', + 'io.flutter.embedding.engine.plugins.broadcastreceiver', + 'io.flutter.embedding.engine.plugins.contentprovider', + 'io.flutter.embedding.engine.plugins.lifecycle', + 'io.flutter.embedding.engine.plugins.service', + 'io.flutter.embedding.engine.plugins.shim', + 'io.flutter.embedding.engine.renderer', + 'io.flutter.embedding.engine.systemchannels', + 'io.flutter.plugin.common', + 'io.flutter.plugin.editing', + 'io.flutter.plugin.platform', + 'io.flutter.util', + 'io.flutter.view', + ] + + android_package_list = os.path.join(SCRIPT_DIR, 'android_reference') + + command = [ + JavadocBin(), + '-classpath', ':'.join(classpath), + '-d', args.out_dir, + '-linkoffline', 'https://developer.android.com/reference/', android_package_list, + '-source', '1.8', + ] + packages + + if not args.quiet: + print(' '.join(command)) + + try: + output = subprocess.check_output(command, stderr=subprocess.STDOUT) + if not args.quiet: + print(output) + except subprocess.CalledProcessError as e: + print(e.output.decode('utf-8')) + return e.returncode + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/licenses/README.md b/tools/licenses/README.md index ccd44c0a82f75..d0d4c868ae189 100644 --- a/tools/licenses/README.md +++ b/tools/licenses/README.md @@ -20,7 +20,7 @@ We look at changes to the goldens to determine if there are any actual changes to the licenses. To update the goldens, make sure you've rebased your branch to the -latest upstream master and then run the following in this directory: +latest upstream main and then run the following in this directory: ``` pub get diff --git a/tools/pub_get_offline.py b/tools/pub_get_offline.py index f8a1d2f7b3242..c978e8ddac548 100644 --- a/tools/pub_get_offline.py +++ b/tools/pub_get_offline.py @@ -17,6 +17,7 @@ ALL_PACKAGES = [ os.path.join("src", "flutter", "ci"), os.path.join("src", "flutter", "flutter_frontend_server"), + os.path.join("src", "flutter", "shell", "vmservice"), os.path.join("src", "flutter", "testing", "benchmark"), os.path.join("src", "flutter", "testing", "dart"), os.path.join("src", "flutter", "testing", "litetest"), diff --git a/vulkan/vulkan_application.cc b/vulkan/vulkan_application.cc index 38560ac2ad08c..31dd54aaa1bf1 100644 --- a/vulkan/vulkan_application.cc +++ b/vulkan/vulkan_application.cc @@ -101,15 +101,16 @@ VulkanApplication::VulkanApplication( } // Now that we have an instance, set up instance proc table entries. - if (!vk.SetupInstanceProcAddresses(instance)) { + if (!vk.SetupInstanceProcAddresses(VulkanHandle(instance))) { FML_DLOG(INFO) << "Could not set up instance proc addresses."; return; } - instance_ = {instance, [this](VkInstance i) { - FML_DLOG(INFO) << "Destroying Vulkan instance"; - vk.DestroyInstance(i, nullptr); - }}; + instance_ = VulkanHandle{instance, [this](VkInstance i) { + FML_DLOG(INFO) + << "Destroying Vulkan instance"; + vk.DestroyInstance(i, nullptr); + }}; if (enable_instance_debugging) { auto debug_report = std::make_unique(vk, instance_); @@ -178,7 +179,8 @@ std::unique_ptr VulkanApplication::AcquireFirstCompatibleLogicalDevice() const { for (auto device_handle : GetPhysicalDevices()) { auto logical_device = std::make_unique( - vk, device_handle, enable_validation_layers_); + vk, VulkanHandle(device_handle), + enable_validation_layers_); if (logical_device->IsValid()) { return logical_device; } diff --git a/vulkan/vulkan_backbuffer.cc b/vulkan/vulkan_backbuffer.cc index d8be29e51d3db..b8b62aedbeb9f 100644 --- a/vulkan/vulkan_backbuffer.cc +++ b/vulkan/vulkan_backbuffer.cc @@ -65,7 +65,7 @@ bool VulkanBackbuffer::CreateSemaphores() { return false; } - semaphores_[i] = {semaphore, semaphore_collect}; + semaphores_[i] = VulkanHandle{semaphore, semaphore_collect}; } return true; @@ -90,7 +90,7 @@ bool VulkanBackbuffer::CreateFences() { return false; } - use_fences_[i] = {fence, fence_collect}; + use_fences_[i] = VulkanHandle{fence, fence_collect}; } return true; diff --git a/vulkan/vulkan_command_buffer.cc b/vulkan/vulkan_command_buffer.cc index c98a345fb3b75..8b767808b7ba0 100644 --- a/vulkan/vulkan_command_buffer.cc +++ b/vulkan/vulkan_command_buffer.cc @@ -33,7 +33,7 @@ VulkanCommandBuffer::VulkanCommandBuffer( vk.FreeCommandBuffers(device_, pool_, 1, &buffer); }; - handle_ = {buffer, buffer_collect}; + handle_ = VulkanHandle{buffer, buffer_collect}; valid_ = true; } diff --git a/vulkan/vulkan_debug_report.cc b/vulkan/vulkan_debug_report.cc index 1e5478753607b..5994363f67abe 100644 --- a/vulkan/vulkan_debug_report.cc +++ b/vulkan/vulkan_debug_report.cc @@ -4,6 +4,7 @@ #include "vulkan_debug_report.h" +#include #include #include @@ -208,9 +209,10 @@ VulkanDebugReport::VulkanDebugReport( return; } - handle_ = {handle, [this](VkDebugReportCallbackEXT handle) { - vk.DestroyDebugReportCallbackEXT(application_, handle, nullptr); - }}; + handle_ = VulkanHandle{ + handle, [this](VkDebugReportCallbackEXT handle) { + vk.DestroyDebugReportCallbackEXT(application_, handle, nullptr); + }}; valid_ = true; } diff --git a/vulkan/vulkan_device.cc b/vulkan/vulkan_device.cc index 4684a62b3ef1f..67191a2810c9a 100644 --- a/vulkan/vulkan_device.cc +++ b/vulkan/vulkan_device.cc @@ -103,8 +103,8 @@ VulkanDevice::VulkanDevice(VulkanProcTable& p_vk, return; } - device_ = {device, - [this](VkDevice device) { vk.DestroyDevice(device, nullptr); }}; + device_ = VulkanHandle{ + device, [this](VkDevice device) { vk.DestroyDevice(device, nullptr); }}; if (!vk.SetupDeviceProcAddresses(device_)) { FML_DLOG(INFO) << "Could not set up device proc addresses."; @@ -120,7 +120,7 @@ VulkanDevice::VulkanDevice(VulkanProcTable& p_vk, return; } - queue_ = queue; + queue_ = VulkanHandle(queue); const VkCommandPoolCreateInfo command_pool_create_info = { .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, @@ -137,9 +137,10 @@ VulkanDevice::VulkanDevice(VulkanProcTable& p_vk, return; } - command_pool_ = {command_pool, [this](VkCommandPool pool) { - vk.DestroyCommandPool(device_, pool, nullptr); - }}; + command_pool_ = VulkanHandle{ + command_pool, [this](VkCommandPool pool) { + vk.DestroyCommandPool(device_, pool, nullptr); + }}; valid_ = true; } diff --git a/vulkan/vulkan_handle.h b/vulkan/vulkan_handle.h index d011493d6f19c..eb53a063d9cf0 100644 --- a/vulkan/vulkan_handle.h +++ b/vulkan/vulkan_handle.h @@ -21,7 +21,7 @@ class VulkanHandle { VulkanHandle() : handle_(VK_NULL_HANDLE) {} - VulkanHandle(Handle handle, const Disposer& disposer = nullptr) + explicit VulkanHandle(Handle handle, const Disposer& disposer = nullptr) : handle_(handle), disposer_(disposer) {} VulkanHandle(VulkanHandle&& other) @@ -46,8 +46,9 @@ class VulkanHandle { return *this; } - operator bool() const { return handle_ != VK_NULL_HANDLE; } + explicit operator bool() const { return handle_ != VK_NULL_HANDLE; } + // NOLINTNEXTLINE(google-explicit-constructor) operator Handle() const { return handle_; } /// Relinquish responsibility of collecting the underlying handle when this diff --git a/vulkan/vulkan_image.cc b/vulkan/vulkan_image.cc index 7f3b5eb9cdde4..ccd69fee9c181 100644 --- a/vulkan/vulkan_image.cc +++ b/vulkan/vulkan_image.cc @@ -31,7 +31,7 @@ bool VulkanImage::InsertImageMemoryBarrier( const VulkanCommandBuffer& command_buffer, VkPipelineStageFlagBits src_pipline_bits, VkPipelineStageFlagBits dest_pipline_bits, - VkAccessFlagBits dest_access_flags, + VkAccessFlags dest_access_flags, VkImageLayout dest_layout) { const VkImageMemoryBarrier image_memory_barrier = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, diff --git a/vulkan/vulkan_image.h b/vulkan/vulkan_image.h index 9402f0eade535..cdab2faeaca06 100644 --- a/vulkan/vulkan_image.h +++ b/vulkan/vulkan_image.h @@ -16,7 +16,7 @@ class VulkanCommandBuffer; class VulkanImage { public: - VulkanImage(VulkanHandle image); + explicit VulkanImage(VulkanHandle image); ~VulkanImage(); @@ -26,7 +26,7 @@ class VulkanImage { const VulkanCommandBuffer& command_buffer, VkPipelineStageFlagBits src_pipline_bits, VkPipelineStageFlagBits dest_pipline_bits, - VkAccessFlagBits dest_access_flags, + VkAccessFlags dest_access_flags, VkImageLayout dest_layout); private: diff --git a/vulkan/vulkan_proc_table.cc b/vulkan/vulkan_proc_table.cc index 513a9dba7d927..775da412c9887 100644 --- a/vulkan/vulkan_proc_table.cc +++ b/vulkan/vulkan_proc_table.cc @@ -4,8 +4,6 @@ #include "vulkan_proc_table.h" -#include - #include "flutter/fml/logging.h" #define ACQUIRE_PROC(name, context) \ @@ -16,10 +14,12 @@ namespace vulkan { -VulkanProcTable::VulkanProcTable() +VulkanProcTable::VulkanProcTable() : VulkanProcTable("libvulkan.so"){}; + +VulkanProcTable::VulkanProcTable(const char* path) : handle_(nullptr), acquired_mandatory_proc_addresses_(false) { acquired_mandatory_proc_addresses_ = - OpenLibraryHandle() && SetupLoaderProcAddresses(); + OpenLibraryHandle(path) && SetupLoaderProcAddresses(); } VulkanProcTable::~VulkanProcTable() { @@ -43,7 +43,7 @@ bool VulkanProcTable::AreDeviceProcsSetup() const { } bool VulkanProcTable::SetupLoaderProcAddresses() { - if (handle_ == nullptr) { + if (!handle_) { return true; } @@ -51,8 +51,8 @@ bool VulkanProcTable::SetupLoaderProcAddresses() { #if VULKAN_LINK_STATICALLY GetInstanceProcAddr = &vkGetInstanceProcAddr; #else // VULKAN_LINK_STATICALLY - reinterpret_cast( - dlsym(handle_, "vkGetInstanceProcAddr")); + reinterpret_cast(const_cast( + handle_->ResolveSymbol("vkGetInstanceProcAddr"))); #endif // VULKAN_LINK_STATICALLY if (!GetInstanceProcAddr) { @@ -99,7 +99,7 @@ bool VulkanProcTable::SetupInstanceProcAddresses( return true; }(); - instance_ = {handle, nullptr}; + instance_ = VulkanHandle{handle, nullptr}; return true; } @@ -129,6 +129,7 @@ bool VulkanProcTable::SetupDeviceProcAddresses( ACQUIRE_PROC(ResetCommandBuffer, handle); ACQUIRE_PROC(ResetFences, handle); ACQUIRE_PROC(WaitForFences, handle); +#ifndef TEST_VULKAN_PROCS #if OS_ANDROID ACQUIRE_PROC(AcquireNextImageKHR, handle); ACQUIRE_PROC(CreateSwapchainKHR, handle); @@ -145,42 +146,23 @@ bool VulkanProcTable::SetupDeviceProcAddresses( ACQUIRE_PROC(SetBufferCollectionConstraintsFUCHSIAX, handle); ACQUIRE_PROC(GetBufferCollectionPropertiesFUCHSIAX, handle); #endif // OS_FUCHSIA - device_ = {handle, nullptr}; +#endif // TEST_VULKAN_PROCS + device_ = VulkanHandle{handle, nullptr}; return true; } -bool VulkanProcTable::OpenLibraryHandle() { +bool VulkanProcTable::OpenLibraryHandle(const char* path) { #if VULKAN_LINK_STATICALLY - static char kDummyLibraryHandle = '\0'; - handle_ = reinterpret_cast(&kDummyLibraryHandle); - return true; + handle_ = fml::NativeLibrary::CreateForCurrentProcess(); #else // VULKAN_LINK_STATICALLY - dlerror(); // clear existing errors on thread. - handle_ = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL); - if (handle_ == nullptr) { - FML_DLOG(WARNING) << "Could not open the vulkan library: " << dlerror(); - return false; - } - return true; + handle_ = fml::NativeLibrary::Create(path); #endif // VULKAN_LINK_STATICALLY + return !!handle_; } bool VulkanProcTable::CloseLibraryHandle() { -#if VULKAN_LINK_STATICALLY handle_ = nullptr; return true; -#else - if (handle_ != nullptr) { - dlerror(); // clear existing errors on thread. - if (dlclose(handle_) != 0) { - FML_DLOG(ERROR) << "Could not close the vulkan library handle. This " - "indicates a leak."; - FML_DLOG(ERROR) << dlerror(); - } - handle_ = nullptr; - } - return handle_ == nullptr; -#endif } PFN_vkVoidFunction VulkanProcTable::AcquireProc( @@ -211,13 +193,14 @@ GrVkGetProc VulkanProcTable::CreateSkiaGetProc() const { return [this](const char* proc_name, VkInstance instance, VkDevice device) { if (device != VK_NULL_HANDLE) { - auto result = AcquireProc(proc_name, {device, nullptr}); + auto result = + AcquireProc(proc_name, VulkanHandle{device, nullptr}); if (result != nullptr) { return result; } } - return AcquireProc(proc_name, {instance, nullptr}); + return AcquireProc(proc_name, VulkanHandle{instance, nullptr}); }; } diff --git a/vulkan/vulkan_proc_table.h b/vulkan/vulkan_proc_table.h index 291ad0f7517f6..7ecc348d366ee 100644 --- a/vulkan/vulkan_proc_table.h +++ b/vulkan/vulkan_proc_table.h @@ -8,6 +8,7 @@ #include "flutter/fml/macros.h" #include "flutter/fml/memory/ref_counted.h" #include "flutter/fml/memory/ref_ptr.h" +#include "flutter/fml/native_library.h" #include "third_party/skia/include/core/SkRefCnt.h" #include "third_party/skia/include/gpu/vk/GrVkBackendContext.h" #include "vulkan_handle.h" @@ -25,7 +26,7 @@ class VulkanProcTable : public fml::RefCountedThreadSafe { public: using Proto = T; - Proc(T proc = nullptr) : proc_(proc) {} + explicit Proc(T proc = nullptr) : proc_(proc) {} ~Proc() { proc_ = nullptr; } @@ -39,9 +40,9 @@ class VulkanProcTable : public fml::RefCountedThreadSafe { return *this; } - operator bool() const { return proc_ != nullptr; } + explicit operator bool() const { return proc_ != nullptr; } - operator T() const { return proc_; } + operator T() const { return proc_; } // NOLINT(google-explicit-constructor) private: T proc_; @@ -105,6 +106,7 @@ class VulkanProcTable : public fml::RefCountedThreadSafe { DEFINE_PROC(ResetCommandBuffer); DEFINE_PROC(ResetFences); DEFINE_PROC(WaitForFences); +#ifndef TEST_VULKAN_PROCS #if OS_ANDROID DEFINE_PROC(GetPhysicalDeviceSurfaceCapabilitiesKHR); DEFINE_PROC(GetPhysicalDeviceSurfaceFormatsKHR); @@ -123,18 +125,20 @@ class VulkanProcTable : public fml::RefCountedThreadSafe { DEFINE_PROC(SetBufferCollectionConstraintsFUCHSIAX); DEFINE_PROC(GetBufferCollectionPropertiesFUCHSIAX); #endif // OS_FUCHSIA +#endif // TEST_VULKAN_PROCS #undef DEFINE_PROC private: - void* handle_; + fml::RefPtr handle_; bool acquired_mandatory_proc_addresses_; VulkanHandle instance_; VulkanHandle device_; VulkanProcTable(); + explicit VulkanProcTable(const char* path); ~VulkanProcTable(); - bool OpenLibraryHandle(); + bool OpenLibraryHandle(const char* path); bool SetupLoaderProcAddresses(); bool CloseLibraryHandle(); PFN_vkVoidFunction AcquireProc( diff --git a/vulkan/vulkan_provider.h b/vulkan/vulkan_provider.h index 6dd8ff109d675..e32e99b917446 100644 --- a/vulkan/vulkan_provider.h +++ b/vulkan/vulkan_provider.h @@ -25,9 +25,10 @@ class VulkanProvider { &fence)) != VK_SUCCESS) return vulkan::VulkanHandle(); - return {fence, [this](VkFence fence) { - vk().DestroyFence(vk_device(), fence, nullptr); - }}; + return VulkanHandle{fence, [this](VkFence fence) { + vk().DestroyFence(vk_device(), fence, + nullptr); + }}; } }; diff --git a/vulkan/vulkan_surface.cc b/vulkan/vulkan_surface.cc index f410d43f81719..31e460344c070 100644 --- a/vulkan/vulkan_surface.cc +++ b/vulkan/vulkan_surface.cc @@ -30,10 +30,10 @@ VulkanSurface::VulkanSurface( return; } - surface_ = {surface, [this](VkSurfaceKHR surface) { - vk.DestroySurfaceKHR(application_.GetInstance(), surface, - nullptr); - }}; + surface_ = VulkanHandle{ + surface, [this](VkSurfaceKHR surface) { + vk.DestroySurfaceKHR(application_.GetInstance(), surface, nullptr); + }}; valid_ = true; } diff --git a/web_sdk/web_test_utils/lib/environment.dart b/web_sdk/web_test_utils/lib/environment.dart index 1ba54b6b4745a..3b0772cd5f6a8 100644 --- a/web_sdk/web_test_utils/lib/environment.dart +++ b/web_sdk/web_test_utils/lib/environment.dart @@ -111,10 +111,6 @@ class Environment { /// The "pub" executable file. String get pubExecutable => pathlib.join(dartSdkDir.path, 'bin', 'pub'); - /// The "dart2js" executable file. - String get dart2jsExecutable => - pathlib.join(dartSdkDir.path, 'bin', 'dart2js'); - /// Path to where github.com/flutter/engine is checked out inside the engine workspace. io.Directory get flutterDirectory => io.Directory(pathlib.join(engineSrcDir.path, 'flutter')); @@ -180,6 +176,12 @@ class Environment { 'goldens', )); + /// Path to the base directory to be used by Skia Gold. + io.Directory get webUiSkiaGoldDirectory => io.Directory(pathlib.join( + webUiDartToolDir.path, + 'skia_gold', + )); + /// Directory to add test results which would later be uploaded to a gcs /// bucket by LUCI. io.Directory get webUiTestResultsDirectory => io.Directory(pathlib.join( diff --git a/web_sdk/web_test_utils/lib/goldens.dart b/web_sdk/web_test_utils/lib/goldens.dart index f67efc880e07d..47f9f6cd30a18 100644 --- a/web_sdk/web_test_utils/lib/goldens.dart +++ b/web_sdk/web_test_utils/lib/goldens.dart @@ -4,7 +4,6 @@ import 'dart:io' as io; import 'package:image/image.dart'; -import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:yaml/yaml.dart'; @@ -104,7 +103,7 @@ class ImageDiff { } static int _average(Iterable values) { - return values.reduce((a, b) => a + b) ~/ values.length; + return values.reduce((int a, int b) => a + b) ~/ values.length; } /// The value of the pixel at [x] and [y] coordinates. @@ -129,9 +128,9 @@ class ImageDiff { _reflectedPixel(image, x + 1, y + 1), ]; return [ - _average(pixels.map((p) => getRed(p))), - _average(pixels.map((p) => getGreen(p))), - _average(pixels.map((p) => getBlue(p))), + _average(pixels.map((int p) => getRed(p))), + _average(pixels.map((int p) => getGreen(p))), + _average(pixels.map((int p) => getBlue(p))), ]; case PixelComparison.precise: final int pixel = image.getPixel(x, y); @@ -141,7 +140,7 @@ class ImageDiff { getBlue(pixel), ]; default: - throw 'Unrecognized pixel comparison value: ${pixelComparison}'; + throw 'Unrecognized pixel comparison value: $pixelComparison'; } } @@ -188,7 +187,7 @@ class ImageDiff { /// Returns text explaining pixel difference rate. String getPrintableDiffFilesInfo(double diffRate, double maxRate) => - '(${((diffRate) * 100).toStringAsFixed(4)}% of pixels were different. ' + '(${(diffRate * 100).toStringAsFixed(4)}% of pixels were different. ' 'Maximum allowed rate is: ${(maxRate * 100).toStringAsFixed(4)}%).'; /// Downloads the repository that stores the golden files. @@ -234,7 +233,7 @@ class GoldensRepoFetcher { } await _runGit( - ['fetch', 'origin', 'master'], + ['fetch', 'origin', 'main'], _webUiGoldensRepositoryDirectory.path, ); await _runGit( diff --git a/web_sdk/web_test_utils/lib/image_compare.dart b/web_sdk/web_test_utils/lib/image_compare.dart index 7c92bc7870173..7952930c95658 100644 --- a/web_sdk/web_test_utils/lib/image_compare.dart +++ b/web_sdk/web_test_utils/lib/image_compare.dart @@ -9,8 +9,15 @@ import 'package:path/path.dart' as p; import 'environment.dart'; import 'goldens.dart'; +import 'skia_client.dart'; -/// Compares a screenshot taken through a test with it's golden. +/// Whether this code is running on LUCI. +bool _isLuci = Platform.environment.containsKey('SWARMING_TASK_ID') && Platform.environment.containsKey('GOLDCTL'); +bool _isPreSubmit = _isLuci && Platform.environment.containsKey('GOLD_TRYJOB'); +bool _isPostSubmit = _isLuci && !_isPreSubmit; + + +/// Compares a screenshot taken through a test with its golden. /// /// Used by Flutter Web Engine unit tests and the integration tests. /// @@ -18,15 +25,29 @@ import 'goldens.dart'; /// is simply `OK`, however when they fail it contains a detailed explanation /// on which files are compared, their absolute locations and an HTML page /// that the developer can see the comparison. -String compareImage( +Future compareImage( Image screenshot, bool doUpdateScreenshotGoldens, String filename, PixelComparison pixelComparison, - double maxDiffRateFailure, { + double maxDiffRateFailure, + SkiaGoldClient? skiaClient, { + // TODO(mdebbar): Remove these args with goldens repo. String goldensDirectory = '', + String filenameSuffix = '', bool write = false, -}) { +}) async { + if (_isLuci && skiaClient != null) { + // This is temporary to get started by uploading existing screenshots to + // Skia Gold. The next step would be to actually use Skia Gold for + // comparison. + + // TODO(mdebbar): Use Skia Gold for comparison, not only for uploading. + await _uploadToSkiaGold(skiaClient, screenshot, filename); + } + + filename = filename.replaceAll('.png', '$filenameSuffix.png'); + final Environment environment = Environment(); if (goldensDirectory.isEmpty) { goldensDirectory = p.join( @@ -137,3 +158,42 @@ Golden file $filename did not match the image generated by the test. } return 'OK'; } + +Future _uploadToSkiaGold( + SkiaGoldClient skiaClient, + Image screenshot, + String filename, +) async { + // Can't upload to Gold Skia unless running in LUCI. + assert(_isLuci); + + // Write the screenshot to the file system so it can be consumed by the + // `goldctl` tool. + final File goldenFile = File(p.join(environment.webUiSkiaGoldDirectory.path, filename)); + await goldenFile.writeAsBytes(encodePng(screenshot), flush: true); + + if (_isPreSubmit) { + return _uploadInPreSubmit(skiaClient, filename, goldenFile); + } + if (_isPostSubmit) { + return _uploadInPostSubmit(skiaClient, filename, goldenFile); + } +} + +Future _uploadInPreSubmit( + SkiaGoldClient skiaClient, + String filename, + File goldenFile, +) { + assert(_isPreSubmit); + return skiaClient.tryjobAdd(filename, goldenFile); +} + +Future _uploadInPostSubmit( + SkiaGoldClient skiaClient, + String filename, + File goldenFile, +) { + assert(_isPostSubmit); + return skiaClient.imgtestAdd(filename, goldenFile); +} diff --git a/web_sdk/web_test_utils/lib/skia_client.dart b/web_sdk/web_test_utils/lib/skia_client.dart new file mode 100644 index 0000000000000..fae028364bcd9 --- /dev/null +++ b/web_sdk/web_test_utils/lib/skia_client.dart @@ -0,0 +1,406 @@ +// 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:io'; + +import 'package:crypto/crypto.dart'; +import 'package:path/path.dart' as path; +import 'package:process/process.dart'; + +import 'environment.dart'; + +const String _kGoldctlKey = 'GOLDCTL'; + +const String _skiaGoldHost = 'https://flutter-engine-gold.skia.org'; +const String _instance = 'flutter-engine'; + +/// A client for uploading image tests and making baseline requests to the +/// Flutter Gold Dashboard. +class SkiaGoldClient { + /// Creates a [SkiaGoldClient] with the given [workDirectory]. + /// + /// The [browserName] parameter is the name of the browser that generated the + /// screenshots. + SkiaGoldClient(this.workDirectory, { required this.browserName }); + + /// Whether the Skia Gold client is available and can be used in this + /// environment. + static bool get isAvailable => Platform.environment.containsKey(_kGoldctlKey); + + /// The name of the browser running the tests. + final String browserName; + + /// A controller for launching sub-processes. + final ProcessManager process = const LocalProcessManager(); + + /// A client for making Http requests to the Flutter Gold dashboard. + final HttpClient httpClient = HttpClient(); + + /// The local [Directory] for the current test context. In this directory, the + /// client will create image and JSON files for the `goldctl` tool to use. + final Directory workDirectory; + + String get _tempPath => path.join(workDirectory.path, 'temp'); + String get _keysPath => path.join(workDirectory.path, 'keys.json'); + String get _failuresPath => path.join(workDirectory.path, 'failures.json'); + + /// Indicates whether the `goldctl` tool has been initialized for the current + /// test context. + bool _isInitialized = false; + + /// Indicates whether the client has already been authorized to communicate + /// with the Skia Gold backend. + bool get _isAuthorized { + final File authFile = File(path.join(_tempPath, 'auth_opt.json')); + + if(authFile.existsSync()) { + final String contents = authFile.readAsStringSync(); + final Map decoded = json.decode(contents) as Map; + return !(decoded['GSUtil'] as bool); + } + return false; + } + + /// The path to the local [Directory] where the `goldctl` tool is hosted. + String get _goldctl { + assert( + isAvailable, + 'Trying to use `goldctl` in an environment where it is not available', + ); + return Platform.environment[_kGoldctlKey]!; + } + + /// Prepares the local work space for golden file testing and calls the + /// `goldctl auth` command. + /// + /// This ensures that the `goldctl` tool is authorized and ready for testing. + Future auth() async { + if (_isAuthorized) + return; + final List authCommand = [ + _goldctl, + 'auth', + '--work-dir', _tempPath, + '--luci', + ]; + + final ProcessResult result = await process.run(authCommand); + + if (result.exitCode != 0) { + final StringBuffer buf = StringBuffer() + ..writeln('Skia Gold authorization failed.') + ..writeln('Luci environments authenticate using the file provided ' + 'by LUCI_CONTEXT. There may be an error with this file or Gold ' + 'authentication.') + ..writeln('Debug information for Gold:') + ..writeln('stdout: ${result.stdout}') + ..writeln('stderr: ${result.stderr}'); + throw Exception(buf.toString()); + } + } + + /// Executes the `imgtest init` command in the `goldctl` tool. + /// + /// The `imgtest` command collects and uploads test results to the Skia Gold + /// backend, the `init` argument initializes the current test. + Future _imgtestInit() async { + if (_isInitialized) { + return; + } + + final File keys = File(_keysPath); + final File failures = File(_failuresPath); + + await keys.writeAsString(_getKeysJSON()); + await failures.create(); + final String commitHash = await _getCurrentCommit(); + + final List imgtestInitCommand = [ + _goldctl, + 'imgtest', 'init', + '--instance', _instance, + '--work-dir', _tempPath, + '--commit', commitHash, + '--keys-file', keys.path, + '--failure-file', failures.path, + '--passfail', + ]; + + if (imgtestInitCommand.contains(null)) { + final StringBuffer buf = StringBuffer() + ..writeln('A null argument was provided for Skia Gold imgtest init.') + ..writeln('Please confirm the settings of your golden file test.') + ..writeln('Arguments provided:'); + imgtestInitCommand.forEach(buf.writeln); + throw Exception(buf.toString()); + } + + final ProcessResult result = await process.run(imgtestInitCommand); + + if (result.exitCode != 0) { + final StringBuffer buf = StringBuffer() + ..writeln('Skia Gold imgtest init failed.') + ..writeln('An error occurred when initializing golden file test with ') + ..writeln('goldctl.') + ..writeln() + ..writeln('Debug information for Gold:') + ..writeln('stdout: ${result.stdout}') + ..writeln('stderr: ${result.stderr}'); + throw Exception(buf.toString()); + } + _isInitialized = true; + } + + /// Executes the `imgtest add` command in the `goldctl` tool. + /// + /// The `imgtest` command collects and uploads test results to the Skia Gold + /// backend, the `add` argument uploads the current image test. A response is + /// returned from the invocation of this command that indicates a pass or fail + /// result. + /// + /// The [testName] and [goldenFile] parameters reference the current + /// comparison being evaluated. + Future imgtestAdd(String testName, File goldenFile) async { + await _imgtestInit(); + + final List imgtestCommand = [ + _goldctl, + 'imgtest', 'add', + '--work-dir', _tempPath, + '--test-name', cleanTestName(testName), + '--png-file', goldenFile.path, + ]; + + final ProcessResult result = await process.run(imgtestCommand); + + if (result.exitCode != 0) { + // We do not want to throw for non-zero exit codes here, as an intentional + // change or new golden file test expect non-zero exit codes. Logging here + // is meant to inform when an unexpected result occurs. + print('goldctl imgtest add stdout: ${result.stdout}'); + print('goldctl imgtest add stderr: ${result.stderr}'); + } + + return true; + } + + /// Executes the `imgtest init` command in the `goldctl` tool for tryjobs. + /// + /// The `imgtest` command collects and uploads test results to the Skia Gold + /// backend, the `init` argument initializes the current tryjob. + Future _tryjobInit() async { + if (_isInitialized) { + return; + } + + final File keys = File(_keysPath); + final File failures = File(_failuresPath); + + await keys.writeAsString(_getKeysJSON()); + await failures.create(); + final String commitHash = await _getCurrentCommit(); + + final List imgtestInitCommand = [ + _goldctl, + 'imgtest', 'init', + '--instance', _instance, + '--work-dir', _tempPath, + '--commit', commitHash, + '--keys-file', keys.path, + '--failure-file', failures.path, + '--passfail', + '--crs', 'github', + '--patchset_id', commitHash, + ...getCIArguments(), + ]; + + if (imgtestInitCommand.contains(null)) { + final StringBuffer buf = StringBuffer() + ..writeln('A null argument was provided for Skia Gold tryjob init.') + ..writeln('Please confirm the settings of your golden file test.') + ..writeln('Arguments provided:'); + imgtestInitCommand.forEach(buf.writeln); + throw Exception(buf.toString()); + } + + final ProcessResult result = await process.run(imgtestInitCommand); + + if (result.exitCode != 0) { + final StringBuffer buf = StringBuffer() + ..writeln('Skia Gold tryjobInit failure.') + ..writeln('An error occurred when initializing golden file tryjob with ') + ..writeln('goldctl.') + ..writeln() + ..writeln('Debug information for Gold:') + ..writeln('stdout: ${result.stdout}') + ..writeln('stderr: ${result.stderr}'); + throw Exception(buf.toString()); + } + _isInitialized = true; + } + + /// Executes the `imgtest add` command in the `goldctl` tool for tryjobs. + /// + /// The `imgtest` command collects and uploads test results to the Skia Gold + /// backend, the `add` argument uploads the current image test. A response is + /// returned from the invocation of this command that indicates a pass or fail + /// result for the tryjob. + /// + /// The [testName] and [goldenFile] parameters reference the current + /// comparison being evaluated. + Future tryjobAdd(String testName, File goldenFile) async { + await _tryjobInit(); + + final List imgtestCommand = [ + _goldctl, + 'imgtest', 'add', + '--work-dir', _tempPath, + '--test-name', cleanTestName(testName), + '--png-file', goldenFile.path, + ]; + + final ProcessResult result = await process.run(imgtestCommand); + + final String resultStdout = result.stdout.toString(); + if (result.exitCode != 0 && + !(resultStdout.contains('Untriaged') || resultStdout.contains('negative image'))) { + final StringBuffer buf = StringBuffer() + ..writeln('Unexpected Gold tryjobAdd failure.') + ..writeln('Tryjob execution for golden file test $testName failed for') + ..writeln('a reason unrelated to pixel comparison.') + ..writeln() + ..writeln('Debug information for Gold:') + ..writeln('stdout: ${result.stdout}') + ..writeln('stderr: ${result.stderr}') + ..writeln(); + throw Exception(buf.toString()); + } + } + + /// Returns the latest positive digest for the given test known to Skia Gold + /// at head. + Future getExpectationForTest(String testName) async { + late String? expectation; + final String traceID = getTraceID(testName); + await HttpOverrides.runWithHttpOverrides>(() async { + final Uri requestForExpectations = Uri.parse( + '$_skiaGoldHost/json/v2/latestpositivedigest/$traceID' + ); + late String rawResponse; + try { + final HttpClientRequest request = await httpClient.getUrl(requestForExpectations); + final HttpClientResponse response = await request.close(); + rawResponse = await utf8.decodeStream(response); + final dynamic jsonResponse = json.decode(rawResponse); + if (jsonResponse is! Map) + throw const FormatException('Skia gold expectations do not match expected format.'); + expectation = jsonResponse['digest'] as String?; + } on FormatException catch (error) { + print( + 'Formatting error detected requesting expectations from Flutter Gold.\n' + 'error: $error\n' + 'url: $requestForExpectations\n' + 'response: $rawResponse' + ); + rethrow; + } + }, + SkiaGoldHttpOverrides(), + ); + return expectation; + } + + /// Returns a list of bytes representing the golden image retrieved from the + /// Skia Gold dashboard. + /// + /// The provided image hash represents an expectation from Skia Gold. + Future>getImageBytes(String imageHash) async { + final List imageBytes = []; + await HttpOverrides.runWithHttpOverrides>(() async { + final Uri requestForImage = Uri.parse( + '$_skiaGoldHost/img/images/$imageHash.png', + ); + + final HttpClientRequest request = await httpClient.getUrl(requestForImage); + final HttpClientResponse response = await request.close(); + await response.forEach((List bytes) => imageBytes.addAll(bytes)); + }, + SkiaGoldHttpOverrides(), + ); + return imageBytes; + } + + /// Returns the current commit hash of the engine repository. + Future _getCurrentCommit() async { + final Directory webUiRoot = environment.webUiRootDir; + if (!webUiRoot.existsSync()) { + throw Exception('Web Engine root could not be found: $webUiRoot\n'); + } else { + final ProcessResult revParse = await process.run( + ['git', 'rev-parse', 'HEAD'], + workingDirectory: webUiRoot.path, + ); + if (revParse.exitCode != 0) { + throw Exception('Current commit of Web Engine can not be found.'); + } + return (revParse.stdout as String).trim(); + } + } + + /// Returns a Map of key value pairs used to uniquely identify the + /// configuration that generated the given golden file. + /// + /// Currently, the only key value pairs being tracked are the platform and + /// browser the image was rendered on. + Map _getKeys() { + return { + 'CI': 'luci', + 'Platform': Platform.operatingSystem, + 'Browser': browserName, + }; + } + + /// Same as [_getKeys] but encodes it in a JSON string. + String _getKeysJSON() { + return json.encode(_getKeys()); + } + + /// Removes the file extension from the [fileName] to represent the test name + /// properly. + String cleanTestName(String fileName) { + return fileName.split(path.extension(fileName))[0]; + } + + /// Returns a list of arguments for initializing a tryjob based on the testing + /// environment. + List getCIArguments() { + final String jobId = Platform.environment['LOGDOG_STREAM_PREFIX']!.split('/').last; + final List refs = Platform.environment['GOLD_TRYJOB']!.split('/'); + final String pullRequest = refs[refs.length - 2]; + + return [ + '--changelist', pullRequest, + '--cis', 'buildbucket', + '--jobid', jobId, + ]; + } + + /// Returns a trace id based on the current testing environment to lookup + /// the latest positive digest on Skia Gold with a hex-encoded md5 hash of + /// the image keys. + String getTraceID(String testName) { + final Map keys = { + ..._getKeys(), + 'name': testName, + 'source_type': _instance, + }; + final String jsonTrace = json.encode(keys); + final String md5Sum = md5.convert(utf8.encode(jsonTrace)).toString(); + return md5Sum; + } +} + +/// Used to make HttpRequests during testing. +class SkiaGoldHttpOverrides extends HttpOverrides { } diff --git a/web_sdk/web_test_utils/pubspec.yaml b/web_sdk/web_test_utils/pubspec.yaml index 1097d8834b1d6..6aedb286401eb 100644 --- a/web_sdk/web_test_utils/pubspec.yaml +++ b/web_sdk/web_test_utils/pubspec.yaml @@ -5,7 +5,12 @@ environment: sdk: ">=2.12.0-0 <3.0.0" dependencies: - path: 1.8.0 + collection: 1.15.0 + crypto: 3.0.1 image: 3.0.1 js: 0.6.3 + meta: 1.3.0 + path: 1.8.0 + process: 4.2.3 + typed_data: 1.3.0 yaml: 3.0.0