
在Embox操作系统(我是我的开发者)中,对OpenGL的支持早在一段时间前就出现了,但是没有明智的性能检查,仅渲染具有多个图形基元的场景。
我对gamedev从来没有特别感兴趣,尽管我当然喜欢游戏,并决定-这是一种获得乐趣的好方法,但同时检查OpenGL并查看游戏如何与OS交互。
在本文中,我将讨论如何在Embox上构建和运行Quake3。
更确切地说,我们不会运行Quake3本身,而是运行基于它的ioquake3 ,它也具有开源代码。 为了简单起见,我们将ioquake3称为地震:)
我将立即保留一点,即本文不会分析Quake源代码及其体系结构(您可以在此处阅读有关内容,并在Habré上进行翻译 ),而本文将重点介绍如何确保在新操作系统上启动游戏。
为了更好地理解,简化了本文中提供的代码片段:省略了错误检查,使用了伪代码,依此类推。 原始资源可以在我们的资源库中找到。
依存关系
奇怪的是,构建Quake3并不需要很多库。 我们将需要:
在第一段中,所有内容都很清楚-如果没有这些功能,使用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
中设置了用于处理图形的后端。
看起来像这样:
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
src/video/qnx/video.c
为基础,但是首先我们将实现几乎为空:
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 };
将您的VideoBootStrap
添加到阵列:
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; ... } 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; }
需要第二个处理程序来告诉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) { 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
就是这样,现在您可以将-enable-kvm
标志(或-no-kvm
标志)传递给qemu,以免使用硬件加速。
总结
游戏开始,根据需要显示图形,控件正在工作。 不幸的是,由于帧速率低(每秒2-3帧),因此控制起来非常不方便,因此图形是在CPU上以一个流(也没有SIMD)的方式绘制的。
移植过程很有趣。 也许将来有可能在具有硬件图形加速功能的平台上启动quake,但现在我将重点关注什么。