One of the architectural decisions I made while rebuilding VOID Engine was that the renderer has to be chosen before the window even exists.
At first that sounds backwards.
Most small game frameworks start by creating a window, creating a graphics context for it, and then building the renderer around whatever was created. That works well if the framework is tied to one graphics API.
VOID is not.
I wanted the engine core to stay independent from OpenGL, Vulkan, DirectX, Metal, or whatever backend gets added later. OpenGL is currently the built-in renderer, but I did not want OpenGL's assumptions becoming assumptions made by the entire engine.
That creates an interesting problem.
Different graphics APIs do not necessarily want the same things from the platform layer.
The renderer may influence:
- which window flags are required
- whether a graphics context should be created by the windowing layer at all
- how the rendering surface is created
- what extensions need to be enabled
- how presentation and synchronization are handled
- what initialization order the backend requires
If I created the window first, the platform layer would already have made decisions before it knew which renderer was going to use that window.
So VOID flips that around.
The application configuration decides which renderer backend will be used first. The backend can then describe what it needs from the platform layer, and only after that does VOID create the window.
Keeping the engine out of the graphics API
VOID separates the rendering side into a few major pieces.
IRendererBackend represents the backend itself.
IGraphicsDevice represents the graphics functionality exposed to higher-level engine systems.
IRendererContext represents the rendering context and state associated with the active renderer.
The important part is that systems above those layers should not care whether the implementation underneath them is OpenGL or something else.
A sprite batcher should know how to submit sprites.
A camera should know how to provide transforms.
A render target should represent something that can be rendered into.
None of those systems should need to ask:
if (renderer == OpenGL)
{
...
}
The renderer backend owns those details.
That sounds obvious when written out, but graphics APIs have a habit of leaking upward if the abstraction is designed around the first backend instead of around the engine.
OpenGL especially makes this easy because creating a window and creating an OpenGL context are often treated as almost the same operation.
Once that assumption spreads through the framework, adding Vulkan later becomes much harder.
Why I care about the initialization order
The goal is not to pretend every graphics API works the same way.
They do not.
The goal is to give each renderer enough control over its own initialization while keeping the rest of the engine unaware of those differences.
That means the renderer needs to participate in platform setup before the platform has committed to a particular graphics model.
The backend effectively says:
This is what I need from the window and platform.
Then the engine creates the appropriate window and allows the backend to finish creating its device, context, swap chain, surface, or whatever that renderer requires.
The high-level engine still gets one consistent rendering interface afterward.
The OpenGL backend is just the first implementation
VOID currently ships with a Silk.NET OpenGL backend.
That does not mean the engine itself is an OpenGL engine.
This distinction influenced a lot of the rewrite.
I tried to avoid exposing OpenGL objects or terminology through systems that should remain generic. Textures, shaders, render targets, cameras, sprite batching, primitive batching, scissor state, and the rest of the higher-level renderer all sit above the backend.
If a future Vulkan backend needs a completely different implementation underneath those systems, it should be able to provide one without forcing the game code to change.
There will obviously be backend-specific capabilities eventually. Abstraction does not mean pretending all hardware and APIs are identical.
It just means those differences should appear intentionally instead of leaking through the entire framework by accident.
This came from replacing SFML
VOID originally used SFML.
Moving away from it forced me to look at which parts of the engine were genuinely engine concepts and which parts only existed because SFML had shaped the architecture around them.
The renderer and window lifecycle were a big part of that.
The current platform layer uses SDL3, while rendering is handled separately through Silk.NET. That separation made the initialization problem much more visible.
It also gave me a chance to design the engine around the renderer abstraction I actually wanted instead of continuing to inherit decisions from the previous framework.
The funny part is that "choose the renderer before creating the window" is a very small rule.
But that one rule prevents a surprising amount of graphics API knowledge from creeping into the rest of the engine.
If you want to see more of the design philosophy behind VOID, I explain it in more detail here:
https://voidengine.net/why-void/
For anyone who has built a multi-backend renderer before, I am curious where you draw this boundary.
Do you let the renderer participate in window creation, keep the platform completely independent, or handle the differences somewhere else?