Refactoring the Graphics
Mon 18 October 2021Sunday's problems
In a moment when I was feeling particularly inspired to work on Crosslight, I wanted to finally take a crack at 2D sprite animation. After all, I had sprite animation in the first ever game engine I made as described in the Chocolate Engine page.
I went to Crosslight's code, and started writing some ImGui windows to give me some interface to edit sprites with. I designed it to have a slider for the timeline, the keyframes, as well as a window where you can manage the sprite's z-axis and parented sprites to simulate a 2D skeleton (if you wanted the arm of a character to move using just rotations for example.)
I quickly ran into a problem though. As soon as I compiled the ImGui code, I was quickly met with a segmentation fault. I'm not entirely sure why it was a segfault at first, but after quick inspection, it was an ImGui assert telling me that a frame had already started. It was immediately obvious why.
The culprit
for ( const auto& pSys : apSystemManager->GetSystemList() )
pSys->Update( time );
Check it out, you can't even tell what's going on here. It's essentially just a loop that updates all systems that are created in the program. The problem with this, is there is a specific order in which these systems are updated, which prevents me from being able to call ImGui functions outside the Gui system without running in to all kinds of problems. Specifically, the renderer implicitly ends ImGui's frame when it draws to the screen, making it impossible to line up a new ImGui frame with an end frame. I had no idea how to fix this, so it was time for a refactor larger than just moving all Gui elements into the renderer.
The solution
Hey, when I load the client shared object into the program, those globals in the object are protected in their own address space. Who says I can't just make the renderer run in it's own shared object? This would offer portability as well as convenience, as I can just load the object once, and link functions where they are needed.
However, instead of dlsym()
'ing a bunch of
functions from this shared object, I can use some cool preprocessor
macros to automate this. I plan to use them such that a file could
automatically link shared object functions using something like this:
/* Creates function pointers, and a function to call to initialize functions. */
DLLOPEN( graphics )
/* Creates functions x, returning void, and y returning double and taking a double as a parameter. */
DLLEXPORT( DLLFUNC( void, x ), DLLFUNC( double, y, double ) )
void SomeInitFunction()
{
/* Links the functions from "graphics.so". */
init_graphics_shims();
/* You can just call the function like so. */
double result = y( 3.14 );
}
I would make my own "meta compiler", sort of like how Qt has its own. During the compilation, my compiler will parse the macros, and make appropriate functions for them, so as to keep the code clean.
I was initially skeptic about the usage of
this method, because I thought if I used dlopen()
on the same
DLL more than once, it'd create new address spaces for those shared
objects.
It turns out, dlopen()
is really good about avoiding this!
I wrote a simple main loop that does the following:
#include <stdio.h>
#include <dlfcn.h>
int main( const int argc, const char **argv )
{
/* Load the same dll thirteen times. */
for ( int i = 0; i < 13; ++i )
{
void *pHandle = dlopen( "./dll.so", RTLD_LAZY );
if ( !pHandle )
return fprintf( stderr, "Error: %s\n", dlerror( ) );
double ( *sin )( double ) = 0;
*( void** )( &sin ) = dlsym( pHandle, "sin" );
if ( !sin )
return fprintf( stderr, "Error: %s\n", dlerror( ) );
printf( "\n\tdlopen() handle = %p\n", pHandle );
printf( "\tdlsym() handle = %p\n\n", ( void* )sin );
printf( "sin( %f ) = %f\n", 1.57, sin( 1.57 ) );
/* Close the dll. */
dlclose( pHandle );
}
return 0;
}
Output
dlopen() handle = 0xaaaaefd9f2c0
dlsym() handle = 0xffffb6dc4554
sin( 1.570000 ) = 0.975649
dlopen() handle = 0xaaaaefd9f2c0
dlsym() handle = 0xffffb6dc4554
sin( 1.570000 ) = 0.975649
dlopen() handle = 0xaaaaefd9f2c0
dlsym() handle = 0xffffb6dc4554
sin( 1.570000 ) = 0.975649
...
I guess as it turns out, I can open the shared object as many times as I want, and it'll still use the same address space! Granted I don't close the handle, I can use these functions anywhere in the calling code.
I tried using this method in Crosslight's code, and to my surprise, after I got past the undefined symbol error because of a typo, the graphics shared object worked flawlessly.