Quake 2 Source Code Review 2/4
Quake2 is made of one kernel and two modules loaded at runtime: Game and Renderer.
The very interesting part is that anything can be pluged into the kernel via polymorphism.
Before reading further make sure you have a good understanding of how virtual memory works with this great article (mirror)
just in case you need to refresh your memory.
Quake 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
Dynamic linking provided numerous advantages:
- Renderer:
- Clean Quake2 kernel code, limited code entropy, no crazy
#ifdef
all 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.exe
holds a structureref_OpenGL_t
with function pointers toNULL
(in grey).- The DLL module (
ref_opengl.dll
) also holds a structurekernel_fct_t
with 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 (); .... } }