
No sistema operacional Embox (do qual sou desenvolvedor), o suporte ao OpenGL apareceu há algum tempo, mas não havia verificação de desempenho sensata, apenas renderizando cenas com várias primitivas gráficas.
Eu nunca fiquei particularmente interessado em gamedev, embora, é claro, goste de jogos e tenha decidido - essa é uma boa maneira de se divertir, mas ao mesmo tempo verifique o OpenGL e veja como os jogos interagem com o sistema operacional.
Neste artigo, falarei sobre como criar e executar o Quake3 na Embox.
Mais precisamente, não executaremos o Quake3 em si, mas o ioquake3 com base nele, que também possui código-fonte aberto. Para simplificar, chamaremos ioquake3 apenas quake :)
Farei uma reserva imediatamente de que o artigo não analisa o código-fonte do Quake e sua arquitetura (você pode ler sobre ele aqui , existem traduções no Habré ), e este artigo se concentrará em como garantir que o jogo seja lançado no novo sistema operacional.
Os fragmentos de código apresentados no artigo são simplificados para melhor compreensão: as verificações de erros são omitidas, o pseudocódigo é usado e assim por diante. Fontes originais podem ser encontradas em nosso repositório .
Dependências
Curiosamente, não são necessárias muitas bibliotecas para criar o Quake3. Vamos precisar de:
- POSIX + LibC -
malloc()
/ memcpy()
/ printf()
e assim por diante - libcurl - rede
- Mesa3D - Suporte OpenGL
- SDL - suporte a dispositivos de entrada e áudio
Com o primeiro parágrafo, e assim tudo fica claro - sem essas funções, é difícil fazer no desenvolvimento em C, e o uso dessas chamadas é esperado. Portanto, de alguma forma, o suporte para essas interfaces está disponível em quase todos os sistemas operacionais e, nesse caso, praticamente não havia necessidade de adicionar funcionalidade. Eu tive que lidar com o resto.
libcurl
Foi o mais simples. Libc é suficiente para criar libcurl (é claro, alguns recursos não estarão disponíveis, mas não serão necessários). Configurar e construir esta biblioteca é estaticamente muito simples.
Geralmente, aplicativos e bibliotecas são vinculados dinamicamente, mas porque na Embox, o modo principal está vinculando em uma imagem, vincularemos tudo estaticamente.
Dependendo do sistema de compilação usado, as etapas específicas variam, mas o significado é algo como isto:
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
O Mesa é uma estrutura de código aberto para trabalhar com gráficos, suporta várias interfaces (OpenCL, Vulkan e outras), mas, neste caso, estamos interessados no OpenGL. Transportar uma estrutura tão grande é o tópico de um artigo separado. Vou me limitar apenas ao que o Embox Mesa3D já possui :) Obviamente, qualquer implementação OpenGL é adequada aqui.
Sdl
SDL é uma estrutura multiplataforma para trabalhar com dispositivos de entrada, áudio e gráficos.
Por enquanto, vamos martelar tudo, exceto gráficos, e para desenhar molduras, escreveremos funções de stub para ver quando elas começam a ser chamadas.
Os back-ends para trabalhar com gráficos são definidos em SDL2-2.0.8/src/video/SDL_video.c
.
Parece algo como isto:
static VideoBootStrap *bootstrap[] = { #if SDL_VIDEO_DRIVER_COCOA &COCOA_bootstrap, #endif #if SDL_VIDEO_DRIVER_X11 &X11_bootstrap, #endif ... }
Para não se preocupar com o suporte "normal" para a nova plataforma, basta adicionar seu VideoBootStrap
Para simplificar, você pode src/video/qnx/video.c
algo como base, por exemplo, src/video/qnx/video.c
ou src/video/raspberry/SDL_rpivideo.c
, mas primeiro tornaremos a implementação geralmente quase vazia:
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 };
Adicione seu VideoBootStrap
à matriz:
static VideoBootStrap *bootstrap[] = { &EMBOX_bootstrap, #if SDL_VIDEO_DRIVER_COCOA &COCOA_bootstrap, #endif #if SDL_VIDEO_DRIVER_X11 &X11_bootstrap, #endif ... }
Basicamente, neste ponto, você já pode compilar SDL. Como na libcurl, os detalhes da compilação dependerão do sistema de compilação específico, mas de alguma forma você precisa fazer algo assim:
./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
Nós coletamos o próprio Quake
O Quake3 envolve o uso de bibliotecas dinâmicas, mas o vincularemos estaticamente, como todo o resto.
Para fazer isso, defina algumas variáveis no Makefile
CROSS_COMPILING=1 USE_OPENAL=0 USE_OPENAL_DLOPEN=0 USE_RENDERER_DLOPEN=0 SHLIBLDFLAGS=-static
Primeiro lançamento
Para simplificar, rodaremos no qemu / x86. Para fazer isso, você precisa instalá-lo (aqui e abaixo haverá comandos para o Debian, pois outros pacotes de distribuição podem ser chamados de forma diferente).
sudo apt install qemu-system-i386
E o próprio lançamento:
qemu-system-i386 -kernel build/base/bin/embox -m 1024 -vga std -serial stdio
No entanto, ao iniciar o Quake, recebemos imediatamente um erro
> 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
O erro não é exibido pelo jogo, mas pelo sistema operacional. Debag mostrou que esse erro foi causado pelo suporte SIMD incompleto para x86 no QEMU: parte das instruções não é suportada e gera uma exceção de comando desconhecida (código de operação inválido). upd: Como sugerido por WGH nos comentários, o problema real era que eu esqueci de ativar explicitamente o suporte SSE em cr0 / cr4, então tudo está bem com o QEMU.
Isso acontece não no próprio Quake, mas no OpenLibM (esta é a biblioteca que usamos para implementar funções matemáticas - sin()
, expf()
e similares). Faça o patch do OpenLibm para que __test_sse()
não faça uma verificação real no SSE, mas simplesmente acredite que não há suporte.
As etapas acima são suficientes para executar, a seguinte saída é visível no 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
Já não é ruim, o Quake3 tenta iniciar e até exibe uma mensagem de erro! Como você pode ver, ele não possui os arquivos no diretório baseq3
. Ele contém sons, texturas e tudo isso. Observe que pak0.pk3
deve ser retirado de um CD-ROM licenciado (sim, o código aberto não implica uso gratuito).
Preparação de 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
Agora você pode transferir o dispositivo de bloco para o qemu
qemu-system-i386 -kernel build/base/bin/embox -m 1024 -vga std -serial stdio -hda quake.img
Quando o sistema iniciar, monte o disco em /mnt
e execute quake3 neste diretório, desta vez travando mais 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 erro ocorre novamente com o SIMD no Qemu. upd: Como sugerido por WGH nos comentários, o problema real era que eu esqueci de ativar explicitamente o suporte SSE em cr0 / cr4, então tudo está bem com o QEMU. Dessa vez, as instruções são usadas na máquina virtual Quake3 x86. O problema foi resolvido substituindo a implementação do x86 por uma VM interpretada (mais sobre a máquina virtual Quake3 e, em princípio, recursos de arquitetura, você pode ler tudo no mesmo artigo ). Depois disso, nossas funções para SDL começam a ser chamadas, mas, é claro, nada acontece, porque essas funções ainda não fazem nada.
Adicionar suporte gráfico
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; }
O segundo manipulador é necessário para informar ao SDL quais funções chamar ao trabalhar com o OpenGL.
Para fazer isso, iniciamos uma matriz e, do início ao início, verificamos quais chamadas estão ausentes, algo como isto:
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; }
Em algumas reinicializações, a lista fica completa o suficiente para que uma tela inicial e um menu sejam desenhados. Felizmente, o Mesa possui todas as funções necessárias. A única coisa é que, por algum motivo, não há função glGetString()
, glGetString()
teve que ser usada.
Agora, quando o aplicativo é iniciado, uma tela inicial é exibida, um brinde!

