移植Quake3


Embox操作系统(我是我的开发者)中,对OpenGL的支持早在一段时间前就出现了,但是没有明智的性能检查,仅渲染具有多个图形基元的场景。


我对gamedev从来没有特别感兴趣,尽管我当然喜欢游戏,并决定-这是一种获得乐趣的好方法,但同时检查OpenGL并查看游戏如何与OS交互。


在本文中,我将讨论如何在Embox上构建和运行Quake3。


更确切地说,我们不会运行Quake3本身,而是运行基于它的ioquake3 ,它也具有开源代码。 为了简单起见,我们将ioquake3称为地震:)


我将立即保留一点,即本文不会分析Quake源代码及其体系结构(您可以在此处阅读有关内容,并在Habré上进行翻译 ),而本文将重点介绍如何确保在新操作系统上启动游戏。


为了更好地理解,简化了本文中提供的代码片段:省略了错误检查,使用了伪代码,依此类推。 原始资源可以在我们的资源库中找到。


依存关系


奇怪的是,构建Quake3并不需要很多库。 我们将需要:


  • POSIX + LibC- malloc() / memcpy() / printf()等等
  • libcurl-网络
  • Mesa3D -OpenGL支持
  • SDL-输入和音频设备支持

在第一段中,所有内容都很清楚-如果没有这些功能,使用C进行开发时将很难做到这一点,并且期望使用这些调用。 因此,几乎所有操作系统都可以某种方式获得对这些接口的支持,并且在这种情况下,实际上不需要添加功能。 我不得不处理其余的事情。


libcurl


这是最简单的。 Libc足以构建libcurl(当然,某些功能将不可用,但不需要)。 配置和构建该库在静态上非常简单。


通常,应用程序和库都是动态链接的,但是因为 在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 #       

台面/ 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.csrc/video/raspberry/SDL_rpivideo.c src/video/qnx/video.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 #      

我们收集地震本身


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 

该错误不是由游戏显示,而是由操作系统显示。 Debag指出此错误是由于QEMU中对x86的SIMD支持不完全引起的:不支持部分指令,并抛出未知的命令异常(无效操作码)。 upd:正如WGH在评论中所建议的那样,真正的问题是我忘记了在cr0 / cr4中明确启用SSE支持,因此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-ROM中获取(是的,开放源代码并不意味着可以免费使用)。


磁盘准备


 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 

Qemu中的SIMD再次出现此错误。 upd:正如WGH在评论中所建议的那样,真正的问题是我忘记了在cr0 / cr4中明确启用SSE支持,因此QEMU一切正常。 这次,这些指令在Quake3 x86虚拟机中使用。 通过用解释后的VM替换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()函数, glGetString()必须使用glGetString()


现在,当应用程序启动时,会出现一个初始屏幕,欢呼!




添加输入设备


将键盘和鼠标支持添加到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对密钥代码使用其自己的枚举,因此您必须将OS密钥代码转换为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, }; 

键盘就这些了,其余的将由SDL和Quake本身处理。 顺便说一句,这里的结果是,在处理键击时,quake使用了QEMU不支持的指令,您必须从x86虚拟机切换到解释的虚拟机,为此,我们在BASE_CFLAGS += -DNO_VM_COMPILED添加了BASE_CFLAGS += -DNO_VM_COMPILED


之后,最后,您可以郑重地“跳过”屏幕保护程序,甚至开始游戏(破解一些错误:))。 令人惊讶的是,尽管帧速率很低,但一切都按原样呈现。



现在,您可以开始鼠标支持。 对于鼠标中断,您需要一个处理程序,并且事件处理将变得有些复杂。 我们只限于鼠标左键。 显然,您可以以相同的方式添加右键,滚轮等。


 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构建游戏,所有库以及OS本身(如果突然之间有人读到了这个地方,但不知道这个标志是什么,可以在此处找到有关GCC优化标志的更多信息)。


此外,我们使用最低分辨率-320x240来促进处理器的工作。


虚拟机


KVM(基于内核的虚拟机)使您可以使用硬件虚拟化(Intel VT和AMD-V)来提高性能。 Qemu支持此机制,要使用它,您需要执行以下操作。


首先,您需要在BIOS中启用虚拟化支持。 我有一个技嘉B450M DS3H主板,并且通过MIT->高级频率设置->高级CPU核心设置-> SVM模式->启用(技嘉,您怎么了?)打开了AMD-V。


然后我们放入必要的包并添加适当的模块


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

就是这样,现在您可以将-enable-kvm标志(或-no-kvm标志)传递给qemu,以免使用硬件加速。


总结



游戏开始,根据需要显示图形,控件正在工作。 不幸的是,由于帧速率低(每秒2-3帧),因此控制起来非常不方便,因此图形是在CPU上以一个流(也没有SIMD)的方式绘制的。


移植过程很有趣。 也许将来有可能在具有硬件图形加速功能的平台上启动quake,但现在我将重点关注什么。

Source: https://habr.com/ru/post/zh-CN428634/


All Articles