Skip to content

Custom Renderers

shmellyorc edited this page Sep 13, 2026 · 2 revisions

Custom Renderers

VOID's renderer is a public extension point.

The built-in backend uses Silk.NET.OpenGL, but it is selected through the same renderer contract available to your own backend.

A custom renderer can live in your game project, another assembly, or a separate NuGet package.

This page is a usage guide, not a full graphics-API tutorial. You still need to implement the GPU work required by Vulkan, Direct3D, OpenGL, or another API.

Selecting a Renderer

Register a factory before Build():

var settings = GameSettings.Instance
    .SetAppCompany("MyStudio")
    .SetAppName("MyGame")
    .SetRenderer(() => new MyRenderer())
    .Build();

If your renderer has a public parameterless constructor:

var settings = GameSettings.Instance
    .SetRenderer<MyRenderer>()
    .Build();

If no renderer is supplied, VOID uses its built-in OpenGL backend.

What a Renderer Implements

Your backend implements IRendererBackend.

A simplified shape looks like this:

public sealed class MyRenderer : IRendererBackend
{
    public string Name => "My Renderer";
    public GraphicsApi Api => GraphicsApi.Custom;
    public GraphicsVersion Version => new(1, 0);

    public RendererWindowFlags RequiredWindowFlags
        => RendererWindowFlags.None;

    public RendererCapabilities Capabilities { get; private set; }

    public IGraphicsDevice Device { get; private set; }

    public IGraphicsShaderProgram Default2DShader { get; private set; }

    public bool IsInitialized { get; private set; }

    public void Initialize(IRendererContext context)
    {
        // Create the graphics device using the window/platform
        // services exposed by context.

        // Device = new MyGraphicsDevice(...);
        // Default2DShader = Device.CreateShaderProgram(...);
        // Capabilities = ...;

        IsInitialized = true;
    }

    public void BeginFrame(Color clearColor)
    {
        Device.Clear(clearColor);
    }

    public void EndFrame()
    {
        // Submit/present using your backend.
    }

    public void Resize(int width, int height)
    {
        // Recreate swapchain/backbuffer resources if required.
    }

    public void Dispose()
    {
        Device?.WaitIdle();
        Default2DShader?.Dispose();
        Device?.Dispose();
    }
}

A real renderer also supplies implementations of VOID's graphics-resource contracts:

  • IGraphicsDevice
  • IGraphicsBuffer
  • IGraphicsTexture
  • IGraphicsRenderTarget
  • IGraphicsShaderProgram

VOID's batching layer submits backend-neutral RenderCommand values to the active IGraphicsDevice.

Camera transforms use VOID's own matrix type. See Matrix and Camera → Matrix-Based Camera Pipeline.

Why RequiredWindowFlags Exists

VOID creates the renderer object before it creates the native window.

That lets your backend tell the platform layer what the window must support:

public RendererWindowFlags RequiredWindowFlags
    => RendererWindowFlags.Vulkan;

Available flags are:

RendererWindowFlags.None
RendererWindowFlags.OpenGL
RendererWindowFlags.Vulkan
RendererWindowFlags.Metal

Examples:

Backend Typical Flag
OpenGL OpenGL
Vulkan Vulkan
Direct3D None
Custom software/native backend Usually None

The public contract contains a Metal flag, but the current SDL host does not yet create/expose an SDL Metal view or CAMetalLayer. Cocoa currently exposes the native NSWindow*. A Metal backend that needs a Metal view/layer requires additional host plumbing today.

IRendererContext

VOID passes an IRendererContext to Initialize.

It provides renderer-facing platform services without exposing VOID's internal SDL classes.

Useful properties:

public void Initialize(IRendererContext context)
{
    Console.WriteLine(context.PlatformBackend);
    Console.WriteLine(context.WindowSize);
    Console.WriteLine(context.RenderSize);
}

OpenGL-style Contexts

A renderer using an SDL-created OpenGL context can resolve functions through:

nint address =
    context.GetProcAddress("glCreateShader");

Presentation:

context.SwapBuffers();

Swap interval:

context.TrySetSwapInterval(1);

The built-in OpenGL backend uses this path.

Native Window Handles

Backends such as Vulkan or Direct3D often need a real operating-system window/display handle.

