Projects Assemblies

ctf gamex86.dll

game gamex86.dll

quake2 quake.exe

ref_soft ref_soft.dll

ref_gl ref_gl.dll

Note: ctf and game projects overwrite each other, more on this later.

Note 2: the assembly can not be performed at first because of the absence of the DirectX header:

fatal error C1083: Can not open include file: 'dsound.h': No such file or directory

I installed Direct3D SDK and Microsoft SDK (for MFC), and everything was fine compiled.

Software erosion: it seems that what happened to the Quake code base begins to occur and With Quake 2: it is not possible to open the working environment in Visual Studio 2010. You must use VS 2008.

Note: if an error occurs after compilation

"Could not Fall back to software refresh! "

it means that the DLL renderer could not be loaded properly. But this is easy to fix:

the Quake2 kernel loads its two DLLs using the win32 API: LoadLibrary. If the DLL is not the one that is expected, or you can not resolve the DLL dependency, then the failure occurs without displaying an error message. Therefore:

Connect all five projects with one library – right-click on each project -> properties -> C / C ++: make sure that "runtime library" = Multi-threaded Debug DLL Configuration "Debug", otherwise use release).

If you use the version of quake2 released by id Software, this will fix the bug.

If you are using my version: I added the ability to save screenshots in PNG to the engine, so you also need to compile libpng and libz (they are in a subdirectory) . Make sure that the Debug DLL configuration is selected. When building, do not forget to put the DLL libpng and zlib in one folder with quake2.exe.

The architecture of Quake2

When I read the Quake 1 code, I divided the article (its translation is here) into three parts: "Network", "Forecasting" and "Visualization". This approach would be appropriate for Quake 2, because the engine is not fundamentally different at the core, but improvements are easier to notice if you divide the article into three main types of projects:

Type Project Information about the project The main engine (. Exe) A kernel that executes the module call and exchanges information between the client and the server. In the working environment, this is the project quake2 . The renderer module (.dll) Responsible for visualization. The working environment contains a software renderer ( ref_soft ) and an OpenGL renderer ( ref_gl ). The game module (.dll) Responsible for the gameplay (game content, weapons, monster behavior …). The working environment contains a single-user module ( game ) and the Capture The Flag module ctf ).

Quake2 has a single-threaded architecture, the entry point is in win32 / sys_win.c . The method WinMain performs the following tasks:

game_export_t * ge; // Contains function pointers to the dll of the game Refexport_t re; // Contains function pointers on the renderer dll WinMain () // From quake2.exe { Qcommon_Init (argc, argv); While (1) { Qcommon_Frame { SV_Frame () // Server code { // Not used as a server in network mode If (! Svs.initialized) Return; // Go to game.dll via the function pointer Ge-> RunFrame (); } CL_Frame () // Client code { // If only the server does not render anything If (dedicated-> value) Return; // Go to the renderer.dll through the function pointer Re.BeginFrame (); // [...] Re.EndFrame (); } } } }

A completely disassembled cycle can be found in my notes.

You may ask: "Why do we need such changes in architecture?" To answer this question, let's look at all versions of Quake from 1996 to 1997:

A lot of executable files were created, and each time it was required to forge or customize the code through the preprocessor #ifdef . It was complete chaos, and to get rid of it, it was necessary: ​​

Unify client / server.

Collect a kernel that can load interchangeable modules.

The new approach can be illustrated in this way:

Two major improvements:

Client-server unification: there was no longer one exe for the client And another for the server, the main executable could work as a server, client, or both. Even in single-user mode, there was still a client and server running in the same executable file (although in this case, the data was exchanged through a local buffer, rather than via TCP IP / IPX).

there was no longer one exe for the client And another for the server, the main executable could work as a server, client, or both. Even in single-user mode, there was still a client and server running in the same executable file (although in this case, the data was exchanged through a local buffer, rather than via TCP IP / IPX). Modularity: part of the code became interchangeable due to dynamic connection. The renderer and game code became modules that could be switched without changing the Quake2 kernel. Thus, by using two structures containing function pointers, polymorphism was achieved.

These two changes made the code base extremely elegant and more readable than Quake 1, which suffered from code entropy.

In terms of implementation, DLL projects should only disclose one method GetRefAPI for renderers and GetGameAPI for the game (look at the .def file in the "Resource Files" folder):

reg_gl / Resource Files / reg_soft.def

] EXPORTS GetGameAPI

When the kernel needs to load the module, it loads the DLL into the process space, gets the address GetRefAPI from GetProcAddress gets the required function pointers, and that's it.

