[Разработка под Android, Dart, Разработка под Linux, Производство и разработка электроники, Flutter] Как портировать SDK Flutter на ТВ-приставку для разработки и запуска приложений Android TV
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Недавно мы успешно портировали фреймворк 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.soFlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args, display /* userdata */, &engine_);где в качестве параметров идет передача настроек проекта (директория с собранным flutter bundle) FlutterProjectArgs args и аргументов рендеринга FlutterRendererConfig config. В первой структуре как раз задается путь bundle-пакета, собранного flutter-утилитой, а во второй используются контексты OpenGL .// пример использования на github.com/DEgITx/flutter_wayland/blob/master/src/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/DEgITx/flutter_wayland/blob/master/src/wayland_display.cc — корректная реализация, т.е. помогла измененная очередность вызова функций.Теперь, когда нюанс запуска улажен, мы рады увидеть заветное демо-окно приложения на экране :-).
2. Оптимизация производительностиНастало время проверить производительность. И, честно говоря, она нас не сильно порадовала в режиме отладки (debug mode). Что-то работало шустро, что-то — наоборот, имело большие просадки по фреймам и тормозило гораздо больше, чем что-то похожее на EFL+Node.js.Мы немного расстроились и начали копать дальше. В SDK Flutter есть специальный режим компиляции машинного кода AOT, это даже не jit, а именно компиляция в нативный код со всеми сопутствующими оптимизациями, именно это подразумевается под по релиз-версией Flutter. Такой поддержки у нас в эмбеддере пока не было, добавляем.Необходимы определенные инструкции, поданные аргументами к FlutterEngineRun// полная реализация — github.com/DEgITx/flutter_wayland/blob/master/src/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 на других устройствах.[!?] Вопросы и комментарии приветствуются. На них будет отвечать автор статьи Алексей Касьянчук, наш инженер-программист
===========
Источник:
habr.com
===========
Похожие новости:
- [Open source, Разработка под Linux, Софт] Линус Торвальдс остался недоволен рядом моментов в использовании Rust для Linux
- Компания Google представила патчи многоуровневого LRU для Linux
- Поддержка Rust для ядра Linux столкнулась с критикой Торвальдса
- [Разработка веб-сайтов, JavaScript, VueJS] Микрофронтенды: разделяй и властвуй
- [Разработка под Android, Смартфоны] В Android появится функция, которая напоминает не забываться в смартфоне при ходьбе
- [JavaScript, Программирование, ReactJS, Учебный процесс в IT] React: наглядное пособие для начинающих. Создаем свой компонент без знаний JavaScript (перевод)
- [Python, JavaScript, Программирование, Учебный процесс в IT] Ontol: подборка видео-лекций и каналов для продвинутых программистов
- [Разработка под iOS, Разработка мобильных приложений, Разработка под Android, Swift, Flutter] Онлайн митап по мобильной кросс-платформе 15 апреля
- [JavaScript, Медийная реклама] «Продам гараж»: фронт и реклама в hh.ru
- [Системное администрирование, IT-инфраструктура, Виртуализация, Хранение данных] Гиперконвергентная система AERODISK vAIR v2. Часть 1. Система виртуализации АИСТ
Теги для поиска: #_razrabotka_pod_android (Разработка под Android), #_dart, #_razrabotka_pod_linux (Разработка под Linux), #_proizvodstvo_i_razrabotka_elektroniki (Производство и разработка электроники), #_flutter, #_android_tv, #_prilozhenija_dlja_smart_tv (приложения для smart tv), #_flutter, #_cpp, #_bash, #_javascript, #_promwad, #_rdk, #_ott, #_linux, #_razrabotka_pod_android (
Разработка под Android
), #_dart, #_razrabotka_pod_linux (
Разработка под Linux
), #_proizvodstvo_i_razrabotka_elektroniki (
Производство и разработка электроники
), #_flutter
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:49
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Недавно мы успешно портировали фреймворк 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 flutter --local-engine-src-path PATH_TO_BUILDED_ENGINE_src
--local-engine=host_debug_unopt build bundle Важно! Сборка проекта должна происходить не на устройстве с собранной библиотекой, а на хостовой, т.е. x86_64!
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; } 2. Оптимизация производительностиНастало время проверить производительность. И, честно говоря, она нас не сильно порадовала в режиме отладки (debug mode). Что-то работало шустро, что-то — наоборот, имело большие просадки по фреймам и тормозило гораздо больше, чем что-то похожее на EFL+Node.js.Мы немного расстроились и начали копать дальше. В SDK Flutter есть специальный режим компиляции машинного кода AOT, это даже не jit, а именно компиляция в нативный код со всеми сопутствующими оптимизациями, именно это подразумевается под по релиз-версией Flutter. Такой поддержки у нас в эмбеддере пока не было, добавляем.Необходимы определенные инструкции, поданные аргументами к FlutterEngineRun// полная реализация — github.com/DEgITx/flutter_wayland/blob/master/src/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; $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 //...
filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), //... =========== Источник: habr.com =========== Похожие новости:
Разработка под Android ), #_dart, #_razrabotka_pod_linux ( Разработка под Linux ), #_proizvodstvo_i_razrabotka_elektroniki ( Производство и разработка электроники ), #_flutter |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:49
Часовой пояс: UTC + 5