Гоняем GTA: Vice City на беспроводном роутере TP-Link TL-WDR4900.
❯ Что это такое?
Это беспроводной роутер TP-Link, оснащённый внешним графическим процессором AMD Radeon GPU. Он подключается через PCIe, работает под Debian Linux, и на этом роутере можно играть в игры:
❯ В чём изюминка этого роутера?
TP-LINK’s TL-WDR4900 v1 — это очень интересный WiFi-роутер. Вместо типичных ЦП MIPS или ARM, устанавливаемых в обычных WiFi-роутерах, в WDR4900 стоит ЦП на базе PowerPC от NXP.
ЦП NXP/Freescale QorIQ P1014, используемый в WDR4900 — это 32-битный процессор PowerPC.
В таких ЦП предоставляется полноценное 36-разрядное адресное пространство, и этот процессор отличается высокой производительностью (для роутера 2013 года выпуска), и в нём установлены отличные контроллеры PCIe.
Они быстро приобрели популярность в сообществах OpenWrt и Freifunk, что неудивительно для дешёвого роутера, на котором стоит такой высокопроизводительный ЦП. WiFi-чипсеты с частотами 2,4 ГГц и 5 ГГц (производства Qualcomm/Atheros) подключаются к ЦП через PCIe.
❯ Проблемы PCIe во встраиваемых системах
Платы PCIe прозрачно отображаются на пространство памяти ЦП хоста. Контроллер PCIe на ЦП хоста затем отправляет все обращения, затрагивающие конкретную область памяти, именно на то PCIe-устройство, которое отвечает за эту область памяти.
На каждой PCIe-плате может предусматриваться сразу несколько таких отображений, также именуемых “BAR” (регистры для назначения базовых адресов). Максимальный размер таких отображений от ЦП к ЦП варьируется.
Ранее даже обычный CM4 для Raspberry Pi мог выделить под графические карты всего 64 МиБиБ своего адресного пространства.
Многие другие устройства (например, ЦП роутеров на основе MIPS) ограничиваются всего 32 МиБиБ (или менее).
В принципе, для всех современных графических карт требуется, чтобы пространство BAR-адресов на хост-системе составляло минимум 128 МиБиБ – столько нужно для коммуникации с драйвером. Даже на сравнительно новых картах, в частности, Intel ARC требуется “Resizable BAR” — это маркетинговый термин для обозначения очень больших 64-разрядных областей памяти. Такие карты позволяют отобразить всю VRAM (порядка 12+ ГиБиБ) на пространство памяти хоста.
Даже при наличии достаточного BAR-пространства, память устройства PCIe может работать не совсем так, как обычная память (например, на ЦП с архитектурой x86). Именно поэтому возникали многочисленные проблемы при попытках подключить GPU к Raspberry Pi.
Схожие проблемы (касающиеся упорядочивания памяти/кэширования/карт nGnRE/выравнивания) возникают даже на больших серверных процессорах Arm64, и из-за этого приходится грубо ломать ядро и выдумывать такие обходные пути, как, например:
❯ Дооборудуем слот miniPCIe
В заводской комплектации роутер не обеспечивает какой-либо соединяемости с внешними устройствами через PCIe. Для подключения к графической карте была разработана собственная печатная плата-переходник miniPCIe, подключаемая к роутеру медным эмалированным проводом:
Дорожки PCIe, ведущие от ЦП к одному из чипсетов Atheros пришлось обрезать и перенаправить в слот miniPCIe.
U-boot сообщает, что PCIe2 подключён к графической карте AMD Radeon HD 7470:
U-Boot 2010.12-svn19826 (Apr 24 2013 - 20:01:21)
CPU: P1014, Version: 1.0, (0x80f10110)
Core: E500, Version: 5.1, (0x80212151)
Clock Configuration:
CPU0:800 MHz,
CCB:400 MHz,
DDR:333.333 MHz (666.667 MT/s data rate) (Asynchronous), IFC:100 MHz
L1: D-cache 32 kB enabled
I-cache 32 kB enabled
Board: P1014RDB
SPI: ready
DRAM: 128 MiB
L2: 256 KB enabled
Using default environment
PCIe1: Root Complex of mini PCIe Slot, x1, regs @ 0xffe0a000
01:00.0 - 168c:abcd - Network controller
PCIe1: Bus 00 - 01
PCIe2: Root Complex of PCIe Slot, x1, regs @ 0xffe09000
03:00.0 - 1002:6778 - Display controller
03:00.1 - 1002:aa98 - Multimedia device
PCIe2: Bus 02 - 03
In: serial
Out: serial
Err: serial
Net: initialization for Atheros AR8327/AR8328
eTSEC1
auto update firmware: is_auto_upload_firmware = 0!
Autobooting in 1 seconds
=>
❯ Установка Debian Linux
Установив на роутер OpenWrt, мы сразу обзаводимся действующим ядром и пользовательским пространством, но пользовательское пространство в OpenWrt получается довольно ограниченным (busybox, musl libc, никаких библиотек для графики/игр, т. д.).
Также при работе с устанавливаемым по умолчанию ядром OpenWrt нам не хватало графических драйверов AMD. Проблему с драйверами удалось решить, скомпилировав собственное дерево OpenWrt, в котором мы включили дополнительные модули. Затем загрузили это ядро через TFTP прямо из u-boot:
setenv ipaddr 10.42.100.4
tftpboot 0x2000000 10.42.100.60:wdr4900-nfs-openwrt.bin
bootm 0x2000000
К счастью, в архитектуре Debian Linux как раз на этот случай предусмотрен порт “PowerPCSPE”, рассчитанный на ЦП именно такого типа (e500/e500v2). На системе со статически компилируемыми пользовательскими двоичными файлами QEMU и с правильно настроенными обработчиками binfmt можно воспользоваться инструментом debootstrap из арсенала Debian: он позволяет создавать на основе зеркал загрузочный участок пользовательского пространства:
sudo QEMU_CPU=e500v2 debootstrap --exclude=usr-is-merged --arch=powerpcspe --keyring ~/gamingrouter/debian-ports-archive-keyring-removed.gpg unstable "$TARGET" https://snapshot.debian.org/archive/debian-ports/20190518T205337Z/
debootstrap через chroot зайдёт в новоиспечённую файловую систему рута и просто выполнит двоичные файлы (post-install hooks, etc). Эта работа явно выполняется qemu-user-static, отвечающим за выполнение двоичных файлов PowerPCSPE на этой хост-машине amd64. Из дополнительной переменной окружения QEMU_CPU=e500v2 среда QEMU узнаёт, какой именно ЦП эмулировать CPU.
❯ GPU amdgpu (современный AMD)
Первые эксперименты мы ставили на графическом процессоре AMD Radeon RX570 GPU, воспользовавшись современным графическим драйвером amdgpu. В результате возникли очень странные артефакты и пока – никакого (нормального) изображения:
Немного позанимавшись отладкой и, наконец, установив на другом компьютере 32-разрядный x86 (i386) Linux, мы заметили, что такая же проблема возникает на любой другой 32-разрядной платформе, даже на обычных Intel ПК. По-видимому, в amdgpu есть какая-то несовместимость с 32-разрядными платформами.
Мы открыли обсуждение по этому багу, но что-то его пока не очень активно разбирают.
❯ GPU radeon (унаследованный AMD)
Но с картой AMD Radeon HD 7470, использующей более старинный драйвер radeon всё вдруг начало работать:
❯ Проблемы с порядком от старшего к младшему
Для этой платформы мы скомпилировали reVC (собранная методом обратной разработки версия GTA Vice City, исходный код которой выложен в открытом доступе). Для этого потребовалось подготовить собственные сборки premake, glfw3, glew и reVC как таковой.
root@gaming-router:/home/user/GTAVC# ./reVC
Segmentation fault
Упс :)
Надо ещё поработать. Оказывается, сама игра и рендеринговый движок (как минимум, в декомпилированной версии) вообще не приспособлены к работе от старшего к младшему. При загрузке игровых ресурсов приходится загружать прямо в память структуры (в которых содержатся смещения, размеры, числа, координаты, т. д.). Данные в этих структурах нумеруются от младшего к старшему, и они оказываются на платформе, которая работает по принципу от старшего к младшему. Из-за этого игра пытается обращаться к памяти с абсурдными смещениями и почти сразу падает.
Мы несколько дней потратили на пропатчивание игры и рендерингового движка librw, чтобы этот код нормально работал на машинах с нумерацией от старшего к младшему. В исходном коде нашлось более 100 механизмов, которые требовалось пропатчить, патчи выглядели примерно так:
@@ -118,6 +136,7 @@ RwTexDictionaryGtaStreamRead1(RwStream *stream)
assert(size == 4);
if(RwStreamRead(stream, &numTextures, size) != size)
return nil;
+ numTextures = le32toh(numTextures);
texDict = RwTexDictionaryCreate();
if(texDict == nil)
@@ -458,8 +477,8 @@ CreateTxdImageForVideoCard()
RwStreamWrite(img, buf, num);
}
- dirInfo.offset = pos / CDSTREAM_SECTOR_SIZE;
- dirInfo.size = size;
+ dirInfo.offset = htole32(pos / CDSTREAM_SECTOR_SIZE);
+ dirInfo.size = htole32(size);
strncpy(dirInfo.name, filename, sizeof(dirInfo.name));
pDir->AddItem(dirInfo);
CStreaming::RemoveTxd(i);
После того, как игра успевала загрузить некоторые ресурсы при помощи RwStreamRead(), а данные, загруженные в структуры, требовалось конвертировать из порядка нумерации от младшего к старшему в порядок нумерации, принятый на хосте.
При таких операциях как сохранение игр, настроек, т. д., требовалось оснастить обратным механизмом, который всегда обеспечивал сохранение в порядке от младшего к старшему.
Теперь нам вполне удавалось загружать игру, осматривать мир, ездить на машине. Но при попытке отобразить персонажа возникают очень странные графические глитчи.
❯ Глитчи с моделью игрока
Внимание: картинка сильно рябит
В следующем видео много проблескивающих элементов/глитчей. Если у вас эпилепсия или могут начаться судороги от реакции на свет или другие раздражители, пожалуйста, не смотрите этот ролик.
Когда были отключены все главные и второстепенные персонажи, видимых глитчей не стало. Всё работало нормально, в игру вполне можно было играть (настолько, насколько можно поиграть без второстепенных персонажей).
Ещё несколько дней мы потратили на поиски бага в нашем коде. Очевидно, мы допустили какую-то ошибку, реализуя поддержку порядка от старшего к младшему. Все применимые переменные, координаты, вершины, преобразования выводились в дамп как числа и сравнивались с той версией игры, где действовал порядок от младшего к старшему.
Теперь всё выглядело совершенно нормально, и больше никаких проблем мы найти не смогли.
В таком состоянии проект застыл на несколько месяцев.
❯ Порт Wii U
Нам удалось найти в Интернете другой порт для reVC: the Wii U. Wii U использует ЦП IBM Espresso, это процессор на основе PowerPC, точно как и наш. Он также работает в порядке от старшего к младшему.
Мы связались с Гэри, автором этого порта Wii U, и очень-очень вежливо поинтересовались, можно ли взглянуть на исходный код, пропатченный под порядок от старшего к младшему. Гэри, спасибо ещё раз!
Пересадив патчи Гэри в обычную базу кода reVC (отказавшись от всех Wii U-специфичных изменений), мы смогли запустить reVC на TP-Link при помощи хорошо изученных патчей Гэри…
И начались всё те же повреждения графики, что и раньше. Да что же это такое?!
На данном этапе мы повсюду искали ответ, ставя под сомнение и пытаясь проверить, разумно ли сделана ровно каждая часть системы: ядро, драйверы GPU, компиляторы и библиотеки.
PowerPC SPE — это не самая распространённая архитектура (её даже удалили из GCC 9), с очень необычными расширениями чисел с плавающей точкой (эта архитектура очень отличается от обычных ЦП PowerPC).
Отключили spe (-mno-spe) и переключились на программную модель чисел с плавающей точкой, переключились на e500, e500v2 в качестве целевых платформ компиляции, т. д. — ничего не поменялось.
❯ Тест i386
Чтобы убедиться, что код не сломан, мы подключили тот же GPU к машине с x86 (надёжный ThinkPad T430, через ExpressCard 34). Установили ту же самую версию Debian 10, те же библиотеки, тот же драйвер radeon, ту же прошивку и скомпилировали тот же самый исходный код reVC для i386.
Игра работала отлично, никаких графических дефектов не наблюдалось.
❯ Современное ядро LLVM
На данном этапе мы захотели опробовать более новое ядро (с более новыми драйверами radeon). GCC прекратила поддержку PowerPC SPE, поэтому собрать современный Linux 6.7 под GCC 8 не выйдет. Но в LLVM/clang только что появилась поддержка PowerPC SPE, Linux также можно собирать при помощи clang.
make LLVM=1 ARCH=powerpc OBJCOPY="~/binutils-2.42/build/binutils/objcopy" all -j 40 V=1
mkimage -C none -a 0x1200000 -e 0x1200000 -A powerpc -d arch/powerpc/boot/simpleImage.tl-wdr4900-v1 uImage12-nvme
Нам потребовалось предоставить нашу собственную версию binutils/objcopy (с поддержкой PowerPC) и ld.
Прочие изменения, которые потребовалось внести в TP-Link WDR4900 с ядром из основной ветки оказались совсем небольшими:
diff --git a/arch/powerpc/boot/Makefile b/arch/powerpc/boot/Makefile
index 968aee202..5ce3eeb09 100644
--- a/arch/powerpc/boot/Makefile
+++ b/arch/powerpc/boot/Makefile
@@ -181,6 +181,7 @@ src-plat-$(CONFIG_PPC_PSERIES) += pseries-head.S
src-plat-$(CONFIG_PPC_POWERNV) += pseries-head.S
src-plat-$(CONFIG_PPC_IBM_CELL_BLADE) += pseries-head.S
src-plat-$(CONFIG_MVME7100) += motload-head.S mvme7100.c
+src-plat-$(CONFIG_TL_WDR4900_V1) += simpleboot.c fixed-head.S
src-plat-$(CONFIG_PPC_MICROWATT) += fixed-head.S microwatt.c
@@ -351,7 +352,7 @@ image-$(CONFIG_TQM8548) += cuImage.tqm8548
image-$(CONFIG_TQM8555) += cuImage.tqm8555
image-$(CONFIG_TQM8560) += cuImage.tqm8560
image-$(CONFIG_KSI8560) += cuImage.ksi8560
-
+image-$(CONFIG_TL_WDR4900_V1) += simpleImage.tl-wdr4900-v1
# Board ports in arch/powerpc/platform/86xx/Kconfig
image-$(CONFIG_MVME7100) += dtbImage.mvme7100
diff --git a/arch/powerpc/boot/wrapper b/arch/powerpc/boot/wrapper
index 352d7de24..414216454 100755
--- a/arch/powerpc/boot/wrapper
+++ b/arch/powerpc/boot/wrapper
@@ -345,6 +345,11 @@ adder875-redboot)
platformo="$object/fixed-head.o $object/redboot-8xx.o"
binary=y
;;
+simpleboot-tl-wdr4900-v1)
+ platformo="$object/fixed-head.o $object/simpleboot.o"
+ link_address='0x1000000'
+ binary=y
+ ;;
simpleboot-*)
platformo="$object/fixed-head.o $object/simpleboot.o"
binary=y
diff --git a/arch/powerpc/kernel/head_85xx.S b/arch/powerpc/kernel/head_85xx.S
index 39724ff5a..80da35f85 100644
--- a/arch/powerpc/kernel/head_85xx.S
+++ b/arch/powerpc/kernel/head_85xx.S
@@ -968,7 +968,7 @@ _GLOBAL(__setup_ehv_ivors)
_GLOBAL(__giveup_spe)
addi r3,r3,THREAD /* want THREAD of task */
lwz r5,PT_REGS(r3)
- cmpi 0,r5,0
+ PPC_LCMPI 0,r5,0
SAVE_32EVRS(0, r4, r3, THREAD_EVR0)
evxor evr6, evr6, evr6 /* clear out evr6 */
evmwumiaa evr6, evr6, evr6 /* evr6 <- ACC = 0 * 0 + ACC */
diff --git a/arch/powerpc/platforms/85xx/Kconfig b/arch/powerpc/platforms/85xx/Kconfig
index 9315a3b69..86ba4b5e4 100644
--- a/arch/powerpc/platforms/85xx/Kconfig
+++ b/arch/powerpc/platforms/85xx/Kconfig
@@ -176,6 +176,18 @@ config STX_GP3
select CPM2
select DEFAULT_UIMAGE
+config TL_WDR4900_V1
+ bool "TP-Link TL-WDR4900 v1"
+ select DEFAULT_UIMAGE
+ select ARCH_REQUIRE_GPIOLIB
+ select GPIO_MPC8XXX
+ select SWIOTLB
+ help
+ This option enables support for the TP-Link TL-WDR4900 v1 board.
+
+ This board is a Concurrent Dual-Band wireless router with a
+ Freescale P1014 SoC.
+
config TQM8540
bool "TQ Components TQM8540"
help
diff --git a/arch/powerpc/platforms/85xx/Makefile b/arch/powerpc/platforms/85xx/Makefile
index 43c34f26f..55268278d 100644
--- a/arch/powerpc/platforms/85xx/Makefile
+++ b/arch/powerpc/platforms/85xx/Makefile
@@ -26,6 +26,7 @@ obj-$(CONFIG_TWR_P102x) += twr_p102x.o
obj-$(CONFIG_CORENET_GENERIC) += corenet_generic.o
obj-$(CONFIG_FB_FSL_DIU) += t1042rdb_diu.o
obj-$(CONFIG_STX_GP3) += stx_gp3.o
+obj-$(CONFIG_TL_WDR4900_V1) += tl_wdr4900_v1.o
obj-$(CONFIG_TQM85xx) += tqm85xx.o
obj-$(CONFIG_PPA8548) += ppa8548.o
obj-$(CONFIG_SOCRATES) += socrates.o socrates_fpga_pic.o
diff --git a/arch/powerpc/platforms/Kconfig.cputype b/arch/powerpc/platforms/Kconfig.cputype
index b2d8c0da2..21bc5f06b 100644
--- a/arch/powerpc/platforms/Kconfig.cputype
+++ b/arch/powerpc/platforms/Kconfig.cputype
@@ -272,7 +272,7 @@ config TARGET_CPU
default "e300c2" if E300C2_CPU
default "e300c3" if E300C3_CPU
default "G4" if G4_CPU
- default "8540" if E500_CPU
+ default "8548" if E500_CPU
default "e500mc" if E500MC_CPU
default "powerpc" if POWERPC_CPU
В результате получилось пригодное к загрузке ядро. Никаких графических дефектов снова не возникло. Оказалось, что это очень приятно – полностью избавиться от инструментария OpenWrt.
❯ qemu-user-static с применением llvmpipe
Чтобы немного упростить отладку, мы скопировали root-файловую систему на локальный компьютер с amd64 (воспользовавшись qemu-user-static again) и сконфигурировали X-сервер так, чтобы он работал с формальным/виртуальным монитором. Затем подключили к системе x11vnc, чтобы можно было смотреть в этот формальный монитор.
Section "Device"
Identifier "Configured Video Device"
Driver "dummy"
VideoRam 256000
EndSection
Section "Monitor"
Identifier "Configured Monitor"
HorizSync 60.0 - 1000.0
VertRefresh 60.0 - 200.0
ModeLine "640x480" 23.75 640 664 720 800 480 483 487 500 -hsync +vsync
# "1920x1080" 148.50 1920 2448 2492 2640 1080 1084 1089 1125 +Hsync +Vsync
EndSection
Section "Screen"
Identifier "Default Screen"
Monitor "Configured Monitor"
Device "Configured Video Device"
DefaultDepth 24
SubSection "Display"
Depth 24
Modes "640x480"
EndSubSection
EndSection
Внутри chroot (при QEMU_CPU установленном для e500v2), мы запустили Xorg, x11vnc и, наконец, reVC:
export LIBGL_ALWAYS_SOFTWARE=true
export GALLIUM_DRIVER=llvmpipe
export DISPLAY=:2
Xorg -config /etc/xorg.conf :2 &
x11vnc -display :2 &
xrandr --output default --mode "800x600"
/home/user/GTAVC/reVC
… притом, что этот механизм работает абсурдно медленно (1 кадр примерно в ~20 с), всё сработало. Работало даже с моделями игроков, без каких-либо графических нарушений. Основные отличия получились такими:
- QEMU эмулирует ЦП, а не реальное аппаратное обеспечение;
- llvmpipe вместо radeon / r600.
Затем установили GALLIUM_DRIVER=llvmpipe на реальном железе. Из-за этого производительность ухудшилась ещё сильнее (примерно по 1 кадру в минуту!), но всё работало!
Никаких графических дефектов заметно не было (хотя, пришлось почти целый час дожидаться погружения в игру…).
❯ Обновление mesa
Затем мы взялись обновить mesa на роутере. Для этого также потребовалось обновить ряд зависимостей. cmake, libglvnd, meson, drm и, наконец, mesa пришлось собирать с нуля, код брали либо прямо с git, либо из последнего релиза.
После установки новых libglvnd, drm и mesa, отображение персонажей нормально заработало и на реальном железе (с ускорением!). Мы до сих пор не выявили реальную причину проблемы (и в какой библиотеке дело), но остались более чем довольны тем, как в итоге удалось решить эту проблему.
Результат на видео.
Автор текста: Manawyrm и tSYS |
Автор перевода: GeeksCat
Переведено специально для блога CatGeek и читателей Дзена. Подписывайтесь на наш канал, чтобы не пропустить новые интересные посты.
Также подписывайтесь на наш телеграмм-канал — только здесь, технично, информативно и с юмором об IT, технике и электронике. Будет интересно.
Также не пропустите наш техно-блог на Хабре — Timeweb Cloud.
Облачные сервисы Timeweb Cloud — это реферальная ссылка, которая может помочь поддержать наши проекты.