| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| // Entrypoint for running a Flutter app on Fuchsia. |
| // |
| // Usage: ./main <path_to_flutter_asset_bundle> |
| |
| #include <fuchsia/fonts/cpp/fidl.h> |
| #include <fuchsia/scenic/scheduling/cpp/fidl.h> |
| #include <fuchsia/sysmem/cpp/fidl.h> |
| #include <fuchsia/ui/app/cpp/fidl.h> |
| #include <fuchsia/ui/composition/cpp/fidl.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/async/default.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fidl/cpp/binding_set.h> |
| #include <lib/fidl/cpp/interface_request.h> |
| #include <lib/sys/cpp/component_context.h> |
| #include <lib/syslog/global.h> |
| #include <lib/ui/scenic/cpp/view_identity.h> |
| #include <zircon/status.h> |
| |
| #include <string> |
| |
| #include "src/embedder/embedder_state.h" |
| #include "src/embedder/engine/embedder.h" |
| #include "src/embedder/flatland_ids.h" |
| #include "src/embedder/flatland_view_provider.h" |
| #include "src/embedder/logging.h" |
| #include "src/embedder/software_surface.h" |
| #include "src/embedder/software_surface_producer.h" |
| |
| namespace embedder { |
| namespace { |
| |
| // TODO(akbiggs): Don't hardcode this screen size, get it from |
| // an event instead. |
| constexpr int kScreenWidth = 1280; |
| constexpr int kScreenHeight = 800; |
| |
| /// Fuchsia implementation of the Flutter Engine's |vsync_callback|. |
| void FuchsiaVsync(void* user_data, intptr_t baton) { |
| EmbedderState* embedder = static_cast<EmbedderState*>(user_data); |
| |
| // Encourage the Engine to vsync for a 60Hz display. |
| const uint64_t now_nanos = FlutterEngineGetCurrentTime(); |
| const uint64_t target_vsync_time = now_nanos + 16.6 * 1e6; |
| FlutterEngineOnVsync(embedder->engine, baton, now_nanos, target_vsync_time); |
| } |
| |
| /// Fuchsia implementation of the Flutter Engine's |log_message_callback|. |
| void FuchsiaLogMessage(const char* tag, const char* message, void* user_data) { |
| FX_LOG(INFO, tag, message); |
| } |
| |
| /// Fuchsia implementation of the Flutter Engine's software rendering |
| /// |surface_acquire_callback|. |
| bool FuchsiaAcquireSoftwareSurface(void* user_data, size_t width, size_t height, |
| uint8_t** allocation, size_t* stride) { |
| EmbedderState* embedder = static_cast<EmbedderState*>(user_data); |
| |
| if (embedder->software_surface_producer == nullptr) { |
| FX_LOG(ERROR, kLogTag, "Software surface producer has not been initialized."); |
| } |
| |
| if (!embedder->software_surface_producer->IsValid()) { |
| // TODO(akbiggs): Maybe recover from this state? |
| FX_LOGF(ERROR, kLogTag, "Software surface producer has become invalid."); |
| return false; |
| } |
| |
| auto surface = embedder->software_surface_producer->ProduceSurface( |
| {.width = static_cast<uint32_t>(width), .height = static_cast<uint32_t>(height)}); |
| if (surface == nullptr) { |
| FX_LOGF(ERROR, kLogTag, "Failed to acquire software surface of size (%lu, %lu).", width, |
| height); |
| return false; |
| } |
| |
| // If we receive an unitialized surface, we need to create a Flatland |
| // image and associate it with the surface. |
| if (surface->GetImageId() == 0) { |
| auto image_id = embedder->flatland_connection->NextContentId().value; |
| const auto surface_size = surface->GetSize(); |
| |
| fuchsia::math::SizeU image_size; |
| image_size.width = surface_size.width; |
| image_size.height = surface_size.height; |
| |
| fuchsia::ui::composition::ImageProperties image_properties; |
| image_properties.set_size(image_size); |
| |
| embedder->flatland_connection->flatland()->CreateImage( |
| {image_id}, surface->GetBufferCollectionImportToken(), 0, std::move(image_properties)); |
| |
| surface->SetImageId(image_id); |
| surface->SetReleaseImageCallback([flatland = embedder->flatland_connection.get(), image_id]() { |
| flatland->flatland()->ReleaseImage({image_id}); |
| }); |
| } |
| |
| // Enqueue fences for the next present. |
| embedder->flatland_connection->EnqueueAcquireFence(surface->GetAcquireFence()); |
| embedder->flatland_connection->EnqueueReleaseFence(surface->GetReleaseFence()); |
| |
| // Preserve the surface for when we need to present it. |
| embedder->software_surface = std::move(surface); |
| *allocation = embedder->software_surface->GetAllocation(); |
| *stride = embedder->software_surface->GetBytesPerRow(); |
| |
| return true; |
| } |
| |
| /// Fuchsia implementation of the Flutter Engine's software rendering |
| /// |surface_present_callback|. |
| bool FuchsiaPresentSoftwareSurface(void* user_data, const void* allocation, size_t row_bytes, |
| size_t height) { |
| EmbedderState* embedder = static_cast<EmbedderState*>(user_data); |
| |
| if (embedder->software_surface == nullptr) { |
| // We have no surface acquired to present. |
| return false; |
| } |
| |
| // TODO(akbiggs): Replace with composition logic matching |
| // https://github.com/flutter/engine/blob/main/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc#L108. |
| auto image_id = embedder->software_surface->GetImageId(); |
| embedder->flatland_connection->flatland()->SetContent({kRootTransformId}, {image_id}); |
| // TODO(akbiggs): Remove SetImageDestinationSize call once |
| // https://fuchsia-review.googlesource.com/c/fuchsia/+/696647 lands. |
| const auto& surface_size = embedder->software_surface->GetSize(); |
| fuchsia::math::SizeU image_size; |
| image_size.width = surface_size.width; |
| image_size.height = surface_size.height; |
| embedder->flatland_connection->flatland()->SetImageDestinationSize({image_id}, image_size); |
| |
| embedder->flatland_connection->Present(); |
| |
| // Submit surfaces that are being rendered so that we signal their fences. |
| std::vector<std::unique_ptr<SoftwareSurface>> surfaces_to_submit; |
| surfaces_to_submit.push_back(std::move(embedder->software_surface)); |
| embedder->software_surface = nullptr; |
| embedder->software_surface_producer->SubmitSurfaces(std::move(surfaces_to_submit)); |
| |
| return true; |
| } |
| |
| /// Runs the Flutter app whose assets (including snapshots) are located |
| /// at |assets_path| in the component's package. |
| /// |
| /// The Engine instance will be stored in |embedder| and |embedder| will be |
| /// the user data for all callbacks from the Flutter Engine. |
| bool RunFlutterApp(const char* assets_path, EmbedderState* embedder) { |
| FlutterRendererConfig renderer_config = { |
| .type = kSoftware, |
| .software = |
| { |
| .struct_size = sizeof(FlutterSoftwareRendererConfig), |
| .surface_present_callback = FuchsiaPresentSoftwareSurface, |
| .surface_acquire_callback = FuchsiaAcquireSoftwareSurface, |
| }, |
| }; |
| // Connect and set up the system font provider. |
| fuchsia::fonts::ProviderSyncPtr sync_font_provider; |
| zx_status_t status = embedder->component_context->svc()->Connect<fuchsia::fonts::Provider>( |
| sync_font_provider.NewRequest()); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, "Failed to connect to fonts provider service: %s", |
| zx_status_get_string(status)); |
| } |
| FlutterProjectArgs project_args = { |
| .struct_size = sizeof(FlutterProjectArgs), |
| .assets_path = assets_path, |
| |
| .vsync_callback = FuchsiaVsync, |
| .log_message_callback = FuchsiaLogMessage, |
| |
| .log_tag = "flutter_app", |
| .font_initialization_data = sync_font_provider.Unbind().TakeChannel().release(), |
| }; |
| |
| FlutterEngineResult result = FlutterEngineRun(FLUTTER_ENGINE_VERSION, &renderer_config, |
| &project_args, embedder, &embedder->engine); |
| |
| if (result != kSuccess || embedder->engine == nullptr) { |
| FX_LOGF(ERROR, kLogTag, "Could not run the Flutter Engine. Error code: %d", |
| static_cast<int>(result)); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace |
| } // namespace embedder |
| |
| int main(int argc, const char* argv[]) { |
| if (argc != 2) { |
| FX_LOG(ERROR, embedder::kLogTag, "usage: executable <path to flutter bundle>"); |
| return EXIT_FAILURE; |
| } |
| const char* assets_path = argv[1]; |
| |
| embedder::EmbedderState embedder = { |
| // This must be initialized before calling RunFlutterApp, as |
| // this hack https://github.com/flutter/engine/pull/33472 relies |
| // on it. |
| .component_context = sys::ComponentContext::Create(), |
| }; |
| |
| // Serving the component context must happen after RunFlutterApp |
| // because the embedder platform has a hack that adds inspect data into the |
| // Dart VM using the component context |
| // (https://github.com/flutter/engine/pull/33472). |
| if (!embedder::RunFlutterApp(assets_path, &embedder)) { |
| return EXIT_FAILURE; |
| } |
| |
| // Attaching the async loop to the current thread must happen |
| // immediately after RunFlutterApp to ensure that bindings in the embedder |
| // use the embedder's dispatcher to send requests instead of the Dart VM's |
| // dispatcher. |
| // |
| // Bindings created in the embedder before this line will never get fired. |
| // |
| // TODO(https://fxbug.dev/75282): Remove the logic in the Dart VM that sets its |
| // own default dispatcher to reduce the amount of global state. |
| async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread); |
| |
| // Necessary to get Flutter to render frames. |
| FlutterWindowMetricsEvent window_metrics_event = { |
| .struct_size = sizeof(FlutterWindowMetricsEvent), |
| |
| // TODO(akbiggs): Don't hardcode these. |
| .width = embedder::kScreenWidth, |
| .height = embedder::kScreenHeight, |
| |
| .pixel_ratio = 1, |
| .left = 0, |
| .top = 0, |
| .physical_view_inset_top = 0, |
| .physical_view_inset_right = 0, |
| .physical_view_inset_bottom = 0, |
| .physical_view_inset_left = 0, |
| }; |
| FlutterEngineSendWindowMetricsEvent(embedder.engine, &window_metrics_event); |
| |
| // Connect to Flatland. |
| fuchsia::ui::composition::FlatlandHandle flatland_handle; |
| zx_status_t status = |
| embedder.component_context->svc()->Connect<fuchsia::ui::composition::Flatland>( |
| flatland_handle.NewRequest()); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, "Failed to connect to Flatland: %s", |
| zx_status_get_string(status)); |
| return EXIT_FAILURE; |
| } |
| // TODO(akbiggs): Create this on the raster thread per FlatlandConnection's |
| // documentation. |
| embedder.flatland_connection = std::make_unique<embedder::FlatlandConnection>( |
| // TODO(akbiggs): Pick a more appropriate debug name based on the Flutter |
| // app name. |
| "FlutterEmbedder" /* debug_name */, std::move(flatland_handle), |
| /* on_error_callback = */ |
| []() { |
| // TODO(akbiggs): What should we do on errors? |
| }, |
| /* on_frame_presented_callback = */ |
| [](fuchsia::scenic::scheduling::FramePresentedInfo info) { |
| // TODO(akbiggs): What should we do after a frame finishes presenting? |
| }); |
| |
| embedder::FlatlandViewProvider view_provider(embedder.flatland_connection.get()); |
| fidl::BindingSet<fuchsia::ui::app::ViewProvider> view_provider_bindings; |
| status = embedder.component_context->outgoing()->AddPublicService<fuchsia::ui::app::ViewProvider>( |
| [&view_provider, |
| &view_provider_bindings](fidl::InterfaceRequest<fuchsia::ui::app::ViewProvider> request) { |
| view_provider_bindings.AddBinding(&view_provider, std::move(request)); |
| }); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, "Failed to add ViewProvider service: %s", |
| zx_status_get_string(status)); |
| } |
| |
| // Our run loop's dispatcher must be used to serve the component context in |
| // order for requests' handlers to get called while we're looping. |
| status = embedder.component_context->outgoing()->ServeFromStartupInfo(loop.dispatcher()); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, embedder::kLogTag, "Failed to serve component context: %s", |
| zx_status_get_string(status)); |
| } |
| |
| embedder.software_surface_producer = std::make_unique<embedder::SoftwareSurfaceProducer>(); |
| |
| // Loop until we're done. |
| loop.Run(); |
| |
| FX_LOG(INFO, embedder::kLogTag, "Done looping."); |
| return EXIT_SUCCESS; |
| } |