MEngine (2) System Layer And Main Loop
The system layer of the engine deals with a few things, including a common system interface that wraps up functionality that is normally dependent on OS API's or non-portable library code. This layer of the code is completely conditionally compiled based upon the OS it is being compiled on, and will provide an interface for common engine functions further up in the code, along with the actual main loop itself for Windows and for Linux/Apple systems.
The System Layer
The system layer provides a few advantages for the code, as functions labeled Sys_
are system functions that are guaranteed to operate the same no matter which OS is in use. This way of doing things
also avoids using ugly preprocessor switches for OS conditional compilation, and avoids that pollution in the common and primarily developed code, leaving all the ugliness to the system for handling.
The system layer also deals with windowing (creating and managing the games window and draw surface) as well as OpenGL, which is the graphics API of choice for this engine.
Taking a look at the CMakeLists file for the engine, you can see the parts of the code which are conditionally compiled:
# Check the Operating System and include the appropriate file for execution
if(WIN32)
target_sources(MEngine PRIVATE
"src/sys/win32/winlocal.h"
"src/sys/win32/wglext.h"
"src/sys/win32/winmain.c"
"src/sys/win32/wndproc.c"
"src/sys/win32/syswin.c"
"src/sys/win32/glwnd.c"
"src/sys/win32/wininput.c"
)
elseif(LINUX OR APPLE)
target_sources(MEngine PRIVATE
"src/sys/posix/posixlocal.h"
"src/sys/posix/main.c"
"src/sys/posix/wndcb.c"
"src/sys/posix/sysposix.c"
"src/sys/posix/posixglwnd.c"
"src/sys/posix/posixinput.c"
)
include_directories(src/sys/posix)
endif()
if(LINUX)
target_sources(MEngine PRIVATE
"src/sys/posix/linux/syslinux.c"
)
elseif(APPLE)
target_sources(MEngine PRIVATE
"src/sys/posix/macos/sysmacos.c"
)
endif()
As you can see, Windows, Linux, and MacOS have their own subdirectories which implements their own main loops, any OS specific code, and also implements the sys.h
interface. Effectively what we have here
resembles compile time polymorphism, as every single function inside sys.h
must be implemented by each OS's "system layer".
An example of such functions will include some of the functionality discussed in part 1 here is the DLL/SO/DYLIB game library loading. These operating systems have different ways that a shared library must be loaded, how it must be unloaded, and how functions are pulled from the library.
For example:
- On Windows using Win32, the
LoadLibrary
function must be used from the Win32 API:
void *Sys_LoadDLL(const char *dllname)
{
wchar_t wdllname[SYS_MAX_PATH] = { 0 };
if (!MultiByteToWideChar(CP_UTF8, 0, dllname, -1, wdllname, SYS_MAX_PATH))
return(NULL);
HMODULE libhandle = LoadLibrary(wdllname);
if (!libhandle)
WindowsError();
return(libhandle);
}
- And on Linux/MacOS using POSIX, the
dlopen
function must be used:
void *Sys_LoadDLL(const char *dllname)
{
void *handle = NULL;
handle = dlopen(dllname, RTLD_NOW);
if (!handle)
{
Log_WriteSeq(LOG_ERROR, "Failed to load DLL: %s", dlerror());
dlerror();
return(NULL);
}
return(handle);
}
This is an example of the conditional compilation, on different systems, the Sys_LoadDLL
function is implemented differently, but will perform the same task of loading shared libraries.
There are many different Sys_
functions found to be found in the codebade and I generally create them as I find myself needing such functionality.
The Main Loop
Even though the engine has multiple main loops based on the OS for which its compiled, they both perform the same functionality as well as some additional OS dependent setup work. The main functions primary job is to initialise the engines systems and sub-systems, create a window, handle window messages (keyboard/mouse input, window resizing, etc...), and to run the engine and game loop each frame.
- On Windows, the main function can be found in
src/sys/win32/winmain.c
and since this is a Windows application, it uses theWinMain
function instead of the usual entry point:
int WINAPI wWinMain(HINSTANCE hinst, HINSTANCE hprevinst, PWSTR pcmdline, int ncmdshow)
- And on Linux and Apple systems, the standard
main
function is used, which can be found insrc/sys/posix/main.c
, you can also note that windowing on POSIX systems is done with the GLFW library.
The main loop itself actually looks quite simple, the following code is the main function for Windows systems with some OS specific code left out for clarity:
int WINAPI wWinMain(HINSTANCE hinst, HINSTANCE hprevinst, PWSTR pcmdline, int ncmdshow)
{
if (!Common_Init())
{
Common_Shutdown();
return(1);
}
MSG msg;
while (1)
{
while (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
{
if (!GetMessage(&msg, NULL, 0, 0))
{
Common_Shutdown();
return(0);
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Common_Frame();
}
return(1);
}
And the following code is the main function run on POSIX systems:
int main(int argc, char **argv)
{
if (!Common_Init())
{
Common_Shutdown();
return(1);
}
while (!glfwWindowShouldClose(posixstate.window))
{
Common_Frame();
glfwPollEvents();
}
Common_Shutdown();
return(0);
}
As you can see, other than the windowing method used, the core functionality remains the same: Initialise the common engine libraries and interfaces, poll for window events, and run the combined game and engine frame logic.