
Dans le système d'exploitation Embox (dont je suis le développeur), la prise en charge d'OpenGL est apparue il y a quelque temps, mais il n'y a pas eu de vérification des performances sensible, uniquement le rendu des scènes avec plusieurs primitives graphiques.
Je n'ai jamais été particulièrement intéressé par gamedev, bien que, bien sûr, j'aime les jeux, et j'ai décidé - c'est un bon moyen de s'amuser, mais en même temps, vérifiez OpenGL et voyez comment les jeux interagissent avec le système d'exploitation.
Dans cet article, je vais vous expliquer comment créer et exécuter Quake3 sur Embox.
Plus précisément, nous n'exécuterons pas Quake3 lui-même, mais ioquake3 basé sur lui, qui a également du code open source. Pour simplifier, nous appellerons ioquake3 juste quake :)
Je réserve tout de suite que l'article n'analyse pas le code source de Quake et son architecture (vous pouvez le lire ici , il y a des traductions sur Habré ), et cet article se concentrera sur la façon de s'assurer que le jeu est lancé sur le nouveau système d'exploitation.
Les fragments de code présentés dans l'article sont simplifiés pour une meilleure compréhension: les vérifications d'erreurs sont omises, un pseudo-code est utilisé, etc. Les sources originales peuvent être trouvées dans notre référentiel .
Dépendances
Curieusement, peu de bibliothèques sont nécessaires pour construire Quake3. Nous aurons besoin de:
- POSIX + LibC -
malloc()
/ memcpy()
/ printf()
et ainsi de suite - libcurl - mise en réseau
- Mesa3D - Prise en charge d'OpenGL
- SDL - prise en charge des périphériques d'entrée et audio
Avec le premier paragraphe, et donc tout est clair - sans ces fonctions, il est difficile de le faire lors du développement en C, et l'utilisation de ces appels est très attendue. Par conséquent, la prise en charge de ces interfaces est en quelque sorte disponible dans presque tous les systèmes d'exploitation et, dans ce cas, il n'était pratiquement pas nécessaire d'ajouter des fonctionnalités. Je devais m'occuper du reste.
libcurl
C'était le plus simple. Libc suffit pour construire libcurl (bien sûr, certaines fonctionnalités ne seront pas disponibles, mais elles ne seront pas nécessaires). La configuration et la construction de cette bibliothèque sont statiquement très simples.
Habituellement, les applications et les bibliothèques sont liées dynamiquement, mais parce que dans Embox, le mode principal est la liaison dans une image, nous lierons tout statiquement.
Selon le système de construction utilisé, les étapes spécifiques varieront, mais la signification est quelque chose comme ceci:
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 est un framework open source pour travailler avec des graphiques, il prend en charge un certain nombre d'interfaces (OpenCL, Vulkan et autres), mais dans ce cas, nous sommes intéressés par OpenGL. Le portage d'un tel framework est le sujet d'un article séparé. Je me limiterai seulement à ce qu'Embox Mesa3D a déjà :) Bien sûr, toute implémentation OpenGL convient ici.
Sdl
SDL est un framework multiplateforme pour travailler avec des périphériques d'entrée, audio et graphiques.
Pour l'instant, nous allons tout marteler, sauf les graphiques, et pour dessiner des cadres, nous allons écrire des fonctions de stub pour voir quand elles commencent à être appelées.
Les backends pour travailler avec les graphiques sont définis dans SDL2-2.0.8/src/video/SDL_video.c
.
Cela ressemble à ceci:
static VideoBootStrap *bootstrap[] = { #if SDL_VIDEO_DRIVER_COCOA &COCOA_bootstrap, #endif #if SDL_VIDEO_DRIVER_X11 &X11_bootstrap, #endif ... }
Afin de ne pas vous embêter avec le support "normal" de la nouvelle plateforme, ajoutez simplement votre VideoBootStrap
Pour plus de simplicité, vous pouvez prendre quelque chose comme base, par exemple src/video/qnx/video.c
ou src/video/raspberry/SDL_rpivideo.c
, mais nous allons d'abord rendre l'implémentation presque vide:
typedef struct VideoBootStrap { const char *name; const char *desc;``` int (*available) (void); SDL_VideoDevice *(*create) (int devindex); } VideoBootStrap; 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 };
Ajoutez votre VideoBootStrap
au tableau:
static VideoBootStrap *bootstrap[] = { &EMBOX_bootstrap, #if SDL_VIDEO_DRIVER_COCOA &COCOA_bootstrap, #endif #if SDL_VIDEO_DRIVER_X11 &X11_bootstrap, #endif ... }
Fondamentalement, à ce stade, vous pouvez déjà compiler SDL. Comme avec libcurl, les détails de la compilation dépendront du système de construction particulier, mais vous devez en quelque sorte faire quelque chose comme ceci:
./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
Nous collectons Quake lui-même
Quake3 implique l'utilisation de bibliothèques dynamiques, mais nous allons le lier statiquement, comme tout le reste.
Pour ce faire, définissez certaines variables dans le Makefile
CROSS_COMPILING=1 USE_OPENAL=0 USE_OPENAL_DLOPEN=0 USE_RENDERER_DLOPEN=0 SHLIBLDFLAGS=-static
Premier lancement
Pour plus de simplicité, nous allons exécuter sur qemu / x86. Pour ce faire, vous devez l'installer (ici et ci-dessous, il y aura des commandes pour Debian, car les autres packages de distribution peuvent être appelés différemment).
sudo apt install qemu-system-i386
Et le lancement lui-même:
qemu-system-i386 -kernel build/base/bin/embox -m 1024 -vga std -serial stdio
Cependant, au démarrage de Quake, nous obtenons immédiatement une erreur
> 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
L'erreur n'est pas affichée par le jeu, mais par le système d'exploitation. Debag a montré que cette erreur était due à une prise en charge SIMD incomplète pour x86 dans QEMU: une partie des instructions n'est pas prise en charge et lève une exception de commande inconnue (Opcode invalide). upd: Comme suggéré par WGH dans les commentaires, le vrai problème était que j'avais oublié d'activer explicitement le support SSE dans cr0 / cr4, donc tout va bien avec QEMU.
Cela ne se produit pas dans Quake lui-même, mais dans OpenLibM (c'est la bibliothèque que nous utilisons pour implémenter des fonctions mathématiques - sin()
, expf()
etc.). Patch OpenLibm pour que __test_sse()
ne fasse pas de véritable vérification sur SSE, mais pense simplement qu'il n'y a pas de support.
Les étapes ci-dessus sont suffisantes pour s'exécuter, la sortie suivante est visible dans la console:
> 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
Déjà pas mal, Quake3 essaie de démarrer et affiche même un message d'erreur! Comme vous pouvez le voir, il lui manque les fichiers du répertoire baseq3
. Il contient des sons, des textures et tout ça. Remarque, pak0.pk3
doit être extrait d'un CD-ROM sous licence (oui, l'open source n'implique pas une utilisation gratuite).
Préparation du disque
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
Vous pouvez maintenant transférer le périphérique de bloc vers qemu
qemu-system-i386 -kernel build/base/bin/embox -m 1024 -vga std -serial stdio -hda quake.img
Lorsque le système démarre, montez le disque sur /mnt
et exécutez quake3 dans ce répertoire, cette fois il se bloque plus tard
> 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
Cette erreur est à nouveau avec SIMD dans Qemu. upd: Comme suggéré par WGH dans les commentaires, le vrai problème était que j'avais oublié d'activer explicitement le support SSE dans cr0 / cr4, donc tout va bien avec QEMU. Cette fois, les instructions sont utilisées dans la machine virtuelle Quake3 x86. Le problème a été résolu en remplaçant l'implémentation pour x86 par une machine virtuelle interprétée (plus sur la machine virtuelle Quake3 et, en principe, les fonctionnalités architecturales, vous pouvez tout lire dans le même article ). Après cela, nos fonctions pour SDL commencent à être appelées, mais, bien sûr, rien ne se passe, car ces fonctions ne font encore rien.
Ajouter un support graphique
static SDL_VideoDevice *createDevice(int devindex) { ... device->GL_GetProcAddress = glGetProcAddress; device->GL_CreateContext = glCreateContext; ... } SDL_GLContext glCreateContext(_THIS, SDL_Window *window) { OSMesaContext ctx; sdl_init_buffers(); ctx = OSMesaCreateContextExt(OSMESA_BGRA, 16, 0, 0, NULL); OSMesaMakeCurrent(ctx, fb_base, GL_UNSIGNED_BYTE, fb_width, fb_height); return ctx; }
Le second gestionnaire est nécessaire pour indiquer au SDL quelles fonctions appeler lors de l'utilisation d'OpenGL.
Pour ce faire, nous démarrons un tableau et du début au début, nous vérifions quels appels sont manquants, quelque chose comme ceci:
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; }
En quelques redémarrages, la liste devient suffisamment complète pour qu'un écran de démarrage et un menu soient dessinés. Heureusement, Mesa a toutes les fonctions nécessaires. La seule chose est que pour une raison quelconque, il n'y a pas de fonction glGetString()
, glGetString()
a dû être utilisé à la _mesa_GetString()
.
Maintenant, lorsque l'application démarre, un écran de démarrage apparaît, bravo!

Ajouter des périphériques d'entrée
Ajoutez la prise en charge du clavier et de la souris au SDL.
Pour travailler avec des événements, vous devez ajouter un gestionnaire
static SDL_VideoDevice *createDevice(int devindex) { ... device->PumpEvents = pumpEvents; ... }
Commençons par le clavier. Nous raccrochons une fonction pour interrompre la pression / relâchement d'une touche. Cette fonction doit se souvenir de l'événement (dans le cas le plus simple, nous écrivons simplement dans une variable locale, les files d'attente peuvent être utilisées si vous le souhaitez), pour plus de simplicité, nous ne stockerons que le dernier événement.
static struct input_event last_event; static int sdl_indev_eventhnd(struct input_dev *indev) { while (0 == input_dev_event(indev, &last_event)) { } }
Ensuite, dans pumpEvents()
traitons l'événement et le transmettons au 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); } }
Plus d'informations sur les codes clés et SDL_ScancodeSDL utilise sa propre énumération pour les codes clés, vous devez donc convertir le code clé du système d'exploitation en code SDL.
Une liste de ces codes est définie dans le fichier SDL_scancode.h
Par exemple, vous pouvez convertir le code ASCII comme ceci (tous les caractères ASCII ne sont pas ici, mais ils suffisent):
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'est tout avec le clavier, le reste sera géré par SDL et Quake lui-même. Soit dit en passant, il s'est avéré ici que quelque part dans le traitement des frappes, Quake utilise des instructions qui ne sont pas prises en charge par QEMU, vous devez basculer vers la machine virtuelle interprétée à partir de la machine virtuelle x86, pour cela, nous ajoutons BASE_CFLAGS += -DNO_VM_COMPILED
au Makefile.
Après cela, enfin, vous pouvez «sauter» solennellement les économiseurs d'écran et même démarrer le jeu (pirater une erreur :)). Il a été agréablement surpris que tout soit rendu comme il se doit, bien qu'avec un fps très bas.

Vous pouvez maintenant démarrer la prise en charge de la souris. Pour les interruptions de souris, vous avez besoin d'un gestionnaire supplémentaire et la gestion des événements devra être un peu compliquée. Nous nous limitons uniquement au bouton gauche de la souris. Il est clair que de la même manière, vous pouvez ajouter la bonne clé, la molette, etc.
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()); } } }
Après cela, il devient possible de contrôler la caméra et de tirer, bravo! En fait, c'est déjà suffisant pour jouer :)

Optimisation
C'est cool, bien sûr, qu'il y ait du contrôle et une sorte de graphisme, mais un tel FPS n'a absolument aucune valeur. Très probablement, la plupart du temps est consacré à travailler sur OpenGL (qui est un logiciel et, de plus, SIMD n'est pas utilisé), et la mise en œuvre du support matériel est une tâche trop longue et compliquée.
Essayons d'accélérer le jeu avec un peu de sang.
Optimisation du compilateur et résolution inférieure
Nous assemblons le jeu, toutes les bibliothèques et le système d'exploitation lui-même avec -O3
(si, tout d'un coup, quelqu'un a lu à cet endroit, mais ne sait pas ce qu'est ce drapeau - plus d'informations sur les drapeaux d'optimisation GCC peuvent être trouvées ici ).
De plus, nous utilisons la résolution minimale - 320x240 pour faciliter le travail du processeur.
Kvm
KVM (Kernel-based Virtual Machine) vous permet d'utiliser la virtualisation matérielle (Intel VT et AMD-V) pour améliorer les performances. Qemu prend en charge ce mécanisme, pour l'utiliser, vous devez effectuer les opérations suivantes.
Tout d'abord, vous devez activer la prise en charge de la virtualisation dans le BIOS. J'ai une carte mère Gigabyte B450M DS3H, et AMD-V est activé via MIT -> Paramètres de fréquence avancés -> Paramètres de base du processeur avancés -> Mode SVM -> Activé (Gigabyte, quel est le problème avec vous?).
Ensuite, nous mettons le package nécessaire et ajoutons le module approprié
sudo apt install qemu-kvm sudo modprobe kvm-amd
Ça y est, vous pouvez maintenant passer qemu le drapeau -enable-kvm
(ou -no-kvm
pour ne pas utiliser l'accélération matérielle).
Résumé
Le jeu a commencé, les graphiques s'affichent au besoin, le contrôle fonctionne. Malheureusement, les graphiques sont dessinés sur le CPU en un seul flux, également sans SIMD, en raison des faibles fps (2-3 images par seconde), il est très gênant de contrôler.
Le processus de portage était intéressant. Peut-être qu'à l'avenir, il sera possible de lancer Quake sur une plate-forme avec une accélération graphique matérielle, mais pour l'instant je vais me concentrer sur ce qui est.