Do not interpret WindowSystemHandle as one of those handles. It is an opaque VOID/SDL window pointer.

Use:

if (context.TryGetNativeHandle(
        NativeWindowHandleKind.Window,
        out nint window))
{
    // Use the borrowed native handle.
}

Always check:

context.PlatformBackend

before deciding what the returned pointer/value means.

Current Native Handle Map

Platform backend Window Display Instance Surface
Windows HWND HMONITOR HINSTANCE HDC
X11 / XWayland X11 Window/XID Display*
Wayland wl_surface* wl_display* wl_surface*
Cocoa NSWindow*

Unsupported requests return false and a zero handle.

Native handles returned by VOID are borrowed. Your renderer must not free, destroy, or take ownership of them.

For the normal game-facing display/window API, see Window & Displays. Renderer plugins should use IRendererContext only for backend integration.

Example: Windows Handle

public void Initialize(IRendererContext context)
{
    if (context.PlatformBackend != NativeWindowBackend.Windows)
        throw new PlatformNotSupportedException();

    if (!context.TryGetNativeHandle(
            NativeWindowHandleKind.Window,
            out nint hwnd))
    {
        throw new InvalidOperationException(
            "VOID did not provide an HWND.");
    }

    // Create your D3D swap chain/device using hwnd.
}

Example: X11 Handles

public void Initialize(IRendererContext context)
{
    if (context.PlatformBackend != NativeWindowBackend.X11)
        throw new PlatformNotSupportedException();

    if (!context.TryGetNativeHandle(
            NativeWindowHandleKind.Window,
            out nint xid) ||
        !context.TryGetNativeHandle(
            NativeWindowHandleKind.Display,
            out nint display))
    {
        throw new InvalidOperationException(
            "X11 native handles were unavailable.");
    }

    // xid is the X11 Window value.
    // display is Display*.
}

This exact bridge is exercised by the renderer smoke test in:

Src/SmokeTest/Program.cs

Linux Backend Choice

On Linux you can choose which window-system backend VOID asks SDL to initialize:

GameSettings.Instance
    .SetLinuxWindowBackend(
        LinuxWindowBackend.X11ThenWayland);

Options:

LinuxWindowBackend.X11ThenWayland // VOID default
LinuxWindowBackend.Auto
LinuxWindowBackend.X11
LinuxWindowBackend.Wayland

This matters to a custom renderer because PlatformBackend and available native handles will change depending on what SDL actually initialized.

Device Responsibilities

IGraphicsDevice is the bridge between VOID's higher-level rendering and your API.

A backend must be able to:

  • create/update vertex and index buffers
  • create/update textures
  • create shader programs
  • create render targets
  • bind render targets
  • clear
  • execute RenderCommand
  • wait for GPU idle during synchronization/teardown

VOID deliberately keeps API-specific GPU objects behind the public interfaces.

For example, an OpenGL texture object can remain an internal backend class while game code only sees VOID's Texture asset and renderer-neutral IGraphicsTexture.

Default 2D Shader

Your renderer supplies:

IGraphicsShaderProgram Default2DShader

VOID uses this for its normal 2D batching/presentation path when game code has not selected a custom shader.

Your backend must provide an equivalent shader in the representation your API expects.

The built-in 2D path supplies uViewProjection as VOID's 4x4 Matrix, plus the texture/use-texture state described by the renderer contract.

Shader Languages

VOID can describe shader sources as:

ShaderLanguage.Glsl
ShaderLanguage.Hlsl
ShaderLanguage.SpirV
ShaderLanguage.Dxil
ShaderLanguage.Msl
ShaderLanguage.Custom

A backend decides which of those it supports.

Do not assume every custom renderer can consume every .shader file. If you plan to ship multiple backends, design your shader asset strategy around the representations those backends support.

Renderer Smoke Test

VOID keeps a small fake renderer in Src/SmokeTest.

Its purpose is to exercise the same public path a third-party renderer uses:

GameSettings.SetRenderer(...)
        ↓
IRendererBackend.Initialize(...)
        ↓
IRendererContext
        ↓
native platform handles

It is useful when changing the renderer/plugin boundary because it can catch a bridge regression even if the built-in OpenGL renderer still works.


Back to Rendering · Window & Displays · Back to Home

Clone this wiki locally