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

Dgen: разрешение 800x600 и 60 FPS на Raspberry Pi

Сборка DGen в Raspberry Pi OS

В качестве эмулятора я выбрал DGen и на то есть две причины:

  1. Он очень прост для сборки и требует минимум зависимостей. При этом позволяет на этапе конфигурации выбрать: использовать или нет OpenGL (которого адекватно работающего на моей плате нет).
  2. В нём работает звук, а 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 эмулятором:

  1. Ничем не примечательный opcode_D_9 из кода ассемблера эмулятора давал 10% циклов. За счёт некого прерывания ядра.
  2. Функция 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 максимум.

DGen на Raspberry Pi: сравнение производительности в windows-mode и fullscreen

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

DGen на Raspberry Pi: без функции растягивания картинки

Всё те же максимальных 42 кадра в оконном режиме, что и были с 3 потоками ранее. Я позволил себе сделать выводы:

  1. Можно распараллелить выполнение фильтров DGen и получить прирост производительности в оконном режиме. В полноэкранном это не даёт эффекта.
  2. Bottleneck где-то ещё, судя по выводу top’a — в связке libSDL + X-сервер или DGen как-то неэффективно отправляет туда картинку.

На этом этапе я закончил эксперименты, погружаться глубже уже не хотелось. Ведь на глаз то проблема не ощущается.

Заключение

Компилируем, запускаем в разрешении 800×600, не страдаем ерундой и не знаем бед =)

Навигация по записям