Fabien Sanglard's non-blog
Quake 2 Source Code Review 2/4
September 16th, 2011Quake 2 Source Code Review 1/4 (Intro)
Quake 2 Source Code Review 2/4 (Polymorphism)
Quake 2 Source Code Review 3/4 (Software Renderer)
Quake 2 Source Code Review 4/4 (OpenGL Renderer)
Polymorphism with C and Dynamic Linking
Quake2 is made of one kernel and two modules loaded at runtime: Game and Renderer. Before reading further make sure you have a good understanding of
how virtual memory works. Here is a great article (mirror)
just in case you need to refresh your memory.
Dynamic linking provided numerous advantages:
- Renderer:
- Clean Quake2 kernel code, limited code entropy, no crazy
#ifdefall over the place . - Ship game with multiple renderer (software,openGL).
- Renderer can be changed while the game is running.
- Allowed to provide new renderer for hardware release after the game shipped (Glide, Verity).
- Game mod:
- More capabilities to mod makers, the entire game could be altered via game.dll.
- Full native speed for mods, no need to rely on QuakeC and Quake Vitual machine.
- No need to learn QuakeC, the dll were written in C.
But Quake2 was written in C which is not an Oriented Object programming language and the question is then "How do you implement polymorphism with a language which is not OO ?".
The technique is to replicate OO the way JAVA and C++ do it: Using structures containing function pointers.
Hence four structures are used to exchange functions pointers: refimport_t and refexport_t act as container to exchange functions pointers when the renderer module is loaded.
game_import_t and game_export_t are used when the game module is loaded.
A small drawing is better than a long speech
Step 1: In the initial state:
quake2.exeholds a structureref_OpenGL_twith function pointers toNULL(in grey).- The DLL module (
ref_opengl.dll) also holds a structurekernel_fct_twith function pointers toNULL(in grey)
The goal of the process is to exchange function address so each part can call each other.

Step 2: The kernel calling the function populates a structure containing pointers to its own functions and send those values to the DLL.

Step 3: The receiving DLL copies the kernel function pointers and return a structure containing its own function addresses.

The process with the real names is detailled in the two following sections.
Renderer library
The method retrieving the renderer module is VID_LoadRefresh, it is called every frames so Quake can switch renderer
(but the level will have to restart due to preprocessing required by the render).
On the Quake2 kernel side this is what happens:
refexport_t re;
qboolean VID_LoadRefresh( char *name )
{
refimport_t ri;
GetRefAPI_t GetRefAPI;
ri.Sys_Error = VID_Error;
ri.FS_LoadFile = FS_LoadFile;
ri.FS_FreeFile = FS_FreeFile;
ri.FS_Gamedir = FS_Gamedir;
ri.Cvar_Get = Cvar_Get;
ri.Cvar_Set = Cvar_Set;
ri.Vid_GetModeInfo = VID_GetModeInfo;
ri.Vid_MenuInit = VID_MenuInit;
ri.Vid_NewWindow = VID_NewWindow;
GetRefAPI = (void *) GetProcAddress( reflib_library, "GetRefAPI" );
re = GetRefAPI( ri );
...
}
In the code above, Quake2 kernel retrieves the method GetRefAPI function pointer from the renderer dll via GetProcAddress (a win32
built-in method).
On the renderer dll, here is what happen within GetRefAPI:
refexport_t GetRefAPI (refimport_t rimp )
{
refexport_t re;
ri = rimp;
re.api_version = API_VERSION;
re.BeginRegistration = R_BeginRegistration;
re.RegisterModel = R_RegisterModel;
re.RegisterSkin = R_RegisterSkin;
re.EndRegistration = R_EndRegistration;
re.RenderFrame = R_RenderFrame;
re.DrawPic = Draw_Pic;
re.DrawChar = Draw_Char;
re.Init = R_Init;
re.Shutdown = R_Shutdown;
re.BeginFrame = R_BeginFrame;
re.EndFrame = GLimp_EndFrame;
re.AppActivate = GLimp_AppActivate;
return re;
}
At the end of the "handshake", a two ways communication is established between the kernel <-> dll. This is polymorphic because the renderer dll returns its own function addresses within the structure, the Quake2 kernel does not see a difference, it always calls the same function pointer.
Game library
The exact same process goes for the game library, on the kernel side, here is what happen:
game_export_t *ge;
void SV_InitGameProgs (void)
{
game_import_t import;
import.linkentity = SV_LinkEdict;
import.unlinkentity = SV_UnlinkEdict;
import.BoxEdicts = SV_AreaEdicts;
import.trace = SV_Trace;
import.pointcontents = SV_PointContents;
import.setmodel = PF_setmodel;
import.inPVS = PF_inPVS;
import.inPHS = PF_inPHS;
import.Pmove = Pmove;
// 30 function pointer assignation skipped
ge = (game_export_t *)Sys_GetGameAPI (&import);
ge->Init ();
}
void *Sys_GetGameAPI (void *parms)
{
void *(*GetGameAPI) (void *);
//[...]
GetGameAPI = (void *)GetProcAddress (game_library, "GetGameAPI");
if (!GetGameAPI)
{
Sys_UnloadGame ();
return NULL;
}
return GetGameAPI (parms);
}
On the game dll side, here is what happen:
game_import_t gi;
game_export_t *GetGameAPI (game_import_t *import)
{
gi = *import;
globals.apiversion = GAME_API_VERSION;
globals.Init = InitGame;
globals.Shutdown = ShutdownGame;
globals.SpawnEntities = SpawnEntities;
globals.WriteGame = WriteGame;
globals.ReadGame = ReadGame;
globals.WriteLevel = WriteLevel;
globals.ReadLevel = ReadLevel;
globals.ClientThink = ClientThink;
globals.ClientConnect = ClientConnect;
globals.ClientDisconnect = ClientDisconnect;
globals.ClientBegin = ClientBegin;
globals.RunFrame = G_RunFrame;
globals.ServerCommand = ServerCommand;
globals.edict_size = sizeof(edict_t);
return &globals;
}
Using the function pointers
Once the method pointers have been exchanged, polymorphism is enabled. In the code, here the kernel "jumps" in the different modules:
The renderer "jump" in SCR_UpdateScreen:
// this is a quake.exe method, the renderer is abstracted and hence quake2.exe has no idea what renderer is being used.
SCR_UpdateScreen()
{
// re is a struct refexport_t, BeginFrame contains the pointed toward the dll's BeginFrame.
re.BeginFrame( separation[i] );
//From here methods belong to the dll
SCR_CalcVrect()
SCR_TileClear()
V_RenderView()
SCR_DrawStats
SCR_DrawNet
SCR_CheckDrawCenterString
SCR_DrawPause
SCR_DrawConsole
M_Draw
SCR_DrawLoading
re.EndFrame();
//Back to quake.exe methods.
}
The game "jump" in SV_RunGameFrame:
void SV_RunGameFrame (void)
{
sv.framenum++;
sv.time = sv.framenum*100; // don't run if paused
if (!sv_paused->value || maxclients->value > 1)
ge->RunFrame ();
....
}
}
Add a comment
Comments (2)
Just a note though - the expression is supposed to be 'this is what happens,' not 'this is what happen.' (Other than that though, your English is very good!)
Thanks!,
Rich, Gun.io