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


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


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


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


    Точнее, будем запускать не сам Quake3, а основанный на нём ioquake3, у которого тоже открытый исходный код. Для простоты будем называть 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 — это кросс-платформенный фреймворк для работы с устройствами ввода, аудио и графикой.


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


    Бэкэнды для работы с графикой задаются в SDL2-2.0.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)
    {
        SDL_VideoDevice *device;
    
        device = (SDL_VideoDevice *)SDL_calloc(1, sizeof(SDL_VideoDevice));
        if (device == NULL) {
            return NULL;
        }
    
        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
        ...
    }

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


    ./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

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


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


    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). upd: Как подсказал в комментах WGH, на самом деле проблема была в том, что я забыл явно включить поддержку SSE в cr0/cr4, так что с QEMU всё в порядке.


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


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


    > 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

    Уже неплохо, Quake3 пытается запуститься и даже выводит сообщение об ошибке! Как видно, ему не хватает файлов в директории baseq3. Там содержатся звуки, текстуры и всякое такое. Заметьте, 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

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


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


    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,
    };
    

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


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



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


    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()); /* Сюда передаём вертикальное смещение мыши   */
            }
        }
    }

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



    Оптимизация


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


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


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


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


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


    KVM


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


    Во-первых, нужно включить поддержку виртуализации в BIOS. У меня материнка Gigabyte B450M DS3H, и AMD-V включается через M.I.T. -> 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 на платформе с аппаратным графическим ускорением, а пока останавливаюсь на том, что есть.

    Embox

    117,00

    Открытая и свободная ОС для встроенных систем.

    Поделиться публикацией

    Похожие публикации

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

      Полагаю применённый вами позже -enable-kvm решает эту проблему и SSE можно возвращать.
        0

        Странно, но KVM тут не помогает. Ещё страннее, что в исходниках qemu сама инструкция (stmxcsr) упоминается и кажется, что и программно всё должно работать. Но что-то идёт не так. Проверил только что — на хосте инструкция исполняется без проблем.


        Затрудняюсь сказать, что именно идёт не так.

          +2
          ОС должна явно включить поддержку расширенных наборов команд (чтобы не было такого, что программа в юзерспейсе использует какой-нибудь zmm0, а ядро ОС не знает, что такой существует и его надо сохранять при переключении контекста). В embox это сделано?
            0
            Да, дело действительно в этом, как-то не пришло в голову. Спасибо! Добавил замечание в статью об этом косяке.

            На ARM для NEON у нас такое есть, а на x86 — нет.
        0
        я так первый Doom прошел на 386
        в смысле такой же fps был
        когда запустил на нормальном Пне, то заметил как он качается во время ходьбы.
        0
        А что значит «появилась поддержка Open GL», если его нельзя или так сложно использовать?
          0
          если его нельзя

          Можно, это видно на скринах в статье. Графика отрисовывается с помощью Mesa, Mesa реализует OpenGL API.


          так сложно использовать

          Так же, как и обычно — пишете программу, используя OpenGL API, и компилируете.


          Станет понятнее, если прочитать статью чуть дальше первого предложения. Непосредственному использованию самого OpenGL тут посвящено несколько предложений и два листинга кода по 20 строк.

            0
            Видимо неправильно сформулировал вопрос.
            Я понимаю, что у Вас есть поддержка OpenGL, и если использовать API, то все будет работать. Но ведь в Quake и так используется OpenGL API.
            Вы используете Mesa, а она как раз ориентирована для ускорения рендеринга, в том числе за счет использования аппаратного ускорения.
            Просто странно что у Вас по сути есть всё что нужно, но все же рендеринг программный.
              0

              В Mesa действительно есть хорошая поддержка аппаратного ускорения, но она не работает с аппаратурой напрямую, для её использования нужен слой DRM, который и взаимодействует с GPU (и которого у нас пока что нет). Если сконфигурить Mesa с драйвером swrast, она будет, грубо говоря, рисовать картинку в оперативной памяти, без задействования DRM, этот вариант и используется на данный момент.


              Реализовать аналог DRM из Linux достаточно сложно, работаем над этим. В данный момент делается драйвер для Vivante GPU на i.MX6. Получить простую анимацию уже получилось, а вот "подружить" драйвер с Mesa не выходит. Там достаточно много подводных камней, когда закончим м.б. накатаю статью по этой теме.

                +1
                Кстати, SDL поддерживает Vivante GPU на i.MX6 из коробки начиная с версии 2.0.4. Я эту поддержку добавил :)
                  0
                  Круто! Видели, что vivante есть, правда не проверяли как работает.
                  А можно Вам в личку вопросы по поводу акселератора для i.MX6 позадавать, а то при отсуствии документации, как то все очень медленно двигается. Простые сцены уже работают, но есть куча нюансов и костылей.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое