Porting Quake3

 3r3778. 3r3-31. Porting Quake3 3r33737.  3r3778. 3r33766. In the operating system 3r3338. Embox
(of which I am a developer) OpenGL support appeared some time ago, but there was no sensible performance check, only drawing scenes with several graphic primitives. 3r33767. 3r33737.  3r3778. 3r33766. I have never been particularly interested in game devs, although, of course, I like the games, and decided that this is a good way to have fun, but at the same time check OpenGL and see how the games interact with the OS. 3r33767. 3r33737.  3r3778. 3r33766. In this article I will talk about how to build and run Quake3 on Embox. 3r33767. itself. Quake3 , and based on it ioquake3 which is also open source. For simplicity, we will call ioquake3 just a quake :)
3r33737.  3r3778. 3r33766. At once I will make a reservation that the article does not analyze the Quake source code itself and its architecture (you can read about 3r3447. Here,
, There are 3r3323. Transfers to Habré 3r3709.), And in this article we will talk about how to ensure the launch of the game on a new operating system. 3r33767. 3r33737.  3r3778. 3r33766. The code snippets cited in the article are simplified for a better understanding: missing error checks, using pseudo-code, and so on. Original source can be found in our repository . 3r33767. 3r33737.  3r3778.
Dependencies
3r33737.  3r3778. 3r33766. Oddly enough, not many libraries are needed to build Quake3. We will need:
3r33737.  3r3778.
 3r3778.
POSIX + LibC - malloc () / memcpy () / printf () and so on  3r3778. 3r3633. libcurl - Work with the network 3r375.  3r3778. 3r3116. Mesa3D - support OpenGL  3r3778. SDL - Support for input devices and audio 3r375.  3r3778. 3r33737.  3r3778. 3r33766. With the first paragraph, and so everything is clear - it is difficult to do without these functions when developing in C, and the use of these calls is quite expected. Therefore, support for these interfaces is in one way or another practically in all operating systems, and in this case almost no functionality was added. That had to deal with the rest. 3r33767. 3r33737.  3r3778. 3r3384. libcurl 3r33737.  3r3778. 3r33766. It was the easiest. Libc is enough to build libcurl (of course, some features will not be available, but they will not be required). Configuring and building this library is statically very simple. 3r33767. 3r33737.  3r3778. 3r33766. Usually both applications and libraries are linked dynamically, but since in Embox, the main mode is linking into one image, we will link everything statically. 3r33767. 3r33737.  3r3778. 3r33766. Depending on the build system used, the specific steps will differ, but the meaning is something like this: 3r33737.  3r3778. 3r33737. 3r33737. wget https://curl.haxx.se/download/curl-???.tar.gz
tar -xf curl-???.tar.gz
cd curl-???
./configure --enable-static --host = i386-unknown-none -disable-shared
make
ls ./lib/.libs/libcurl.a # This is where we will link 3r3737. 3r33737.  3r3778. 3r3111. Mesa /OpenGL 3r33737.  3r3778. 3r33766. 3r3116. Mesa - this is an open source framework for working with graphics, a number of interfaces are supported (OpenCL, Vulkan and others), but in this case we are interested in OpenGL. Porting such a large framework is the topic of a separate article. I will confine myself only to the fact that Embox Mesa3D already exists in the OS :) Of course, any implementation of OpenGL will work here. 3r33767. 3r33737.  3r3778.

SDL 3r33737. 3r33737.  3r3778. 3r33766. SDL - This is a cross-platform framework for working with input devices, audio and graphics. 3r33767. 3r33737.  3r3778. 3r33766. So far, we are slaughtering everything except graphics, and for frame rendering, we will write stub functions to see when they will be called. 3r33767. 3r33737.  3r3778. 3r33766. Backends for working with graphics are set in r3r3743. SDL2-??? /src /video /SDL_video.c . 3r33767. 3r33737.  3r3778. 3r33766. It looks like this: 3r33737.  3r3778. 3r33737. 3r33737. /* Available video drivers * /
static VideoBootStrap * bootstrap[]= {
#if SDL_VIDEO_DRIVER_COCOA
& COCOA_bootstrap,
#endif
#if SDL_VIDEO_DRIVER_X11
& X11_bootstrap,
#endif
3r3778.} 3r3737. 3r33737.  3r3778. 3r33766. In order not to bother with the "normal" support of the new platform, just add your VideoBootStrap 3r33767. 3r33737.  3r3778. 3r33766. For simplicity, you can take something as a basis, for example, 3r33743. src /video /qnx /video.c or src /video /raspberry /SDL_rpivideo.c , but first we will make the implementation almost empty at all: 3r33737.  3r3778. 3r33737. 3r33737. /* SDL_sysvideo.h * /
typedef struct VideoBootStrap
{
const char * name; 3r3778. const char * desc; `` `3r3778. int (* available) (void); 3r3778. SDL_VideoDevice * (* create) (int devindex); 3r3778.} VideoBootStrap; 3r3778. 3r3778. /* embox_video.c * /
3r3778. static SDL_VideoDevice * createDevice (int devindex)
{
SDL_VideoDevice * device; 3r3778. 3r3778. device = (SDL_VideoDevice *) SDL_calloc (? sizeof (SDL_VideoDevice)); 3r3778. if (device == NULL) {
return NULL; 3r3778.}
3r3778. return device; 3r3778.}
3r3778. static int available () {
return 1; 3r3778.}
3r3778. VideoBootStrap EMBOX_bootstrap = {
"embox", "EMBOX Screen",
available, createDevice
}; 3r3778. 3r33744. 3r3737. 3r33737.  3r3778. 3r33766. Add your VideoBootStrap to array: 3r33737.  3r3778. 3r33737. 3r33737. /* Available video drivers * /
static VideoBootStrap * bootstrap[]= {
& EMBOX_bootstrap,
#if SDL_VIDEO_DRIVER_COCOA
& COCOA_bootstrap,
#endif
#if SDL_VIDEO_DRIVER_X11
& X11_bootstrap,
#endif
3r3778.} 3r3737. 3r33737.  3r3778. 3r33766. In principle, at this stage it is already possible to compile SDL. As with libcurl, the compilation details will depend on the specific build system, but somehow you need to do something like this: 3r33737.  3r3778. 3r33737. 3r33737. ./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
3r3778. make
ls build /.libs /libSDL2.a # This is the file we need 3r3737. 3r33737.  3r3778.

Putting himself Quake

3r33737.  3r3778. 3r33766. Quake3 assumes the use of dynamic libraries, but we will link it statically, like everything else. 3r33767. 3r33737.  3r3778. 3r33766. To do this, set some variables in the Makefile 3r33737.  3r3778. 3r33737. 3r33737. CROSS_COMPILING = 1
USE_OPENAL = 0
USE_OPENAL_DLOPEN = 0
USE_RENDERER_DLOPEN = 0
SHLIBLDFLAGS = -static 3r3737. 3r33737.  3r3778.

First run 3r33737. 3r33737.  3r3778. 3r33766. For simplicity, we will run on qemu /x86. To do this, you need to install it (hereinafter, there will be commands for Debian, for other distributions packages may be called differently). 3r33767. 3r33737.  3r3778. 3r33737. 3r33737. sudo apt install qemu-system-i386 3r3737. 3r33737.  3r3778. 3r33766. And the launch itself: 3r33737.  3r3778. 3r33737. 3r33737. qemu-system-i386-kernel build /base /bin /embox-m 1024 -vga std-select stdio 3r3737. 3r33737.  3r3778. 3r33766. However, when you start Quake, we immediately get the error 3r33737.  3r3778. 3r33737. 3r33737. > 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
3r33744. 3r3737. 3r33737.  3r3778. 3r33766. The error is not displayed by the game, but by the operating system. Debug showed that this error is caused by incomplete SIMD support for x86 in QEMU: some instructions are not supported and generate an exception for an unknown command (Invalid Opcode). 3r33767. 3r33737.  3r3778. 3r33766. This does not happen in Quake itself, but in OpenLibM (this is the library that we use to implement the mathematical functions - 3r33743. Sin () , Expf () 3r34444. And the like). OpenLibm patch to __test_sse () I didn’t do a real SSE check, I just thought there was no support. 3r33767. 3r33737.  3r3778. 3r33766. The above steps are enough to launch, in the console you can see the following conclusion: 3r33737.  3r3778. 3r33737. 3r33737. > quake3
ioq??? linux-x86_64 Nov ???r3r3778. SSE instruction set not available
----- FS_Startup -----
We are looking for the current search path:
//.q3a/baseq3
./baseq3
3r3778. ----------------------
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 ??? point release. Also check your ioq3 file for the baseq3
"
ERROR: couldn't open crashlog.txt
3r3737. 3r33737.  3r3778. 3r33766. Already well, Quake3 is trying to start and even displays an error message! As you can see, it lacks the files in the directory. baseq3 . It contains sounds, textures and all that. Notice, pak0.pk3 should be taken from a licensed CD-ROM (yes, open source does not imply free use). 3r33767. 3r33737.  3r3778.

Preparing the disc 3r33718. 3r33737.  3r3778. 3r33737. 3r33737. sudo apt install qemu-utils
3r3778. # Create a qcow2 image
qemu-img create -f qcow2 quake.img 1G
3r3778. # Add the nbd module
sudo modprobe nbd max_part = 63
3r3778. # Format the qcow2 image and write the required files
there. 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 3r3737. 3r33737.  3r3778. 3r33766. Now you can transfer the block device to qemu 3r33737.  3r3778. 3r33737. 3r33737. qemu-system-i386-kernel build /base /bin /embox-m 1024 -vga std-select stdio-hda quake.img 3r3737. 3r33737.  3r3778. 3r33766. When starting the system, bounce the disk on 3r33743. /mnt and run quake3 in this directory, this time after 3r33737.  3r3778. 3r33737. 3r33737. > mount -t ext4 /dev /hda1 /mnt
> cd /mnt
> quake3
ioq??? linux-x86_64 Nov ???r3r3778. SSE instruction set not available
----- FS_Startup -----
We are looking for 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)
3r3778. ----------------------
4073 files in pk3 files
execing default.cfg
couldn't exec q3config.cfg
Could not exec autoexec.cfg
Hunk_Clear: reset the hunk ok
Com_RandomBytes: using weak randomization
----- Client Initialization -----
Couldn't read q3history. 3r3778. ----- 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 3r3737. 3r33737.  3r3778. 3r33766. This error is again with SIMD in Qemu. This time, the instructions are used in the Quake3 virtual machine for x86. The problem was solved by replacing the implementation for x86 with an interpreted VM (in more detail about the Quake3 virtual machine and, in principle, about architectural features you can read all of 3r3447. In the same article 3r3709.). After that, our functions for the SDL begin to be called, but, of course, nothing happens, because these functions do nothing so far. 3r33767. 3r33737.  3r3778.

Add graphics support

3r33737.  3r3778. 3r33737. 3r33737. static SDL_VideoDevice * createDevice (int devindex) {
3r3778. device-> GL_GetProcAddress = glGetProcAddress; 3r3778. device-> GL_CreateContext = glCreateContext; 3r3778. 3r3778.}
3r3778. /* Here we initialize the OpenGL context * /
SDL_GLContext glCreateContext (_THIS, SDL_Window * window) {
OSMesaContext ctx; 3r3778. 3r3778. /* Here we do OS-dependent initialization - we map video memory, etc. * /
sdl_init_buffers (); 3r3778. 3r3778. /* Next, initialize the Mesa context * /
ctx = OSMesaCreateContextExt (OSMESA_BGRA, 1? ? ? NULL); 3r3778. OSMesaMakeCurrent (ctx, fb_base, GL_UNSIGNED_BYTE, fb_width, fb_height); 3r3778. 3r3778. return ctx; 3r3778.} 3r3737. 3r33737.  3r3778. 3r33766. The second handler is needed to tell SDL which functions to call when working with OpenGL. 3r33767. 3r33737.  3r3778. 3r33766. To do this, we start an array and from start to start we check which calls are missing, something like this: 3r33737.  3r3778. 3r33737. 3r33737. static struct {3r3778. char * proc; 3r3778. void * fn; 3r3778.} embox_sdl_tbl[]= {
{"glClear", glClear},
{"glClearColor", glClearColor},
{"glColor4f", glColor4f},
{"glColor4ubv", glColor4ubv},
{0},
}; 3r3778. 3r3778. 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)) {3r3778. return embox_sdl_tbl[i].fn; 3r3778.}
}
3r3778. printf ("embox /sdl: Failed to find% sn", proc); 3r3778. return 0; 3r3778.} 3r3737. 3r33737.  3r3778. 3r33766. After a few restarts, the list becomes full enough to draw a splash screen and a menu. Fortunately, Mesa has all the necessary functions. The only thing - for some reason, there is no function glGetString () , I had to use instead. _mesa_GetString () . 3r33767. 3r33737.  3r3778. 3r33766. Now when you start the application, the splash screen appears, hooray! 3r33767. 3r33737.  3r3778. 3r33535. 3r33737.  3r3778. 3r? 3530. Add input devices 3r33737.  3r3778. 3r33766. Add keyboard and mouse support to the SDL. 3r33767. 3r33737.  3r3778. 3r33766. To work with events, you need to add a handler 3r33737.  3r3778. 3r33737. 3r33737. static SDL_VideoDevice * createDevice (int devindex) {
3r3778. device-> PumpEvents = pumpEvents; 3r3778. 3r3778.} 3r3737. 3r33737.  3r3778. 3r33766. Let's start with the keyboard. We hang up the function to interrupt key press /release. This function should memorize the event (in the simplest case, we simply write to a local variable, you can optionally use queues), for simplicity, we will only store the last event. 3r33767. 3r33737.  3r3778. 3r33737. 3r33737. static struct input_event last_event; 3r3778. 3r3778. static int sdl_indev_eventhnd (struct input_dev * indev) {
/* While there are new events, overwrite them with the last_event * /
while (0 == input_dev_event (indev, & last_event)) {}
}
3r33744. 3r3737. 3r33737.  3r3778. 3r33766. Then in 3r33743. pumpEvents () process the event and pass it to the SDL: 3r33737.  3r3778. 3r33737. 3r33737. static void pumpEvents (_THIS) {
SDL_Scancode scancode; 3r3778. bool pressed; 3r3778. 3r3778. scancode = scancode_from_event (& last_event); 3r3778. pressed = is_press (last_event); 3r3778. 3r3778. if (pressed) {
SDL_SendKeyboardKey (SDL_PRESSED, scancode); 3r3778.} else {
SDL_SendKeyboardKey (SDL_RELEASED, scancode); 3r3778.}
}
3r33744. 3r3737. 3r33737.  3r3778. 3r? 3593. 3r? 3594. Learn more about key codes and SDL_Scancode 3r335959. 3r? 3596. 3r33766. SDL uses its own enum for key codes, so you have to convert the OS key code to SDL code. 3r33767. 3r33737.  3r3778. 3r33766. The list of these codes is defined in file SDL_scancode.h 3r33767. 3r33737.  3r3778. 3r33766. For example, you can convert the ASCII code like this (not all ASCII characters are here, but these are enough): 3r33767. 3r33737.  3r3778. 3r33737. 3r33737. static int key_to_sdl[]= {
[' ']= SDL_SCANCODE_SPACE,
['r']= SDL_SCANCODE_RETURN,
[27]= SDL_SCANCODE_ESCAPE,
['0']= SDL_SCANCODE_?
['1']= SDL_SCANCODE_?
3r3778.['8']= SDL_SCANCODE_?
['9']= SDL_SCANCODE_?
['a']= SDL_SCANCODE_A,
['b']= SDL_SCANCODE_B,
['c']= SDL_SCANCODE_C,
3r3778.['x']= SDL_SCANCODE_X,
['y']= SDL_SCANCODE_Y,
['z']= SDL_SCANCODE_Z,
}; 3r3778. 3r33744. 3r3737. 3r3774. 3r3774. 3r33737.  3r3778. 3r33766. Everything is on the keyboard, the SDL and Quake itself will do the rest. By the way, about here it turned out that somewhere in the processing of quake key presses uses instructions that are not supported by QEMU, you have to switch to the interpretable virtual machine from the virtual machine for x8? to do this, add BASE_CFLAGS + = -DNO_VM_COMPILED in the makefile. 3r33767. 3r33737.  3r3778. 3r33766. After that, finally, you can solemnly “skip” the screensavers and even start the game (with some error messages :)). It was pleasantly surprised that everything is drawn as it should, albeit with very low fps. 3r33767. 3r33737.  3r3778. 3r33766. 3r33767. 3r33737.  3r3778. 3r33766. Now you can start to support the mouse. Mouse interrupts will require another handler, and event handling will require a bit more complexity. We confine ourselves only to the left mouse button. It is clear that in the same way you can add the right key, wheel, etc. 3r33767. 3r33737.  3r3778. 3r33737. 3r33737. static void pumpEvents (_THIS) {
if (from_keyboard (& last_event)) {
/* Here is our old keyboard handler * /
3r3778.} else {
/* Here we will handle mouse events * /
if (is_left_click (& ​​last_event)) {
/* Left mouse button pressed * /
SDL_SendMouseButton (? ? SDL_PRESSED, SDL_BUTTON_LEFT); 3r3778.} else if (is_left_release (& last_event)) {
/* Left mouse button released * /
SDL_SendMouseButton (? ? SDL_RELEASED, SDL_BUTTON_LEFT); 3r3778.} else {
/* Moving the mouse * /
SDL_SendMouseMotion (? ? ?
Mouse_diff_x (), /* Here we transfer the horizontal displacement of the mouse * /
Mouse_diff_y ()); /* Here we transfer the vertical offset of the mouse * /
}
}
} 3r3737. 3r33737.  3r3778. 3r33766. After that, it is possible to control the camera and shoot, hooray! In fact, this is already enough to play :) 3r33737.  3r3778. 3r33766. Optimization 3r3373749. 3r33737.  3r3778. 3r33766. Cool, of course, that there is a control and some kind of graphics, but such an FPS is completely worthless. Most likely, most of the time is spent on OpenGL (and it is software, and, moreover, SIMD is not used), and the implementation of hardware support is too long and difficult task. 3r33767. 3r33737.  3r3778. 3r33766. We will try to speed up the game with a little blood. 3r33767. 3r33737.  3r3778. 3r33737. Optimize compiler and reduce resolution 3r33737.  3r3778. 3r33766. We assemble the game, all the libraries and the OS itself with 3r3373743. -O3 3r3373744. (if, all of a sudden, someone has spotted it to this place, but does not know what kind of flag it is - in more detail about the GCC optimization flags you can read 3r3708. here ). 3r33767. 3r33737.  3r3778. 3r33766. In addition, we use the minimum resolution - 320x240 to facilitate the work of the processor. 3r33767. 3r33737.  3r3778. 3r33737. KVM 3r33737.  3r3778. 3r33766. KVM (Kernel-based Virtual Machine) allows you to use hardware virtualization (Intel VT and AMD-V) to improve performance. Qemu supports this mechanism, you need to do the following to use it. 3r33767. 3r33737.  3r3778. 3r33766. First, you need to enable virtualization support in the BIOS. I have a motherboard Gigabyte B450M DS3H, and AMD-V is enabled via M.I.T. -> Advanced Frequency Settings -> Advanced CPU Core Settings -> SVM Mode -> Enabled (Gigabyte, what's wrong with you?). 3r33767. 3r33737.  3r3778. 3r33766. Then we put the necessary package and add the appropriate module 3r33767. 3r33737.  3r3778. 3r33737. 3r33737. sudo apt install qemu-kvm
sudo modprobe kvm-amd # Or kvm-intel 3r3737. 3r33737.  3r3778. 3r33766. Everything, now it is possible to transfer qemu flag -enable-kvm (or -no-kvm so as not to use hardware acceleration). 3r33767. 3r33737.  3r3778. 3r33748. The result is
3r33737.  3r3778. 3r3752. 3r3753. 3r3754. 3r3755. 3r3756. 3r3774. 3r3774. 3r3774. 3r33737.  3r3778. 3r33766. The game has started, the graphics are displayed as needed, the control is working. Unfortunately, the graphics are drawn on the CPU in one stream, also without SIMD, because of the low fps (2-3 frames per second) it is very inconvenient to manage. 3r33767. 3r33737.  3r3778. 3r33766. The porting process was interesting. Maybe in the future it will be possible to run quake on a platform with hardware graphic acceleration, but for now I’ll stop on what is. 3r33767. 3r3774. 3r3778. 3r3778.
! function (e) {function t (t, n) {if (! (n in e)) {for (var r, a = e.document, i = a.scripts, o = i.length; o-- ;) if (-1! == i[o].src.indexOf (t)) {r = i[o]; break} if (! r) {r = a.createElement ("script"), r.type = "text /jаvascript", r.async =! ? r.defer =! ? r.src = t, r.charset = "UTF-8"; var d = function () {var e = a.getElementsByTagName ("script")[0]; e.parentNode.insertBefore (r, e)}; "[object Opera]" == e.opera? a.addEventListener? a.addEventListener ("DOMContentLoaded", d,! 1): e.attachEvent ("onload", d ): d ()}}} t ("//mediator.mail.ru/script/2820404/"""_mediator") () (); 3r3772. 3r3778. 3r3774. 3r3778. 3r3778. 3r3778. 3r3778.
+ 0 -

Add comment