Adicionar dispositivos de entrada
Adicione suporte de teclado e mouse ao SDL.
Para trabalhar com eventos, você precisa adicionar um manipulador
static SDL_VideoDevice *createDevice(int devindex) { ... device->PumpEvents = pumpEvents; ... }
Vamos começar com o teclado. Desligamos uma função para interromper a pressão / liberação de uma tecla. Esta função deve lembrar o evento (no caso mais simples, simplesmente escrevemos para uma variável local; as filas podem ser usadas, se desejado); por simplicidade, armazenaremos apenas o último evento.
static struct input_event last_event; static int sdl_indev_eventhnd(struct input_dev *indev) { while (0 == input_dev_event(indev, &last_event)) { } }
Em seguida, em pumpEvents()
processamos o evento e o passamos para o 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); } }
Mais sobre códigos de chave e SDL_ScancodeO SDL usa sua própria enumeração para códigos de chave, portanto, você deve converter o código de chave do SO em código SDL.
Uma lista desses códigos é definida no arquivo SDL_scancode.h
Por exemplo, você pode converter o código ASCII assim (nem todos os caracteres ASCII estão aqui, mas são o suficiente):
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, };
Isso é tudo com o teclado, o restante será tratado pelo SDL e pelo próprio Quake. A propósito, verificou-se por aqui que, em algum lugar no processamento de pressionamentos de tecla, o quake usa instruções que não são suportadas pelo QEMU, você precisa mudar para a máquina virtual interpretada da máquina virtual x86, para isso adicionamos BASE_CFLAGS += -DNO_VM_COMPILED
ao Makefile.
Depois disso, finalmente, você pode solenemente "pular" os protetores de tela e até iniciar o jogo (hackear algum erro :)). Foi uma surpresa agradável que tudo seja renderizado como deveria, embora com um fps muito baixo.

