Неделю назад я писал про эмулятор FCEU для NES для Raspberry Pi OS. Теперь я решил пойти несколько дальше и попробовать запустить игры для Sega Mega Drive на Raspberry Pi. Уточню для читающих: у меня в наличии вторая версия платы v1.1, компилировать буду под неё, но если убрать флаги оптимизации, то рецепт должен остаться рабочим для старших версий (3, 4, 400 или что там на момент прочтения выпустили).

Сборка DGen в Raspberry Pi OS
В качестве эмулятора я выбрал DGen и на то есть две причины:
- Он очень прост для сборки и требует минимум зависимостей. При этом позволяет на этапе конфигурации выбрать: использовать или нет OpenGL (которого адекватно работающего на моей плате нет).
- В нём работает звук, а 60 FPS (требуемых для USA образов Sega Mega Drive) можно получить в X-сервере для разрешения 800×600.
Это плюсы в сравнении с эмуляторами в репозитории Raspberry Pi OS: megnafen и т.д. Но, к сожалению, несмотря на все достоинства, DGen почему-то нет в репозитории, поэтому придётся собирать его самому.
Для начала нужно его скачать (я выбрал стабильную последнюю версию):
$ wget https://sourceforge.net/projects/dgen/files/dgen/1.33/dgen-sdl-1.33.tar.gz
Затем распаковать исходный код:
$ tar -xvf dgen-sdl-1.33.tar.gz
Теперь установим зависимости для сборки:
# apt install libsdl1.2-dev build-essential gcc g++
После этого перейдём в папку и сконфигурируем сборку:
$ CFLAGS="-O3 -mcpu=cortex-a7 -mtune=cortex-a7 -mfloat-abi=hard -mfpu=neon-vfpv4 -funsafe-math-optimizations" CXXFLAGS="-O3 -mcpu=cortex-a7 -mtune=cortex-a7 -mfloat-abi=hard -mfpu=neon-vfpv4 -funsafe-math-optimizations" ./configure --prefix=/usr --disable-opengl --without-doxygen --enable-joystick --disable-debug --enable-threads
Самоей важное тут: —disable-opengl, —enable-joystick и —enable-threads, которые отключают поддержку OpenGL, включают поддержку джойстиков и отдают отрисовку экрана в отдельный поток исполнения, соответственно. Остальные флаги не столь важны, они будут отключены по-умолчанию (—disable-debug) или включены в зависимости от установленных программ (—without-doxygen). CFLAGS и CXXFLAGS тут заданы исключительно под Raspberry Pi 2 v1.1. Для других версий можно использовать такую конфигурацию:
$ CFLAGS="-O3" CXXFLAGS="-O3" ./configure --prefix=/usr --disable-opengl --without-doxygen --enable-joystick --disable-debug --enable-threads
Можно подсмотреть какие-то флаги оптимизации тут. На самом деле, в данном случае они не дадут какого-то невероятного увеличения производительности, зато «мы сделали всё что могли».
В результате у меня получалось такое (должно быть схожим):
configure:
Front-end
OpenGL: no
Joystick: yes
Multi-threading: yes
Crap TV filters: yes
hqx filters: yes
scale2x filters: yes
Compressed ROMs: no
Debugger: no
dZ80 disassembler: no
Debugging: no
VDP debugging: no
Sega Pico: no
VGM dumping: no
CPU cores
Musashi M68K: yes
StarScream: no
Cyclone: yes
MZ80: yes
CZ80: yes
DrZ80: yes
ASM support (yes)
x86 ASM
MZ80: no
MMX memcpy: no
Crap TV filters: no
Tiles: no
ARM ASM
Cyclone: yes
DrZ80: yes
Doxygen documentation: no
Осталось скомпилировать и установить программу:
$ make
# make install
Готово, теперь можно почитать man DGen, посмотреть все опции и настройки, осознать всё, поправить конфигурационный файл по-своему желанию и запустить ROM:
$ man 5 dgenrc
$ dgen /path/to/rom.bin
2 попытки оптимизации, которые оказались практически бесполезны
Дальше будет небольшая история провала попытки оптимизации кода с целью улучшить производительность конкретно под Raspberry Pi 2 v1.1. Возможно, для более старших версий это будет иметь смысл, а у меня же особого эффекта это не дало.
Получив 60 FPS в разрешении 800×600 я решил не останавливаться на достигнутом: ведь я программист и я могу. В разрешении 1920×1080 я получил лишь 29 кадров в секунду из 60, а, значит, есть куда стремиться.
Я запустил DGen под perf и нашёл две функции с явным активным потребления CPU эмулятором:
- Ничем не примечательный opcode_D_9 из кода ассемблера эмулятора давал 10% циклов. За счёт некого прерывания ядра.
- Функция filter_stretch_X, растягивающее разрешение картинки под текущее разрешение давала 30% циклов.
Остальное же было из разряда курочка по зёрнышку клюёт: там 3%, тут 2%, вот и выходило в сумме 100%. Начал я с оптимизации первого пункта, благо сам компилятор подсказывал решение:
drz80/drz80.s: Assembler messages:
drz80/drz80.s:4288: swp{b} use is deprecated for ARMv6 and ARMv7
drz80/drz80.s:4290: swp{b} use is deprecated for ARMv6 and ARMv7
drz80/drz80.s:5327: swp{b} use is deprecated for ARMv6 and ARMv7
drz80/drz80.s:5329: swp{b} use is deprecated for ARMv6 and ARMv7
drz80/drz80.s:5331: swp{b} use is deprecated for ARMv6 and ARMv7
Разгадка в данном случае проста: ядро перехватывает ошибки во время исполнении инструкции SWP на архитектуре armv7 и обрабатывает её самостоятельно на программном уровне. Это и даёт искомый overhead, бегло просмотрев код я сделал вывод: инструкция SWP использовалась не ради атомарности операций, а для удобства программиста. Значит, её можно легко заменить без потери функционала, первый патч:
--- a/drz80/drz80.s
+++ b/drz80/drz80.s
@@ -4285,9 +4285,13 @@
;@EX AF,AF'
opcode_0_8:
add r1,cpucontext,#z80a2
- swp z80a,z80a,[r1]
+ ldr r0, [r1]
+ str z80a, [r1]
+ mov z80a, r0
add r1,cpucontext,#z80f2
- swp z80f,z80f,[r1]
+ ldr r0, [r1]
+ str z80f, [r1]
+ mov z80f, r0
fetch 4
;@ADD HL,BC
opcode_0_9:
@@ -5324,11 +5328,17 @@
;@EXX
opcode_D_9:
add r1,cpucontext,#z80bc2
- swp z80bc,z80bc,[r1]
+ ldr r0, [r1]
+ str z80bc, [r1]
+ mov z80bc, r0
add r1,cpucontext,#z80de2
- swp z80de,z80de,[r1]
+ ldr r0, [r1]
+ str z80de, [r1]
+ mov z80de, r0
add r1,cpucontext,#z80hl2
- swp z80hl,z80hl,[r1]
+ ldr r0, [r1]
+ str z80hl, [r1]
+ mov z80hl, r0
fetch 4
;@JP C,$+3
opcode_D_A:
Его можно применить в папке с исходным кодом с помощью команды:
$ patch -p1 < /path/to/patch/dgen-sdl-1.33-drz80-0001.patch
После этой модификации исходного кода я получил 32 вместо 29 FPS на разрешении 1920×1080.
Затем я попытался оптимизировать функцию filter_stretch_X, но это не давало должного эффекта — всё та же цифра 32. В итоге я распараллелил её выполнение на 3 потока, благо это было легко сделать быстро. Второй патч можно наложить с помощью команды:
$ patch -p1 < ~/tmp/dgen-sdl-1.33-filter-stretch-threads-0001.patch
Прошу не судить строго за код, там нет многих нужных проверок, есть откровенный copy-paste, пример китайского метода кодирования. В целом это плохой код, но это была просто проверка предположения, она не сработала и, соответственно, нужды что-то улучшать у меня нет. Однако, мало ли когда-то он мне пригодиться для старших плат, лучше сохраню тут.
И если на моём PC это позволило увеличить максимальную частоту работы эмулятора с 160 до 250 HZ, то на Raspberry Pi было лишь незначительное улучшение в оконном режиме, в полноэкранном всё те же 32 FPS максимум.

Затем я полностью убрал выполнение функции filter_stretch_X и запустил эмулятор ещё раз в оконном режиме:

Всё те же максимальных 42 кадра в оконном режиме, что и были с 3 потоками ранее. Я позволил себе сделать выводы:
- Можно распараллелить выполнение фильтров DGen и получить прирост производительности в оконном режиме. В полноэкранном это не даёт эффекта.
- Bottleneck где-то ещё, судя по выводу top’a — в связке libSDL + X-сервер или DGen как-то неэффективно отправляет туда картинку.
На этом этапе я закончил эксперименты, погружаться глубже уже не хотелось. Ведь на глаз то проблема не ощущается.
Заключение
Компилируем, запускаем в разрешении 800×600, не страдаем ерундой и не знаем бед =)