Porting Quake3


En el sistema operativo Embox (del cual yo soy el desarrollador), el soporte para OpenGL apareció hace algún tiempo, pero no hubo una comprobación de rendimiento sensata, solo representando escenas con varias primitivas gráficas.


Nunca estuve particularmente interesado en gamedev, aunque, por supuesto, me gustan los juegos y decidí: esta es una buena manera de divertirse, pero al mismo tiempo verifique OpenGL y vea cómo los juegos interactúan con el sistema operativo.


En este artículo, hablaré sobre cómo construir y ejecutar Quake3 en Embox.


Más precisamente, no ejecutaremos Quake3 en sí, sino ioquake3 basado en él, que también tiene código fuente abierto. Por simplicidad, llamaremos a ioquake3 simplemente terremoto :)


Haré una reserva de inmediato para que el artículo no analice el código fuente de Quake y su arquitectura (puede leer sobre esto aquí , hay traducciones en el Habré ), y este artículo se centrará en cómo garantizar que el juego se inicie en el nuevo sistema operativo.


Los fragmentos de código presentados en el artículo se simplifican para una mejor comprensión: se omiten las comprobaciones de errores, se utiliza un pseudocódigo, etc. Las fuentes originales se pueden encontrar en nuestro repositorio .


Dependencias


Por extraño que parezca, no se necesitan muchas bibliotecas para construir Quake3. Necesitaremos:


  • POSIX + LibC - malloc() / memcpy() / printf() y así sucesivamente
  • libcurl - redes
  • Mesa3D - Soporte OpenGL
  • SDL - soporte para dispositivos de entrada y audio

Con el primer párrafo, y todo está claro: sin estas funciones es difícil de hacer cuando se desarrolla en C, y el uso de estas llamadas es bastante esperado. Por lo tanto, el soporte para estas interfaces está de alguna manera disponible en casi todos los sistemas operativos, y en este caso, prácticamente no hubo necesidad de agregar funcionalidad. Tuve que lidiar con el resto.


libcurl


Fue lo más simple. Libc es suficiente para construir libcurl (por supuesto, algunas características no estarán disponibles, pero no serán necesarias). Configurar y construir esta biblioteca es estáticamente muy simple.


Por lo general, tanto las aplicaciones como las bibliotecas están vinculadas dinámicamente, pero porque en Embox, el modo principal es vincular en una imagen, vincularemos todo estáticamente.


Dependiendo del sistema de compilación utilizado, los pasos específicos variarán, pero el significado es algo como esto:


 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 es un marco de código abierto para trabajar con gráficos, admite varias interfaces (OpenCL, Vulkan y otras), pero en este caso estamos interesados ​​en OpenGL. Portar un marco tan grande es el tema de un artículo separado. Me limitaré solo a lo que Embox Mesa3D ya tiene :) Por supuesto, cualquier implementación de OpenGL es adecuada aquí.


Sdl


SDL es un marco multiplataforma para trabajar con dispositivos de entrada, audio y gráficos.


Por ahora, martillaremos todo, excepto los gráficos, y para dibujar marcos, escribiremos funciones de código auxiliar para ver cuándo comienzan a llamarse.


Los backends para trabajar con gráficos se establecen en SDL2-2.0.8/src/video/SDL_video.c .


Se parece a esto:


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

Para no molestarse con el soporte "normal" para la nueva plataforma, simplemente agregue su VideoBootStrap


Para simplificar, puede tomar algo como base, por ejemplo src/video/qnx/video.c o src/video/raspberry/SDL_rpivideo.c , pero primero haremos que la implementación esté casi vacía:


 /* 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 }; 

Agregue su VideoBootStrap a la matriz:


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

Básicamente, en este punto ya puedes compilar SDL. Al igual que con libcurl, los detalles de la compilación dependerán del sistema de compilación particular, pero de alguna manera debe hacer algo como esto:


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

Recopilamos Quake


Quake3 implica el uso de bibliotecas dinámicas, pero lo vincularemos estáticamente, como todo lo demás.


Para hacer esto, establezca algunas variables en el Makefile


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

Primer lanzamiento


Por simplicidad, correremos en qemu / x86. Para hacer esto, debe instalarlo (aquí y debajo habrá comandos para Debian, para otros paquetes de distribución se puede llamar de manera diferente).


 sudo apt install qemu-system-i386 

Y el lanzamiento en sí:


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

Sin embargo, al iniciar Quake, inmediatamente recibimos un error


 > 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 

El juego no muestra el error, sino el sistema operativo. Debag mostró que este error fue causado por un soporte SIMD incompleto para x86 en QEMU: parte de las instrucciones no es compatible y arroja una excepción de comando desconocida (código de operación no válido). upd: Como lo sugirió WGH en los comentarios, el verdadero problema fue que olvidé habilitar explícitamente el soporte SSE en cr0 / cr4, por lo que todo está bien con QEMU.


Esto sucede no en Quake en sí, sino en OpenLibM (esta es la biblioteca que usamos para implementar funciones matemáticas: sin() , expf() y similares). Parche OpenLibm para que __test_sse() no haga una verificación real en SSE, sino que simplemente crea que no hay soporte.


Los pasos anteriores son suficientes para ejecutarse, el siguiente resultado es visible en la consola:


 > 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 

¡Ya no está mal, Quake3 intenta iniciarse e incluso muestra un mensaje de error! Como puede ver, carece de los archivos en el directorio baseq3 . Contiene sonidos, texturas y todo eso. Tenga en cuenta que pak0.pk3 debe tomarse de un CD-ROM con licencia (sí, el código abierto no implica un uso gratuito).


Preparación del disco


 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 

Ahora puede transferir el dispositivo de bloqueo a qemu


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

Cuando se inicia el sistema, monte el disco en /mnt y ejecute quake3 en este directorio, esta vez se bloquea más tarde


 > 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 

Este error es nuevamente con SIMD en Qemu. upd: Como lo sugirió WGH en los comentarios, el verdadero problema fue que olvidé habilitar explícitamente el soporte SSE en cr0 / cr4, por lo que todo está bien con QEMU. Esta vez, las instrucciones se utilizan en la máquina virtual Quake3 x86. El problema se resolvió reemplazando la implementación para x86 con una VM interpretada (más sobre la máquina virtual Quake3 y, en principio, las características arquitectónicas, puede leer todo en el mismo artículo ). Después de eso, nuestras funciones para SDL comienzan a llamarse, pero, por supuesto, no sucede nada, porque Estas funciones no hacen nada todavía.


Agregar soporte gráfico


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

El segundo controlador es necesario para indicarle al SDL qué funciones llamar cuando se trabaja con OpenGL.


Para hacer esto, comenzamos una matriz y de principio a comienzo verificamos qué llamadas faltan, algo como esto:


 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 unos pocos reinicios, la lista se completa lo suficiente como para dibujar una pantalla de bienvenida y un menú. Afortunadamente, Mesa tiene todas las funciones necesarias. Lo único es que, por alguna razón, no hay glGetString() función glGetString() en su _mesa_GetString() tuvo que usar glGetString() .


Ahora, cuando se inicia la aplicación, aparece una pantalla de bienvenida, ¡salud!




Agregar dispositivos de entrada


Agregue soporte para teclado y mouse al SDL.


Para trabajar con eventos, debe agregar un controlador


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

Comencemos con el teclado. Colgamos una función para interrumpir presionar / soltar una tecla. Esta función debe recordar el evento (en el caso más simple, simplemente escribimos en una variable local, las colas se pueden usar si se desea), por simplicidad almacenaremos solo el último evento.


 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)) { } } 

Luego, en pumpEvents() procesamos el evento y lo pasamos al 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); } } 

Más información sobre códigos clave y SDL_Scancode

SDL utiliza su propia enumeración para los códigos de clave, por lo que debe convertir el código de clave del sistema operativo en código SDL.


Una lista de estos códigos se define en el archivo SDL_scancode.h


Por ejemplo, puede convertir el código ASCII de esta manera (no todos los caracteres ASCII están aquí, pero estos son suficientes):


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

Eso es todo con el teclado, el resto será manejado por SDL y Quake. Por cierto, resultó que en algún lugar del procesamiento de pulsaciones de teclas, el terremoto usa instrucciones que no son compatibles con QEMU, debe cambiar a la máquina virtual interpretada desde la máquina virtual x86, para esto agregamos BASE_CFLAGS += -DNO_VM_COMPILED al Makefile.


Después de eso, finalmente, puede "saltear" solemnemente los protectores de pantalla e incluso iniciar el juego (piratear algún error :)). Me sorprendió gratamente que todo se procese como debería, aunque con un fps muy bajo.



Ahora puede iniciar el soporte del mouse. Para las interrupciones del mouse, necesita un controlador más, y el manejo de eventos tendrá que ser un poco complicado. Nos restringimos solo al botón izquierdo del mouse. Está claro que de la misma manera, puede agregar la llave, rueda, 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()); /*      */ } } } 

Después de eso, se hace posible controlar la cámara y disparar, ¡salud! De hecho, esto ya es suficiente para jugar :)



Optimización


Es genial, por supuesto, que haya control y algún tipo de gráficos, pero tal FPS no tiene ningún valor. Lo más probable es que la mayor parte del tiempo se gaste en OpenGL (y es software y, además, SIMD no se usa), y la implementación del soporte de hardware es una tarea demasiado larga y difícil.


Intentemos acelerar el juego con un poco de sangre.


Optimización del compilador y menor resolución.


Estamos construyendo el juego, todas las bibliotecas y el sistema operativo en sí con -O3 (si, de repente, alguien lee en este lugar, pero no sabe qué es esta bandera, puede encontrar más información sobre las banderas de optimización de GCC aquí ).


Además, utilizamos la resolución mínima: 320x240 para facilitar el trabajo del procesador.


Kvm


KVM (máquina virtual basada en kernel) le permite utilizar la virtualización de hardware (Intel VT y AMD-V) para mejorar el rendimiento. Qemu admite este mecanismo, para usarlo debe hacer lo siguiente.


Primero, debe habilitar el soporte de virtualización en el BIOS. Tengo una placa base Gigabyte B450M DS3H, y AMD-V se enciende a través de MIT -> Configuración avanzada de frecuencia -> Configuración avanzada de núcleo de CPU -> Modo SVM -> Activado (Gigabyte, ¿qué te pasa?).


Luego ponemos el paquete necesario y agregamos el módulo apropiado


 sudo apt install qemu-kvm sudo modprobe kvm-amd #  kvm-intel 

Eso es todo, ahora puede pasar qemu el -enable-kvm (o -no-kvm para no usar la aceleración de hardware).


Resumen



El juego comenzó, los gráficos se muestran según sea necesario, el control está funcionando. Desafortunadamente, los gráficos se dibujan en la CPU en una secuencia, también sin SIMD, debido a los bajos fps (2-3 cuadros por segundo) es muy inconveniente de controlar.


El proceso de portabilidad fue interesante. Quizás en el futuro sea posible lanzar un terremoto en una plataforma con aceleración de gráficos de hardware, pero por ahora me centraré en lo que es.

Source: https://habr.com/ru/post/es428634/


All Articles