Architecture

Where the Renderer Boundary Lives: Stride and VOID

Stride and VOID solve graphics abstraction at different layers.

Graphics abstraction sounds like one problem until two engines solve it in completely different places.

I have been looking through Stride's rendering architecture while working on VOID's renderer contracts. The useful part of the comparison was not deciding which engine has the better renderer. Stride and VOID have very different scopes. What interested me was where each project decides the abstraction should stop.

Stride has a deep graphics abstraction inside a much larger rendering stack. VOID exposes a smaller graphics device too, but the main extension boundary sits one level higher: the renderer itself is replaceable.

That difference changes a surprising amount of the architecture.

The Same Problem, a Different Boundary

Both projects need engine code to describe rendering without scattering API-specific calls everywhere.

Stride solves that with a broad graphics layer. Its source exposes concepts such as GraphicsDevice, CommandList, pipeline state, resources, and backend-specific implementations for graphics APIs such as Vulkan and Direct3D.

That is a strong fit for Stride. It is a full 3D engine with a rendering pipeline that needs to model far more GPU behavior than a small 2D framework normally would.

VOID has a different goal. I wanted its normal rendering systems to stay simple while still allowing the entire backend to be replaced from outside the engine.

So the public boundary became three contracts:

IRendererBackend
IGraphicsDevice
IRendererContext

Those interfaces are related, but they solve three different problems.

Stride Goes Deeper at the Graphics Layer

Stride's abstraction is much richer at the GPU level. A GraphicsDevice represents the graphics device, while CommandList handles rendering commands and resource binding. Pipeline state and other graphics resources sit in the same engine-level model.

The source also contains API-specific implementations of shared graphics classes. For example, Stride has Vulkan and Direct3D12 versions of CommandList and PipelineState. Its build infrastructure accounts for graphics-API-specific assemblies as well.

That means the graphics API can change while higher-level Stride systems continue targeting the same Stride graphics model.

It is a deeper abstraction than VOID needs. That is not a criticism. Stride has to support a much larger renderer, including workloads and features VOID simply does not try to model.

VOID Makes the Renderer Itself Replaceable

VOID still needs renderer-neutral buffers, textures, shaders, render targets, uploads, clears, and draw commands. That is the job of IGraphicsDevice.

But IGraphicsDevice is not the top of the boundary.

IRendererBackend owns the renderer lifecycle, graphics device, capabilities, presentation, resize behavior, and the default 2D shader used by VOID's built-in rendering path.

The built-in OpenGL renderer is one implementation of that contract.

A different backend can provide another implementation without making SpriteBatcher, PrimitiveBatcher, cameras, render targets, or the rest of the 2D layer understand that graphics API directly.

From game startup, selecting a renderer can stay small:

GameSettings.Instance
    .SetRenderer<MyRenderer>()
    .Build();

The important part is that the line changes more than the class eventually receiving a draw call.

The Renderer Is Chosen Before the Window Exists

This was one of the parts I really wanted to get right. A Vulkan backend may need a Vulkan-capable window and native surface handles, OpenGL needs an OpenGL context, and Metal has its own requirements.

So VOID chooses the renderer first.

IRendererBackend exposes its required window flags and graphics version before initialization. VOID uses those requirements when creating the native window, then creates an IRendererContext, and only after that initializes the renderer.

If VOID always created an OpenGL window first and asked which renderer the game wanted afterward, the renderer would not actually be replaceable. The platform layer would already have committed to one backend.

This is where IRendererContext matters. It gives a backend the platform services it may need, including function lookup, buffer swapping where appropriate, and borrowed native handles, without exposing VOID's internal SDL implementation as the extension contract.

Why Keep the Public Surface Small?

It would be easy to keep adding lower-level concepts until VOID had a graphics abstraction that looked more like a modern explicit API.

I do not want that unless VOID actually needs it.

A 2D framework should not force every renderer author to implement a giant API just because a larger engine might need those concepts. The public contract should contain enough information to preserve rendering intent without pretending VOID is a universal GPU library.

That is why the graphics device deals in renderer-neutral descriptions and commands, while backend-specific details stay behind the implementation.

OpenGL can cache state internally. A Vulkan renderer could manage command buffers, synchronization, descriptor state, and swapchain details internally. A Direct3D renderer could organize the same engine-level intent around its own API.

The engine should know what needs to be drawn. The backend should know how its API makes that happen.

What Stride Exposes That VOID Does Not

The tradeoff is straightforward: Stride gives developers a much deeper graphics model.

Its rendering architecture includes concepts VOID intentionally does not expose at the same level. That gives Stride more room for complex 3D rendering systems and direct control over a broader set of GPU behavior.

VOID's boundary is narrower because its target is narrower.

The goal is not to reproduce every graphics concept behind a new set of names. The goal is to give VOID's 2D systems enough abstraction that the renderer can be swapped without dragging a specific API through the rest of the engine.

If VOID eventually needs a new renderer-neutral concept, I would rather add it because a real backend requires it than predict every possible need ahead of time.

The External Backend Boundary Is the Important Part

This is the biggest architectural difference I see between the two projects.

Stride's graphics abstraction is deeply integrated into Stride.Graphics and its rendering stack. Looking through the source, adding another graphics API means extending that engine-level graphics implementation across the places that need backend-specific behavior.

VOID deliberately exposes the renderer boundary as a public extension point.

An external backend can implement IRendererBackend, expose its own IGraphicsDevice, use IRendererContext for platform access, and register itself during game configuration.

That was the design target from the beginning of the SDL3 and Silk.NET migration. I did not want OpenGL to be hidden behind interfaces. I wanted OpenGL to be one renderer that happens to ship with VOID.

Neither Approach Is Trying to Solve the Exact Same Problem

I do not think one architecture is universally better. Stride is solving a much larger rendering problem and gives developers considerably more low-level graphics control.

VOID is solving a narrower problem: keep a 2D framework simple while making the entire renderer genuinely replaceable.

That difference in scope leads to two different abstraction boundaries.

A short way I have started thinking about it is:

Stride abstracts graphics APIs.
VOID abstracts the renderer itself.

That sentence is intentionally simplified. Stride's renderer is much broader than one line can describe, and VOID still has a renderer-neutral graphics device underneath its backend contract.

But it captures the distinction I find useful: Stride builds a deep engine-owned graphics model across APIs, while VOID tries to make the renderer implementation itself a clean replaceable seam.

Why the Boundary Matters

Abstraction is not only about avoiding API names in high-level code. It determines who is allowed to replace what.

If the graphics API is abstracted but the renderer architecture remains fixed inside the engine, developers can target multiple APIs through the engine's graphics model.

If the renderer itself is the extension point, a developer can replace a larger chunk of the rendering stack while the rest of the framework keeps the same 2D-facing behavior.

Those are both valid designs. They optimize for different kinds of control.

For VOID, the second one fits the project better. I want the framework to provide a capable default renderer without turning that default into a permanent architectural decision.

OpenGL is what VOID ships today.

It is not what a VOID renderer has to be.

This comparison is based on the current Stride and VOID source as of September 2026. The projects have different scopes, so the interesting part is the abstraction boundary rather than a feature-for-feature ranking.