Interesting Fact: when the game is local, communication between the client and the server is not performed via sockets. Instead, the commands are discarded in the loopback buffer using NET_SendLoopPacket in the client side of the code. The server reconstructs the command from the same buffer using NET_GetLoopPacket .

Random fact: if you've ever seen this photo, you are probably wondering , That the giant display was used by John Carmack around 1996:

It was a 28-inch InterView 28hd96 monitor manufactured by Intergraph. This beast provided a resolution of up to 1920×1080, which is quite impressive for 1995 (more details can be read here (mirror)).

Nostalgic video from Youtube: Workstations Intergraph Computer Systems.

Complement: this article seems to have inspired someone on geek.com because they wrote an article "John Carmack created Quake in 1995 on a 28 inch monitor 16: 9 1080p" (

Addition: it seems that John Carmack still used this monitor when developing Doom 3.

Visualization

The software renderer modules ref_soft ) and the hardware accelerated renderer ( ref_gl ) were so large that I wrote about them separate sections.

Again, it is noteworthy that the kernel even I did not know which renderer was connected: it just called the function pointer in the structure. That is, the rendering pipeline was completely abstracted: and who needs this C ++?

Interesting fact: id Software still uses the coordinate system from the game Wolfenstein 3D 1992 ( At least that's how it was in Doom3). It is important to know when reading the source code of the renderer:

In the id system:

Axis X = left / right

Axis Y = front / rear

Axis Z = top / bottom

In the OpenGL coordinate system:

Axis X = left / right

Axis Y = top / bottom

Axis Z = front / back

Therefore, the OpenGL renderer uses the GL_MODELVIEW matrix to "correct" the coordinate system in each frame using the R_SetupGL ( glLoadIdentity + glRotatef ) method.

Dynamic connection

I can say a lot about kernel / module interactions: I wrote a separate section on dynamic connectivity.

Modding: gamex86.dll

Reading this part of the project was not so interesting, but giving up Quake-C for the compiled module led to two good ones and one very bad effect.

Bad:

Brought portability, the game module must be recompiled for different platforms with certain linker parameters.

Good:

Speed: The Quake-C language of Quake1 was an interpreted code, but the module of the dynamic library Quake2 gamex86.dll is native.

is native. Freedom: modders have access to ALL, not just to what was available through Quake-C.

Interesting fact: it's ironic that id Software has moved to Quake3 back to using a virtual machine (QVM) for game, artificial intelligence and modding.

My quake2

In the process of hacking, I slightly changed the source code of Quake2. I highly recommend adding a DOS console to see the output of the printf output in the process, rather than pausing the game to study the Quake console:

Adding a DOS-style console to the Win32 window is quite simple:

] // sys_win.c Int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { AllocConsole (); Freopen ("conin $", "r", stdin); Freopen ("conout $", "w", stdout); Freopen ("conout $", "w", stderr); ConsoleHandle = GetConsoleWindow (); MoveWindow (consoleHandle, 1,1,680,480,1); Printf ("[sys_win.c] Console initialized. N"); ... }

Since I ran Windows on a Mac with Parallels, it was difficult to press "printscreen" when playing a game. To create the screenshots, I set the "*" key from the digital block:

// keys.c If (key == '*') { If (down) // Avoid automatic retry! Cmd_ExecuteString ("screenshot"); }

And, finally, I added a lot of comments and diagrams. Here is my full source code:

archive.

Memory management

Doom and Quake1 had their own memory manager called "Zone Memory Allocation": it ran malloc and the memory block was managed using a list of pointers. Memory Zone (Memory Zone) could be marked to quickly clear the desired memory category. Zone Memory Allocator ( common.c: Z_Malloc, Z_Free, Z_TagMalloc, Z_FreeTags ) was left in Quake2, but it's pretty useless:

Marks are not used and memory allocation / deallocation is performed on top Malloc and free (I have no idea why id Software was entrusted with this work C Standard Library).

and (I have no idea why id Software was entrusted with this work C Standard Library). The overflow detector (using the constant Z_MAGIC ) is also never used

It is still very useful to measure memory consumption with the attribute size in the header inserted before the allocation of each memory block:

#define Z_MAGIC 0x1d1d Typedef struct zhead_s { Struct zhead_s * prev, * next; Short magic; Short tag; // for group cleaning Int size; } Zhead_t;

The system of caching of surfaces has its own memory manager. The amount of distributed memory depends on the resolution and is determined by a strange formula, which, however, very effectively protects against debris: