Хабрахабр

Портирование Quake3

В операционной системе Embox (разработчиком которой я являюсь) какое-то время назад появилась поддержка OpenGL, но толковой проверки работоспособности не было, только отрисовка сцен с несколькими графическими примитивами.

Я никогда особо не интересовался геймдевом, хотя, само собой, игры мне нравятся, и решил — вот хороший способ развлечься, а заодно проверить OpenGL и посмотреть, как игры взаимодействуют с ОС.

В этой статье я расскажу о том, как собирал и запускал Quake3 на Embox.

Для простоты будем называть ioquake3 просто квейком 🙂 Точнее, будем запускать не сам Quake3, а основанный на нём ioquake3, у которого тоже открытый исходный код.

Сразу оговорюсь, что в статье не анализируется сам исходный код Quake и его архитектура (про это можно почитать здесь, есть переводы на Хабре), а в этой статье речь пойдёт именно про то, как обеспечить запуск игры на новой операционной системе.

Оригинальные исходники можно найти в нашем репозитории. Приводимые в статье фрагменты кода упрощены для лучшего понимания: пропущены проверки на наличие ошибок, используется псевдокод и так далее.

Зависимости

Нам потребуются: Как ни странно, для сборки Quake3 нужно не так уж много библиотек.

  • POSIX + LibC — malloc() / memcpy() / printf() и так далее
  • libcurl — работа с сетью
  • Mesa3D — поддержка OpenGL
  • SDL — поддержка устройств ввода и аудио

Поэтому поддержка данных интерфейсов так или иначе есть практически во всех операционных системах, и в данном случае добавлять функционал практически не пришлось. С первым пунктом и так всё понятно — без этих функций сложно обойтись при разработке на C, и использование этих вызовов вполне ожидаемо. Вот с остальными пришлось разбираться.

libcurl

Для сборки libcurl достаточно libc (конечно, часть фич будет недоступна, но они и не потребуется). Это было самое простое. Сконфигурить и собрать эту библиотеку статически очень просто.

в Embox основным режимом является линковка в один образ, будем линковать всё статически. Обычно и приложения, и библиотеки линкуются динамически, но т.к.

В зависимости от используемой системы сборки, конкретные шаги будут отличаться, но смысл примерно такой:

wget https://curl.haxx.se/download/curl-7.61.1.tar.gz
tar -xf curl-7.61.1.tar.gz
cd curl-7.61.1
./configure --enable-static --host=i386-unknown-none -disable-shared
make
ls ./lib/.libs/libcurl.a # Вот с этим и будем линковаться

Mesa/OpenGL

Портирование такого большого фреймворка — тема отдельной статьи. Mesa — это фреймворк с открытым исходным кодом для работы с графикой, поддерживается ряд интерфейсов (OpenCL, Vulkan и прочие), но в данном случае нас интересует именно OpenGL. Ограничусь лишь тем, что в ОС Embox Mesa3D уже есть 🙂 Само собой, сюда подойдёт любая реализация OpenGL.

SDL

SDL — это кросс-платформенный фреймворк для работы с устройствами ввода, аудио и графикой.

Пока что забиваем всё, кроме графики, а для отрисовки кадров, напишем функции-заглушки, чтобы увидеть, когда они начнут вызываться.

0. Бэкэнды для работы с графикой задаются в SDL2-2. 8/src/video/SDL_video.c.

Выглядит это примерно так:

/* Available video drivers */
static VideoBootStrap *bootstrap[] = {
#if SDL_VIDEO_DRIVER_COCOA &COCOA_bootstrap,
#endif
#if SDL_VIDEO_DRIVER_X11 &X11_bootstrap,
#endif ...
}

Чтобы не заморачиваться с "нормальной" поддержкой новой платформы, просто добавим свой VideoBootStrap

Для простоты можно взять что-нибудь за основу, например src/video/qnx/video.c или src/video/raspberry/SDL_rpivideo.c, но для начала сделаем реализацию вообще почти пустой:

