September 16th, 2011

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:


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:

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 ();
	        
	        ....
        }
    }


Comments

 

Fabien Sanglard @2011