Agora você pode iniciar o suporte ao mouse. Para interrupções do mouse, você precisa de mais um manipulador, e o tratamento de eventos precisará ser um pouco complicado. Nós nos restringimos apenas ao botão esquerdo do mouse. É claro que, da mesma maneira, você pode adicionar a chave, a roda, 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()); } } }
Depois disso, torna-se possível controlar a câmera e disparar, um brinde! Na verdade, isso já é suficiente para jogar :)

Otimização
É legal, é claro, que haja controle e algum tipo de gráfico, mas esse FPS é absolutamente inútil. Provavelmente, a maior parte do tempo é gasta no OpenGL (e é um software e, além disso, o SIMD não é usado), e a implementação do suporte de hardware é uma tarefa muito longa e difícil.
Vamos tentar acelerar o jogo com um pouco de sangue.
Otimização do compilador e resolução mais baixa
Estamos construindo o jogo, todas as bibliotecas e o próprio sistema operacional com -O3
(se, de repente, alguém ler esse lugar, mas não souber o que é esse sinalizador - mais sobre os sinalizadores de otimização do GCC podem ser encontrados aqui ).
Além disso, usamos a resolução mínima - 320x240 para facilitar o trabalho do processador.
Kvm
O KVM (máquina virtual baseada em kernel) permite usar a virtualização de hardware (Intel VT e AMD-V) para melhorar o desempenho. O Qemu suporta esse mecanismo. Para usá-lo, você precisa fazer o seguinte.
Primeiro, você precisa habilitar o suporte à virtualização no BIOS. Eu tenho uma placa-mãe Gigabyte B450M DS3H e o AMD-V é ativado via MIT -> Configurações avançadas de frequência -> Configurações avançadas de núcleo da CPU -> Modo SVM -> Ativado (Gigabyte, o que há de errado com você?).
Em seguida, colocamos o pacote necessário e adicionamos o módulo apropriado
sudo apt install qemu-kvm sudo modprobe kvm-amd
É isso, agora você pode passar para qemu o sinalizador -enable-kvm
(ou -no-kvm
para não usar a aceleração de hardware).
Sumário
O jogo começou, os gráficos são exibidos conforme necessário, o controle está funcionando. Infelizmente, os gráficos são desenhados na CPU em um fluxo, também sem SIMD, por causa dos baixos fps (2-3 quadros por segundo), é muito inconveniente de controlar.
O processo de portabilidade foi interessante. Talvez no futuro seja possível lançar o terremoto em uma plataforma com aceleração gráfica de hardware, mas por enquanto vou me concentrar no que é.