/* SDL_sysvideo.h */
typedef struct VideoBootStrap
{ const char *name; const char *desc;``` int (*available) (void); SDL_VideoDevice *(*create) (int devindex);
} VideoBootStrap; /* embox_video.c */ static SDL_VideoDevice *createDevice(int devindex)
return device;
} static int available() { return 1;
} VideoBootStrap EMBOX_bootstrap = { "embox", "EMBOX Screen", available, createDevice
};

Добавляем свой VideoBootStrap в массив:

/* Available video drivers */
static VideoBootStrap *bootstrap[] = { &EMBOX_bootstrap,
#if SDL_VIDEO_DRIVER_COCOA &COCOA_bootstrap,
#endif
#if SDL_VIDEO_DRIVER_X11 &X11_bootstrap,
#endif ...
}

Как и с libcurl, детали компиляции будут зависеть от конкретной системы сборки, но так или иначе нужно сделать примерно следующее: В принципе, на этом этапе уже можно компилировать SDL.

./configure --host=i386-unknown-none \ --enable-static \ --enable-audio=no \ --enable-video-directfb=no \ --enable-directfb-shared=no \ --enable-video-vulkan=no \ --enable-video-dummy=no \ --with-x=no make
ls build/.libs/libSDL2.a # Этот файл нам и нужен

Собираем сам Quake

Quake3 предполагает использование динамических библиотек, но мы будем линковать его статически, как и всё остальное.

Для этого выставим некоторые переменные в Makefile

CROSS_COMPILING=1
USE_OPENAL=0
USE_OPENAL_DLOPEN=0
USE_RENDERER_DLOPEN=0
SHLIBLDFLAGS=-static

Первый запуск

Для этого нужно его поставить (здесь и далее будут команды для Debian, для других дистрибутивов пакеты могут называться по-другому). Для простоты будем запускать на qemu/x86.

sudo apt install qemu-system-i386

И сам запуск:

qemu-system-i386 -kernel build/base/bin/embox -m 1024 -vga std -serial stdio

Однако при запуске Quake сразу получаем ошибку

> quake3
EXCEPTION [0x6]: error = 00000000
EAX=00000001 EBX=00d56370 ECX=80200001 EDX=0781abfd GS=00000010 FS=00000010 ES=00000010 DS=00000010
EDI=007b5740 ESI=007b5740 EBP=338968ec EIP=0081d370 CS=00000008 EFLAGS=00210202 ESP=37895d6d SS=53535353

Дебаг показал, что эта ошибка вызвана неполной поддержкой SIMD для x86 в QEMU: часть инструкций не поддерживается и генерирует исключение неизвестной команды (Invalid Opcode). Ошибка выводится не игрой, а операционной системой.

Патчим OpenLibm, чтобы __test_sse() не делала настоящую проверку на SSE, а просто считала, что поддержки нет. Происходит это не в самом Quake, а в OpenLibM (это библиотека, которую мы используем для реализации математических функций — sin(), expf() и тому подобных).

Перечисленных выше шагов хватает на запуск, в консоли виден такой вывод:

> quake3
ioq3 1.36 linux-x86_64 Nov 1 2018
SSE instruction set not available
----- FS_Startup -----
We are looking in the current search path:
//.q3a/baseq3
./baseq3 ---------------------- 0 files in pk3 files "pak0.pk3" is missing. Please copy it from your legitimate Q3 CDROM. Point Release files are missing. Please re-install the 1.32 point release. Also check that your ioq3 executable is in the correct place and that every file in the "baseq3 " directory is present and readable ERROR: couldn't open crashlog.txt

Как видно, ему не хватает файлов в директории baseq3. Уже неплохо, Quake3 пытается запуститься и даже выводит сообщение об ошибке! Заметьте, pak0.pk3 должен быть взять с лицензионного CD-диска (да, открытый исходный код не подразумевает бесплатное использование). Там содержатся звуки, текстуры и всякое такое.

Подготовка диска

sudo apt install qemu-utils # Создаём qcow2-образ
qemu-img create -f qcow2 quake.img 1G # Добавляем модуль nbd
sudo modprobe nbd max_part=63 # Форматируем qcow2-образ и пишем туда нужные файлы
sudo qemu-nbd -c /dev/nbd0 quake.img
sudo mkfs.ext4 /dev/nbd0
sudo mount /dev/nbd0 /mnt
cp -r path/to/q3/baseq3 /mnt
sync
sudo umount /mnt
sudo qemu-nbd -d /dev/nbd0

Теперь можно передавать блочное устройство в qemu

qemu-system-i386 -kernel build/base/bin/embox -m 1024 -vga std -serial stdio -hda quake.img

При старте системы замаунтим диск на /mnt и запустим quake3 в этой директории, на этот раз падает позже

> mount -t ext4 /dev/hda1 /mnt
> cd /mnt
> quake3
ioq3 1.36 linux-x86_64 Nov 1 2018
SSE instruction set not available
----- FS_Startup -----
We are looking in the current search path:
//.q3a/baseq3
./baseq3
./baseq3/pak8.pk3 (9 files)
./baseq3/pak7.pk3 (4 files)
./baseq3/pak6.pk3 (64 files)
./baseq3/pak5.pk3 (7 files)
./baseq3/pak4.pk3 (272 files)
./baseq3/pak3.pk3 (4 files)
./baseq3/pak2.pk3 (148 files)
./baseq3/pak1.pk3 (26 files)
./baseq3/pak0.pk3 (3539 files) ----------------------
4073 files in pk3 files
execing default.cfg
couldn't exec q3config.cfg
couldn't exec autoexec.cfg
Hunk_Clear: reset the hunk ok
Com_RandomBytes: using weak randomization
----- Client Initialization -----
Couldn't read q3history.
----- Initializing Renderer ----
-------------------------------
QKEY building random string
Com_RandomBytes: using weak randomization
QKEY generated
----- Client Initialization Complete -----
----- R_Init -----
tty]EXCEPTION [0xe]: error = 00000000
EAX=00000000 EBX=00d2a2d4 ECX=00000000 EDX=111011e0 GS=00000010 FS=00000010 ES=00000010 DS=00000010
EDI=0366d158 ESI=111011e0 EBP=37869918 EIP=00000000 CS=00000008 EFLAGS=00010212 ESP=006ef6ca SS=111011e0
EXCEPTION [0xe]: error = 00000000

На этот раз инструкции используются в виртуальной машине Quake3 для x86. Эта опять ошибка с SIMD в Qemu. После этого начинают вызываться наши функции для SDL, но, само собой, ничего не происходит, т.к. Проблема решилось заменой реализации для x86 на интерпретируемую ВМ (подробнее про виртуальную машину Quake3 и в принципе про архитектурные особенности можно почитать всё в той же статье). эти функции пока что ничего не делают.

Добавляем поддержку графики

static SDL_VideoDevice *createDevice(int devindex) { ... device->GL_GetProcAddress = glGetProcAddress; device->GL_CreateContext = glCreateContext; ...
} /* Здесь инициализируем OpenGL-контекст */
SDL_GLContext glCreateContext(_THIS, SDL_Window *window) { OSMesaContext ctx; /* Здесь делаем ОС-зависимую инициализацию -- мэпируем видеопамять и т.п. */ sdl_init_buffers(); /* Дальше инициализируем контекст Mesa */ ctx = OSMesaCreateContextExt(OSMESA_BGRA, 16, 0, 0, NULL); OSMesaMakeCurrent(ctx, fb_base, GL_UNSIGNED_BYTE, fb_width, fb_height); return ctx;
}

Второй хэндлер нужен для того, чтобы сказать SDL, какие функции вызывать при работе с OpenGL.

Для этого заводим массив и от запуска к запуску проверяем, каких вызовов не хватает, примерно так:

static struct { char *proc; void *fn;
} embox_sdl_tbl[] = { { "glClear", glClear }, { "glClearColor", glClearColor }, { "glColor4f", glColor4f }, { "glColor4ubv", glColor4ubv }, { 0 },
}; void *glGetProcAddress(_THIS, const char *proc) { for (int i = 0; embox_sdl_tbl[i].proc != 0; i++) { if (!strcmp(embox_sdl_tbl[i].proc, proc)) { return embox_sdl_tbl[i].fn; } } printf("embox/sdl: Failed to find %s\n", proc); return 0;
}

Благо, в Mesa есть все необходимые функции. За несколько перезапусков список становится достаточным полным, чтобы нарисовались заставка и меню. Единственное — почему-то нет функции glGetString(), вместо неё пришлось использовать _mesa_GetString().

Теперь при запуске приложения появляется заставка, ура!

Добавляем устройства ввода

Добавим поддержку клавиатуры и мыши в SDL.

Для работы с событиями нужно добавить хэндлер

static SDL_VideoDevice *createDevice(int devindex) { ... device->PumpEvents = pumpEvents; ...
}

Вешаем функцию на прерывание нажатия/отпускания клавиши. Начнём с клавиатуры. Эта функция должна запоминать событие (в простейшем случае, просто пишем в локальную переменную, по желанию можно использовать очереди), для простоты будем хранить только последнее событие.

static struct input_event last_event; static int sdl_indev_eventhnd(struct input_dev *indev) { /* Пока есть новые события, переписываем ими last_event */ while (0 == input_dev_event(indev, &last_event)) { }
}

Затем в pumpEvents() обрабатываем событие и передаём его в SDL:

static void pumpEvents(_THIS) { SDL_Scancode scancode; bool pressed; scancode = scancode_from_event(&last_event); pressed = is_press(last_event); if (pressed) { SDL_SendKeyboardKey(SDL_PRESSED, scancode); } else { SDL_SendKeyboardKey(SDL_RELEASED, scancode); }
}

Подробнее про коды клавиш и SDL_Scancode

В SDL используется свой enum для кодов клавиш, поэтому придётся преобразовать код клавиши ОС в код SDL.

Список этих кодов определяется в файле SDL_scancode.h

Например, ASCII-код преобразовать можно вот так (здесь не все ASCII-символы, но этих вполне хватит):

static int key_to_sdl[] = { [' '] = SDL_SCANCODE_SPACE, ['\r'] = SDL_SCANCODE_RETURN, [27] = SDL_SCANCODE_ESCAPE, ['0'] = SDL_SCANCODE_0, ['1'] = SDL_SCANCODE_1, ... ['8'] = SDL_SCANCODE_8, ['9'] = SDL_SCANCODE_9, ['a'] = SDL_SCANCODE_A, ['b'] = SDL_SCANCODE_B, ['c'] = SDL_SCANCODE_C, ... ['x'] = SDL_SCANCODE_X, ['y'] = SDL_SCANCODE_Y, ['z'] = SDL_SCANCODE_Z,
};

Кстати, примерно тут выяснилось, что где-то в обработке нажатия клавиш quake использует инструкции, не поддерживаемые QEMU, приходится переключиться на интерпретируюмую виртуальную машину с виртуальной машины для x86, для этого добавляем BASE_CFLAGS += -DNO_VM_COMPILED в Makefile. На этом c клавиатурой всё, остальным будут заниматься SDL и сам Quake.

Приятно удивило то, что всё отрисовывается как надо, хоть и с очень низким fps. После этого, наконец, можно торжественно "проскипать" заставки и даже запустить игру (закостылив некоторые error-ы 🙂 ).

Для прерываний мыши понадобится ещё один хэндлер, и обработку событий потребуется немного усложнить. Теперь можно приступить к поддержке мыши. Понятно, что аналогичным образом можно добавить правую клавишу, колёсико и т.п. Ограничимся только левой клавишей мыши.

static void pumpEvents(_THIS) { if (from_keyboard(&last_event)) { /* Здесь наш старый обработчик клавиатуры */ ... } else { /* Здесь будем обрабатывать события мыши */ if (is_left_click(&last_event)) { /* Зажата левая клавиша мыши */ SDL_SendMouseButton(0, 0, SDL_PRESSED, SDL_BUTTON_LEFT); } else if (is_left_release(&last_event)) { /* Отпущена левая клавиша мыши */ SDL_SendMouseButton(0, 0, SDL_RELEASED, SDL_BUTTON_LEFT); } else { /* Перемещение мыши */ SDL_SendMouseMotion(0, 0, 1, mouse_diff_x(), /* Сюда передаём горизонтальное смещение мыши */ mouse_diff_y()); /* Сюда передаём вертикальное смещение мыши */ } }
}

Фактически, этого уже достаточно для того, чтобы играть 🙂 После этого появляется возможность управлять камерой и стрелять, ура!

Оптимизация

Скорее всего, большая часть времени тратится на работу OpenGL (а он программный, и, более того, не используется SIMD), а реализация аппаратной поддержки — слишком долгая и сложная задача. Круто, конечно, что есть управление и какая-то графика, но такой FPS совсем никуда не годится.

Попытаемся ускорить игру "малой кровью".

Оптимизация компилятора и снижение разрешения

Собираем игру, все библиотеки и саму ОС с -O3 (если, вдруг, кто-то очитал до этого места, но не знает, что это за флаг — подробнее про флаги оптимизации GCC можно почитать здесь).

Кроме того, используем минимальное разрешение — 320х240, чтобы облегчить работу процессору.

KVM

Qemu поддерживает этот механизм, для его использования нужно сделать следующее. KVM (Kernel-based Virtual Machine) позволяет использовать аппаратную виртуализацию (Intel VT и AMD-V) для повышения производительности.

У меня материнка Gigabyte B450M DS3H, и AMD-V включается через M. Во-первых, нужно включить поддержку виртуализации в BIOS. T. I. -> Advanced Frequency Settings -> Advanced CPU Core Settings -> SVM Mode -> Enabled (Gigabyte, что с тобой не так?).

Затем ставим нужный пакет и добавляем соответствующий модуль

sudo apt install qemu-kvm
sudo modprobe kvm-amd # Или kvm-intel

Всё, теперь можно передавать qemu флаг -enable-kvm (или -no-kvm, чтобы не использовать аппаратное ускорение).

Итог

К сожалению, графика рисуется на CPU в один поток, ещё и без SIMD, из-за низкого fps (2-3 кадра в секунду) управлять очень неудобно. Игра запустилась, графика отображается как нужно, управление работает.

Может быть, в будущем получится запустить quake на платформе с аппаратным графическим ускорением, а пока останавливаюсь на том, что есть. Процесс портирования был интересным.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть