diff --git a/BUILD.gn b/BUILD.gn index a4dd6ca59ce58..97c2c4fe9c38a 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -84,6 +84,10 @@ group("flutter") { "//flutter/examples/glfw", "//flutter/examples/vulkan_glfw", ] + + if (!is_mac) { + public_deps += [ "//flutter/examples/glfw_drm" ] + } } # If enbaled, compile the SDK / snapshot. diff --git a/examples/glfw_drm/.gitignore b/examples/glfw_drm/.gitignore new file mode 100644 index 0000000000000..b7443460fb263 --- /dev/null +++ b/examples/glfw_drm/.gitignore @@ -0,0 +1 @@ +debug/ diff --git a/examples/glfw_drm/BUILD.gn b/examples/glfw_drm/BUILD.gn new file mode 100644 index 0000000000000..784e5654e5e69 --- /dev/null +++ b/examples/glfw_drm/BUILD.gn @@ -0,0 +1,20 @@ +# 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/examples/examples.gni") + +if (build_embedder_examples) { + executable("glfw_drm") { + output_name = "embedder_example_drm" + + sources = [ "FlutterEmbedderGLFW.cc" ] + + deps = [ + "//flutter/shell/platform/embedder:embedder", + "//third_party/glfw", + ] + + libs = [ "EGL" ] + } +} diff --git a/examples/glfw_drm/CMakeLists.txt b/examples/glfw_drm/CMakeLists.txt new file mode 100644 index 0000000000000..2563bd8bee05a --- /dev/null +++ b/examples/glfw_drm/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.15) +project(FlutterEmbedderGLFW) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11" ) + +add_executable(flutter_glfw FlutterEmbedderGLFW.cc) + +############################################################ +# GLFW +############################################################ +option(GLFW_BUILD_EXAMPLES "" OFF) +option(GLFW_BUILD_TESTS "" OFF) +option(GLFW_BUILD_DOCS "" OFF) +option(GLFW_INSTALL "" OFF) +find_package(OpenGL REQUIRED COMPONENTS EGL) +include_directories(${OPENGL_INCLUDE_DIRS}) +add_subdirectory(${CMAKE_SOURCE_DIR}/../../../third_party/glfw glfw) +target_link_libraries(flutter_glfw glfw OpenGL::EGL) +include_directories(${CMAKE_SOURCE_DIR}/../../../third_party/glfw/include) + +############################################################ +# Flutter Engine +############################################################ +# This is assuming you've built a local version of the Flutter Engine. If you +# downloaded yours is from the internet you'll have to change this. +include_directories(${CMAKE_SOURCE_DIR}/../../shell/platform/embedder) +find_library(FLUTTER_LIB flutter_engine PATHS ${CMAKE_SOURCE_DIR}/../../../out/host_debug_unopt) +target_link_libraries(flutter_glfw ${FLUTTER_LIB}) + +# Copy the flutter library here since the shared library +# name is `./libflutter_engine.dylib`. +add_custom_command( + TARGET flutter_glfw POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${FLUTTER_LIB} + ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/examples/glfw_drm/FlutterEmbedderGLFW.cc b/examples/glfw_drm/FlutterEmbedderGLFW.cc new file mode 100644 index 0000000000000..d75811092f927 --- /dev/null +++ b/examples/glfw_drm/FlutterEmbedderGLFW.cc @@ -0,0 +1,334 @@ +// 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 + +#define GLFW_EXPOSE_NATIVE_EGL +#define GLFW_INCLUDE_GLEXT + +#include +#include +#include +#include +#include +#include +#include "GLFW/glfw3.h" +#include "GLFW/glfw3native.h" +#include "embedder.h" + +// This value is calculated after the window is created. +static double g_pixelRatio = 1.0; +static const size_t kInitialWindowWidth = 800; +static const size_t kInitialWindowHeight = 600; +// Maximum damage history - for triple buffering we need to store damage for +// last two frames; Some Android devices (Pixel 4) use quad buffering. +static const int kMaxHistorySize = 10; + +// Keeps track of the most recent frame damages so that existing damage can +// be easily computed. +std::list damage_history_; + +// Keeps track of the existing damage associated with each FBO ID +std::unordered_map existing_damage_map_; + +EGLDisplay display_; +EGLSurface surface_; + +static_assert(FLUTTER_ENGINE_VERSION == 1, + "This Flutter Embedder was authored against the stable Flutter " + "API at version 1. There has been a serious breakage in the " + "API. Please read the ChangeLog and take appropriate action " + "before updating this assertion"); + +void GLFWcursorPositionCallbackAtPhase(GLFWwindow* window, + FlutterPointerPhase phase, + double x, + double y) { + FlutterPointerEvent event = {}; + event.struct_size = sizeof(event); + event.phase = phase; + event.x = x * g_pixelRatio; + event.y = y * g_pixelRatio; + event.timestamp = + std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()) + .count(); + FlutterEngineSendPointerEvent( + reinterpret_cast(glfwGetWindowUserPointer(window)), &event, + 1); +} + +void GLFWcursorPositionCallback(GLFWwindow* window, double x, double y) { + GLFWcursorPositionCallbackAtPhase(window, FlutterPointerPhase::kMove, x, y); +} + +void GLFWmouseButtonCallback(GLFWwindow* window, + int key, + int action, + int mods) { + if (key == GLFW_MOUSE_BUTTON_1 && action == GLFW_PRESS) { + double x, y; + glfwGetCursorPos(window, &x, &y); + GLFWcursorPositionCallbackAtPhase(window, FlutterPointerPhase::kDown, x, y); + glfwSetCursorPosCallback(window, GLFWcursorPositionCallback); + } + + if (key == GLFW_MOUSE_BUTTON_1 && action == GLFW_RELEASE) { + double x, y; + glfwGetCursorPos(window, &x, &y); + GLFWcursorPositionCallbackAtPhase(window, FlutterPointerPhase::kUp, x, y); + glfwSetCursorPosCallback(window, nullptr); + } +} + +static void GLFWKeyCallback(GLFWwindow* window, + int key, + int scancode, + int action, + int mods) { + if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) { + glfwSetWindowShouldClose(window, GLFW_TRUE); + } +} + +void GLFWwindowSizeCallback(GLFWwindow* window, int width, int height) { + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = width * g_pixelRatio; + event.height = height * g_pixelRatio; + event.pixel_ratio = g_pixelRatio; + FlutterEngineSendWindowMetricsEvent( + reinterpret_cast(glfwGetWindowUserPointer(window)), + &event); +} + +// Auxiliary function used to transform a FlutterRect into the format that is +// expected by the EGL functions (i.e. array of EGLint). +static std::array RectToInts(const FlutterRect rect) { + EGLint height; + eglQuerySurface(display_, surface_, EGL_HEIGHT, &height); + + std::array res{ + static_cast(rect.left), height - static_cast(rect.bottom), + static_cast(rect.right) - static_cast(rect.left), + static_cast(rect.bottom) - static_cast(rect.top)}; + return res; +} + +// Auxiliary function to union the damage regions comprised by two FlutterRect +// element. It saves the result of this join in the rect variable. +static void JoinFlutterRect(FlutterRect* rect, FlutterRect additional_rect) { + rect->left = std::min(rect->left, additional_rect.left); + rect->top = std::min(rect->top, additional_rect.top); + rect->right = std::max(rect->right, additional_rect.right); + rect->bottom = std::max(rect->bottom, additional_rect.bottom); +} + +// Auxiliary function used to check if the given list of extensions contains the +// requested extension name. +static bool HasExtension(const char* extensions, const char* name) { + const char* r = strstr(extensions, name); + auto len = strlen(name); + // check that the extension name is terminated by space or null terminator + return r != nullptr && (r[len] == ' ' || r[len] == 0); +} + +bool RunFlutter(GLFWwindow* window, + const std::string& project_path, + const std::string& icudtl_path) { + FlutterRendererConfig config = {}; + config.type = kOpenGL; + config.open_gl.struct_size = sizeof(config.open_gl); + config.open_gl.make_current = [](void* userdata) -> bool { + glfwMakeContextCurrent(static_cast(userdata)); + return true; + }; + config.open_gl.clear_current = [](void*) -> bool { + glfwMakeContextCurrent(nullptr); // is this even a thing? + return true; + }; + config.open_gl.present_with_info = + [](void* userdata, const FlutterPresentInfo* info) -> bool { + // Free the existing damage that was allocated to this frame. + free(existing_damage_map_[info->fbo_id]); + + // Get list of extensions. + const char* extensions = eglQueryString(display_, EGL_EXTENSIONS); + + // Retrieve the set damage region function. + PFNEGLSETDAMAGEREGIONKHRPROC set_damage_region_ = nullptr; + if (HasExtension(extensions, "EGL_KHR_partial_update")) { + set_damage_region_ = reinterpret_cast( + eglGetProcAddress("eglSetDamageRegionKHR")); + } + + // Retrieve the swap buffers with damage function. + PFNEGLSWAPBUFFERSWITHDAMAGEEXTPROC swap_buffers_with_damage_ = nullptr; + if (HasExtension(extensions, "EGL_EXT_swap_buffers_with_damage")) { + swap_buffers_with_damage_ = + reinterpret_cast( + eglGetProcAddress("eglSwapBuffersWithDamageEXT")); + } else if (HasExtension(extensions, "EGL_KHR_swap_buffers_with_damage")) { + swap_buffers_with_damage_ = + reinterpret_cast( + eglGetProcAddress("eglSwapBuffersWithDamageKHR")); + } + + if (set_damage_region_) { + // Set the buffer damage as the damage region. + auto buffer_rects = RectToInts(info->buffer_damage.damage[0]); + set_damage_region_(display_, surface_, buffer_rects.data(), 1); + } + + // Add frame damage to damage history + damage_history_.push_back(info->frame_damage.damage[0]); + if (damage_history_.size() > kMaxHistorySize) { + damage_history_.pop_front(); + } + + if (swap_buffers_with_damage_) { + // Swap buffers with frame damage. + auto frame_rects = RectToInts(info->frame_damage.damage[0]); + return swap_buffers_with_damage_(display_, surface_, frame_rects.data(), + 1); + } else { + // If the required extensions for partial repaint were not provided, do + // full repaint. + return eglSwapBuffers(display_, surface_); + } + }; + config.open_gl.fbo_callback = [](void*) -> uint32_t { + return 0; // FBO0 + }; + config.open_gl.populate_existing_damage = + [](void* userdata, intptr_t fbo_id, + FlutterDamage* existing_damage) -> void { + // Given the FBO age, create existing damage region by joining all frame + // damages since FBO was last used + EGLint age; + if (glfwExtensionSupported("GL_EXT_buffer_age") == GLFW_TRUE) { + eglQuerySurface(display_, surface_, EGL_BUFFER_AGE_EXT, &age); + } else { + age = 4; // Virtually no driver should have a swapchain length > 4. + } + + existing_damage->num_rects = 1; + + // Allocate the array of rectangles for the existing damage. + existing_damage_map_[fbo_id] = static_cast( + malloc(sizeof(FlutterRect) * existing_damage->num_rects)); + existing_damage_map_[fbo_id][0] = + FlutterRect{0, 0, kInitialWindowWidth, kInitialWindowHeight}; + existing_damage->damage = existing_damage_map_[fbo_id]; + + if (age > 1) { + --age; + // join up to (age - 1) last rects from damage history + for (auto i = damage_history_.rbegin(); + i != damage_history_.rend() && age > 0; ++i, --age) { + if (i == damage_history_.rbegin()) { + if (i != damage_history_.rend()) { + existing_damage->damage[0] = {i->left, i->top, i->right, i->bottom}; + } + } else { + JoinFlutterRect(&(existing_damage->damage[0]), *i); + } + } + } + }; + config.open_gl.gl_proc_resolver = [](void*, const char* name) -> void* { + return reinterpret_cast(glfwGetProcAddress(name)); + }; + config.open_gl.fbo_reset_after_present = true; + + // This directory is generated by `flutter build bundle`. + std::string assets_path = project_path + "/build/flutter_assets"; + FlutterProjectArgs args = { + .struct_size = sizeof(FlutterProjectArgs), + .assets_path = assets_path.c_str(), + .icu_data_path = + icudtl_path.c_str(), // Find this in your bin/cache directory. + }; + FlutterEngine engine = nullptr; + FlutterEngineResult result = + FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, // renderer + &args, window, &engine); + if (result != kSuccess || engine == nullptr) { + std::cout << "Could not run the Flutter Engine." << std::endl; + return false; + } + + glfwSetWindowUserPointer(window, engine); + GLFWwindowSizeCallback(window, kInitialWindowWidth, kInitialWindowHeight); + + return true; +} + +void printUsage() { + std::cout + << "usage: embedder_example_drm " + << std::endl; +} + +void GLFW_ErrorCallback(int error, const char* description) { + std::cout << "GLFW Error: (" << error << ") " << description << std::endl; +} + +int main(int argc, const char* argv[]) { + if (argc != 3) { + printUsage(); + return 1; + } + + std::string project_path = argv[1]; + std::string icudtl_path = argv[2]; + + glfwSetErrorCallback(GLFW_ErrorCallback); + + int result = glfwInit(); + if (result != GLFW_TRUE) { + std::cout << "Could not initialize GLFW." << std::endl; + return EXIT_FAILURE; + } + +#if defined(__linux__) + glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); +#endif + + GLFWwindow* window = glfwCreateWindow( + kInitialWindowWidth, kInitialWindowHeight, "Flutter", NULL, NULL); + if (window == nullptr) { + std::cout << "Could not create GLFW window." << std::endl; + return EXIT_FAILURE; + } + + int framebuffer_width, framebuffer_height; + glfwGetFramebufferSize(window, &framebuffer_width, &framebuffer_height); + g_pixelRatio = framebuffer_width / kInitialWindowWidth; + + // Get the display and surface variables. + display_ = glfwGetEGLDisplay(); + surface_ = glfwGetEGLSurface(window); + + bool run_result = RunFlutter(window, project_path, icudtl_path); + if (!run_result) { + std::cout << "Could not run the Flutter engine." << std::endl; + return EXIT_FAILURE; + } + + glfwSetKeyCallback(window, GLFWKeyCallback); + glfwSetWindowSizeCallback(window, GLFWwindowSizeCallback); + glfwSetMouseButtonCallback(window, GLFWmouseButtonCallback); + + while (!glfwWindowShouldClose(window)) { + glfwWaitEvents(); + } + + glfwDestroyWindow(window); + glfwTerminate(); + + return EXIT_SUCCESS; +} diff --git a/examples/glfw_drm/README.md b/examples/glfw_drm/README.md new file mode 100644 index 0000000000000..a6b4018a2a998 --- /dev/null +++ b/examples/glfw_drm/README.md @@ -0,0 +1,37 @@ +# Flutter Embedder Engine GLFW Example +## Description +This is an example of how to use Flutter Engine Embedder in order to get a +Flutter project rendering in a new host environment. The intended audience is +people who need to support host environment other than the ones already provided +by Flutter. This is an advanced topic and not intended for beginners. + +In this example we are demonstrating rendering a Flutter project inside of the GUI +library [GLFW](https://www.glfw.org/). For more information about using the +embedder you can read the wiki article [Custom Flutter Engine Embedders](https://github.com/flutter/flutter/wiki/Custom-Flutter-Engine-Embedders). + +The key difference between this example and the other GLFW example in this +folder is that the present example implements dirty region management (i.e. +rendering only the regions that have changed between frames as opposed to always +rendering the entire frame). For more information on the implementation of +dirty region management within the Embedder API, see [DRM Embedder](https://flutter.dev/go/drm-embedder). + +## Running Instructions +The following example was tested on Linux but with a bit of tweaking should be +able to run on other *nix platforms and Windows. However, because this example +uses the EGL library, it is not compatible with MacOS platforms. + +The example has the following dependencies: + * [GLFW](https://www.glfw.org/) - This can be installed by running `sudo apt-get install libglfw3` + * [CMake](https://cmake.org/) - This can be installed by running `sudo apt-get install cmake` + * [EGL](https://docs.mesa3d.org/egl.html) - This can be installed by running `sudo apt-get install libglfw3-dev` + * [Flutter](https://flutter.dev/) - This can be installed from the [Flutter webpage](https://flutter.dev/docs/get-started/install) + * [Flutter Engine](https://flutter.dev) - This can be built or downloaded, see [Custom Flutter Engine Embedders](https://github.com/flutter/flutter/wiki/Custom-Flutter-Engine-Embedders) for more information. + +In order to **build** and **run** the example you should be able to go into this directory and run +`./run.sh`. + +## Troubleshooting +There are a few things you might have to tweak in order to get your build working: + * Flutter Engine Location - Inside the `CMakeList.txt` file you will see that it is set up to search for the header and library for the Flutter Engine in specific locations, those might not be the location of your Flutter Engine. + * Pixel Ratio - If the project runs but is drawing at the wrong scale you may have to tweak the `kPixelRatio` variable in `FlutterEmbedderGLFW.cc` file. + * GLFW Location - Inside the `CMakeLists.txt` we are searching for the GLFW library, if CMake can't find it you may have to edit that. diff --git a/examples/glfw_drm/main.dart b/examples/glfw_drm/main.dart new file mode 100644 index 0000000000000..d8ad9221f8e4c --- /dev/null +++ b/examples/glfw_drm/main.dart @@ -0,0 +1,80 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' + show debugDefaultTargetPlatformOverride; +import 'package:flutter_spinkit/flutter_spinkit.dart'; + + +void main() { + // This is a hack to make Flutter think you are running on Google Fuchsia, + // otherwise you will get an error about running from an unsupported platform. + debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + // This is the theme of your application. + // + // Try running your application with "flutter run". You'll see the + // application has a blue toolbar. Then, without quitting the app, try + // changing the primarySwatch below to Colors.green and then invoke + // "hot reload" (press "r" in the console where you ran "flutter run", + // or simply save your changes to "hot reload" in a Flutter IDE). + // Notice that the counter didn't reset back to zero; the application + // is not restarted. + primarySwatch: Colors.blue, + ), + home: MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key? key, required this.title}) : super(key: key); + + // This widget is the home page of your application. It is stateful, meaning + // that it has a State object (defined below) that contains fields that affect + // how it looks. + + // This class is the configuration for the state. It holds the values (in this + // case the title) provided by the parent (in this case the App widget) and + // used by the build method of the State. Fields in a Widget subclass are + // always marked "final". + + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + @override + Widget build(BuildContext context) { + // The Flutter framework has been optimized to make rerunning build methods + // fast, so that you can just rebuild anything that needs updating rather + // than having to individually change instances of widgets. + return Scaffold( + appBar: AppBar( + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: Text(widget.title), + ), + body: Center( + // Center is a layout widget. It takes a single child and positions it + // in the middle of the parent. + child: RepaintBoundary( + child: SpinKitRotatingCircle(color: Colors.blue, size: 50.0), + ), + ), + ); + } +} diff --git a/examples/glfw_drm/run.sh b/examples/glfw_drm/run.sh new file mode 100755 index 0000000000000..0ae097e4cc990 --- /dev/null +++ b/examples/glfw_drm/run.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e # Exit if any program returns an error. + +################################################################# +# Make the host C++ project. +################################################################# +if [ ! -d debug ]; then + mkdir debug +fi +cd debug +cmake -DCMAKE_BUILD_TYPE=Debug .. +make + +################################################################# +# Make the guest Flutter project. +################################################################# +if [ ! -d myapp ]; then + flutter create myapp + cd myapp + flutter pub add flutter_spinkit + cd .. +fi +cd myapp +cp ../../main.dart lib/main.dart +flutter build bundle \ + --local-engine-src-path ../../../../../ \ + --local-engine=host_debug_unopt +cd - + +################################################################# +# Run the Flutter Engine Embedder +################################################################# +./flutter_glfw ./myapp ../../../../third_party/icu/common/icudtl.dat