Недавно мы успешно портировали фреймворк Flutter на ТВ-приставку c открытой программной платформой RDK. В этой статье расскажем о трудностях, с которыми пришлось столкнуться, и предложим решения для успешного запуска и повышения производительности.
Учитывая, что программный стек RDK или Reference Design Kit сейчас активно используется для разработки OTT-приложений, голосового управления приставками и других продвинутых функций для «видео по запросу» (VoD), мы хотели разобраться, сможет ли Flutter работать на ТВ-приставке. Оказалось, что да, но, как это обычно бывает, есть нюансы.
Далее мы по шагам распишем процесс портирования и запуска Flutter на встраиваемых Linux-платформах и разберемся, как этот SDK с открытым исходным кодом от Google чувствует себя на «железе» с ограниченными ресурсами и ARM-процессорами.
Но прежде чем переходить непосредственно к Flutter и его преимуществам скажем пару слов об исходном решении, которое было задействовано на ТВ-приставке. На плате работала связка «набор библиотек EFL + протокол Wayland», а рисование примитивов было реализовано из node.js на основе плагинного нативного модуля. Это решение неплохо себя показало с точки зрения производительности при отображении кадров, однако сам EFL — отнюдь не самый новый фреймворк для отрисовки. А в режиме выполнения node.js со своим огромным event-loopом казался уже не самой перспективной идеей. В то же время Flutter мог позволить нам задействовать более производительную связку рендеринга.
Для тех, кто не в теме: первую версию этого SDK с открытым кодом Google представил еще шесть лет назад. Тогда этот набор средств разработки годился только для Android. Сейчас на нем можно писать приложения для веба, iOS, Linux и даже Google Fuchsia. 🙂 Рабочий язык для разработки приложений на Flutter — Dart, в свое время он был предложен в качестве альтернативы JavaScript.
Перед нами стоял вопрос: даст ли переход на Flutter какой-то выигрыш по производительности? Ведь подход там совершенно иной, хоть в конечном счете и имеется та же графическая подсистема Wayland + OpenGL. Ну и как там с поддержкой процессоров с neon-инструкциями? Были и другие вопросы, например, нюансы по переносу UI на dart или то, что поддержка Linux находится в стадии альфы-беты.
Сборка Flutter Engine для ТВ-приставок на базе ARM
Итак, начнем. Вначале Futter нужно запустить на чужеродной платформе с Wayland + OpenGL ES. В основе рендеринга у Flutter лежит библиотека Skia, которая прекрасно поддерживает OpenGL ES, поэтому в теории все выглядело хорошо.
При сборке Flutter под наши целевые устройства (три ТВ-приставки с RDK), к нашему удивлению, проблемы возникли только на одной. Не будем с ней сражаться, т.к. из-за старой архитектуре intel x86 она для нас не является приоритетной. Лучше сосредоточимся на оставшихся двух ARM-платформах.
Вот, с какими опциями мы собирали Flutter Engine:
./flutter/tools/gn \ --embedder-for-target \ --target-os linux \ --linux-cpu arm \ --target-sysroot DEVICE_SYSROOT --disable-desktop-embeddings \ --arm-float-abi hard --target-toolchain /usr --target-triple arm-linux-gnueabihf --runtime-mode debug ninja -C out/linux_debug_unopt_arm
Большинство опций понятны: собираем под 32-битный ARM-процессор и Linux, выключая при этом все лишнее через --embedder-for-target
--disable-desktop-embeddings
.
Для сборки в системе должен быть установлен clang версии 9 и выше, т.е. это стандартный сборочный механизм Flutter, инструментарий кросс-компиляции gcc не пойдет. Самое важное — подать корректный target-sysroot
устройства с RDK.
Честно говоря, мы удивились, что при сборке не возникло вообще никаких нюансов. На выходе получаем заветную библиотеку flutter_engine.so и заголовок с необходимыми функциями для эмбеддера.
Теперь можно собрать целевой проект flutter/dart с нашей библиотекой/движком. Это сделать легко:
flutter --local-engine-src-path PATH_TO_BUILDED_ENGINE_src --local-engine=host_debug_unopt build bundle
Важно! Сборка проекта должна происходить не на устройстве с собранной библиотекой, а на хостовой, т.е. x86_64!
Для этого достаточно еще раз пройти путь сборкой gn и ninja только под x86_64! Именно она указывается в параметре host_debug_unopt.
PATH_TO_BUILDED_ENGINE_src — это путь, где находится engine/src/out.
За запуск Flutter Engine под системой обычно отвечает embedder, именно он конфигурирует Flutter под целевую систему и дает основные контексты рендеринга библиотеке Skia и Dart-обработчику. Не так давно в состав Flutter добавили linux-embedder, и, в частности, GTK-embedder, так что можно воспользоваться им из коробки. На нашей платформе на момент портирования это был не вариант, нужно было что-то независимое от GTK.
Рассмотрим некоторые особенности реализации, которые пришлось учесть с кастомным эмбеддером (все, кто любит разбирать не нюансы, а исходники целиком, может сразу перейти к проекту нашего форка с доработками на github.com). К тому же по производительности наш вариант немного выигрывал у версии GTK, что было крайне важно для заказчика, и не тянул за собой весь зоопарк GTK-библиотек.
Так что же вообще нужно от эмбеддера для запуска flutter-приложения? Достаточно, чтобы он просто вызывал из библотеки flutter_engine.so
FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args, display /* userdata */, &engine_);
где в качестве параметров идет передача настроек проекта (директория с собранным flutter bundle) FlutterProjectArgs args и аргументов рендеринга FlutterRendererConfig config.
В первой структуре как раз задается путь bundle-пакета, собранного flutter-утилитой, а во второй используются контексты OpenGL .
// пример использования на github.com (flutter_application.cc)
Все довольно примитивно, но этого достаточно для запуска приложения.
Проблемы и их решение
Теперь поговорим о нюансах, с которыми мы столкнулись на этапе портирования. А как же без них? Не только ведь библиотеки собирать 🙂
1. Краш эмбеддера и замена очередности вызова функций
Первая проблема, с которой мы столкнулись — краш эмбеддера под платформой. Казалось бы, инициализация egl-контекста в других приложения происходит нормально, FlutterRendererConfig инициализирован корректно, но нет — эмбеддер не заводится. Значит в связке что-то явно не так. Оказалось, eglBindAPI нельзя вызывать перед eglGetDisplay, на котором происходит особая инициализация nexus-драйвера дисплея (у нас платформа базируется на чипе BCM). В обычном Linux это не проблема, но на целевой платформе оказалась иначе.
Корректная инициализация эмбеддера выглядит так:
egl_display_ = eglGetDisplay(display_); if (egl_display_ == EGL_NO_DISPLAY) { LogLastEGLError(); FL_ERROR("Could not access EGL display."); return false; } if (eglInitialize(egl_display_, nullptr, nullptr) != EGL_TRUE) { LogLastEGLError(); FL_ERROR("Could not initialize EGL display."); return false; } if (eglBindAPI(EGL_OPENGL_ES_API) != EGL_TRUE) { LogLastEGLError(); FL_ERROR("Could not bind the ES API."); return false; }
// github.com (wayland_display.cc) — корректная реализация, т.е. помогла измененная очередность вызова функций.
Теперь, когда нюанс запуска улажен, мы рады увидеть заветное демо-окно приложения на экране :-).
2. Оптимизация производительности
Настало время проверить производительность. И, честно говоря, она нас не сильно порадовала в режиме отладки (debug mode). Что-то работало шустро, что-то — наоборот, имело большие просадки по фреймам и тормозило гораздо больше, чем что-то похожее на EFL+Node.js.
Мы немного расстроились и начали копать дальше. В SDK Flutter есть специальный режим компиляции машинного кода AOT, это даже не jit, а именно компиляция в нативный код со всеми сопутствующими оптимизациями, именно это подразумевается под по релиз-версией Flutter. Такой поддержки у нас в эмбеддере пока не было, добавляем.
Необходимы определенные инструкции, поданные аргументами к FlutterEngineRun
// полная реализация — github.com (elf.cc)
vm_snapshot_instructions_ = dlsym(fd, "_kDartVmSnapshotInstructions"); if (vm_snapshot_instructions_ == NULL) { error_ = strerror(errno); break; } vm_isolate_snapshot_instructions_ = dlsym(fd, "_kDartIsolateSnapshotInstructions"); if (vm_isolate_snapshot_instructions_ == NULL) { error_ = strerror(errno); break; } vm_snapshot_data_ = dlsym(fd, "_kDartVmSnapshotData"); if (vm_snapshot_data_ == NULL) { error_ = strerror(errno); break; } vm_isolate_snapshot_data_ = dlsym(fd, "_kDartIsolateSnapshotData"); if (vm_isolate_snapshot_data_ == NULL) { error_ = strerror(errno); break; }
if (vm_snapshot_data_ == NULL || vm_snapshot_instructions_ == NULL || vm_isolate_snapshot_data_ == NULL || vm_isolate_snapshot_instructions_ == NULL) { return false; } *vm_snapshot_data = reinterpret_cast < const uint8_t * > (vm_snapshot_data_); *vm_snapshot_instructions = reinterpret_cast < const uint8_t * > (vm_snapshot_instructions_); *vm_isolate_snapshot_data = reinterpret_cast < const uint8_t * > (vm_isolate_snapshot_data_); *vm_isolate_snapshot_instructions = reinterpret_cast < const uint8_t * > (vm_isolate_snapshot_instructions_);
FlutterProjectArgs args; // передаем все необходимое в args args.vm_snapshot_data = vm_snapshot_data; args.vm_snapshot_instructions = vm_snapshot_instructions; args.isolate_snapshot_data = vm_isolate_snapshot_data; args.isolate_snapshot_instructions = vm_isolate_snapshot_instructions;
Теперь, когда все есть, нужно собрать приложение особым образом, чтобы получить AOT-скомпилированный модуль под целевую платформу. Это можно сделать, выполнив команду из корня dart-проекта:
$HOST_ENGINE/dart-sdk/bin/dart \ --disable-dart-dev \ $HOST_ENGINE/gen/frontend_server.dart.snapshot \ --sdk-root $DEVICE_ENGINE}/flutter_patched_sdk/ \ --target=flutter \ -Ddart.developer.causal_async_stacks=false \ -Ddart.vm.profile=release \ -Ddart.vm.product=release \ --bytecode-options=source-positions \ --aot \ --tfa \ --packages .packages \ --output-dill build/tmp/app.dill \ --depfile build/kernel_snapshot.d \ package:lib/main.dart $DEVICE_ENGINE/gen_snapshot \ --deterministic \ --snapshot_kind=app-aot-elf \ --elf=build/lib/libapp.so \ --no-causal-async-stacks \ --lazy-async-stacks \ build/tmp/app.dill
Не нужно пугаться огромного количества параметров, большинство из них является стандартными. В частности:
-Ddart.vm.profile=release \
-Ddart.vm.product=release \
указывают на то, что нам не нужен профайлер в комплекте и у нас продуктовая версия.
output-dill
нужен для построения нативной библитеки libapp.so.
Самыми важными для нас являются пути $DEVICE_ENGINE и $HOST_ENGINE — два собранных движка под целевую (ARM) и хост-системы (x86_64) соответственно. Тут важно ничего не перепутать и убедиться, что libapp.so получается именно 32-битной ARM-версией:
$ file libapp.so libapp.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked
Запускаем и-и-и-и… вуаля! — все работает!
И работает шустрее значительно! Теперь уже можно говорить о сравнимой производительности и эффективности рендеринга с исходным приложением на базе набора библиотек EFL. Рендеринг работает почти без запинки и почти идеально на простых приложениях.
3. Подключение устройств ввода
В рамках этой статьи мы пропустим то, как подружили Wayland и эмбеддер с пультом, мышкой и другими устройствами ввода. На их реализацию можно посмотреть в исходниках эмбеддера.
4. Интерфейс на ТВ-приставке под Linux и Android и как увеличить производительность в 2—3 раза
Коснемся еще нескольких нюансов производительности, с которыми столкнулись в продуктовом UI-приложении. Нас очень обрадовала идентичность работы UI как на целевом устройстве, так и на Linux и Android. Уже сейчас Flutter может вполне может похвастаться очень гибкой портируемостью.
Еще отметим интересный опыт оптимизации самого dart-приложения под целевую платформу. Нас разочаровала довольно низкая производительность продуктового приложения (в отличии от демок). Мы взяли в руки профайлер и начали копать идовольно быстро обнаружили активное использование функций __brcm_cpu_dcache_flush и khrn_copy_8888_to_tf32 во время анимаций (на платформе используется чип процессора Broadcom/BCM ). Явно происходило какое-то очень жесткое пиксельное программное трансформирование или копирование во время анимаций. В итоге виновник был найден: в одной из панелей был задействован эффект размытия:
//... filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), //...
Комментирование одного этого эффекта дало увеличение производительности приложения в два—три раза и вывело приложение на стабильные 50-60 fps. Это хороший пример того, как один специфический эффект может уничтожить производительность во всем приложении на Flutter при том, что он был зайствован всего-лишь в одной панели, которая большую часть времени была скрыта.
Итого
В результате мы получили не просто работающее продуктовое приложение, а работающее приложение с качественным фреймрейтом на Flutter на целевом устройстве. Форк и наша версия эмбеддера под RDK и другие платформы на основе Wayland находится тут: github.com/DEgITx/flutter_wayland
Надеемся, опыт нашей команды в разработке и портировании ПО для ТВ-приставок и Smart TV пригодится вам в своих проектах и послужит отправной точкой для портирования Flutter на других устройствах.
[!?] Вопросы и комментарии приветствуются. На них будет отвечать автор статьи Алексей Касьянчук, наш инженер-программист
ссылка на оригинал статьи https://habr.com/ru/post/552588/
Добавить комментарий