How a frame is rendered in Aether3D?

Aether3D is a component-based game engine supporting modern rendering APIs. Currently there is no lighting, but I'm porting my Forward+ implementation to it soon. While there is no lighting at the moment, there are directional and spot light shadows. In this post I will run through steps to render one frame.

Scene object contains GameObjects. GameObjects containing component types SpriteRendererComponent, MeshRendererComponent and TextRendererComponent will be rendered by those GameObjects that contain CameraComponents. Cameras can render into the screen or into a render-texture. If DirectionalLightComponent or SpotLightComponent has its shadow-flag enabled, those will cause a special camera to render their shadow maps.

Rendering steps:

1. Scene.Render()

This method first does housekeeping work needed to render a frame: resets statistics, begins a new render pass, acquires the next swapchain image depending on API etc.

Then it calculates an axis-aligned bounding box for the whole scene (needed for shadow frustum calculation) and updates transformation matrix hierarchy.

Then it collects game objects with camera components into a container and sorts it depending on camera type (render-texture, normal) and its layer etc.

2. Scene.RenderWithCamera()

First, render-texture cameras are looped and this method is called, once for a normal camera and six times for a cube map camera. Then, shadows are rendererd. Finally, cameras rendering directly into the screen are rendererd. If any cameras also want to render depth and normals into a texture (can be used later in post-processing and lighting effects), it's done after this step.

Camera's clear flag is applied at the beginning of this method (clear color, clear depth, don't clear).

At this point, skybox is rendered.
Then, camera's frustum is calculated.
Now game objects are looped and objects containing sprite renderer or text renderer are rendered. Mesh renderer objects are collected and sorted by their distance, then rendered. They reference a Material which feeds the blending state, culling state, shader and uniforms into the renderer.

3. GfxDevice::Draw()

Everything that's rendered uses a method from GfxDevice namespace:
void ae3d::GfxDevice::Draw( VertexBuffer& vertexBuffer, int startIndex, int endIndex, Shader& shader, BlendMode blendMode, DepthFunc depthFunc,
                            CullMode cullMode )

This method first calculates a pipeline state object (PSO) hash and if it's not found in cache, it creates a new PSO.
On Vulkan and D3D12 renderer, descriptor set is filled with draw parameters and the actual drawing uses vkCmdDrawIndexed() or DrawIndexedInstanced(). 

Future work

There is room for improvement, as the engine is still in its early stages (v0.6 under development).

1. PSO objects are expensive to generate so it would be better to generate them before the main loop.
2. There is no instancing support yet.
3. Too little profiling done so far as the main goal has been to get things to work on all APIs (Vulkan, OpenGL, Metal, D3D12).
4. Handling transparency, this is actually currently in development.


Debugging Graphics


I develop a lot of graphics code using Vulkan, OpenGL, D3D12 and Metal and have found the following methods to make my life easier when something doesn't render right:

Enable the debug layer

Enable your API's debug output and check for runtime errors and warnings and fix them all.



ID3D12Debug* debugController;
const HRESULT dhr = D3D12GetDebugInterface( IID_PPV_ARGS( &debugController ) );
if (dhr == S_OK)
... CreateDevice()...
hr = device->QueryInterface( IID_PPV_ARGS( &infoQueue ) );
infoQueue->SetBreakOnSeverity( D3D12_MESSAGE_SEVERITY_ERROR, TRUE );


See https://www.opengl.org/wiki/Debug_Output


Sidenote: On my system RenderDoc crashes if I try to attach a program that has debug layer enabled.
First you need to install LunarG Vulkan SDK from http://lunarg.com/vulkan-sdk/
I contain my debug layer code inside a namespace like this:

namespace debug
    PFN_vkCreateDebugReportCallbackEXT CreateDebugReportCallback = nullptr;
    PFN_vkDestroyDebugReportCallbackEXT DestroyDebugReportCallback = nullptr;
    PFN_vkDebugReportMessageEXT dbgBreakCallback = nullptr;

    VkDebugReportCallbackEXT debugReportCallback = nullptr;
    const int validationLayerCount = 9;
    const char *validationLayerNames[] =

    VkBool32 messageCallback(
        VkDebugReportFlagsEXT flags,
        uint64_t, size_t, int32_t msgCode,
        const char* pLayerPrefix, const char* pMsg,
        void* )
        if (flags & VK_DEBUG_REPORT_ERROR_BIT_EXT)
            ae3d::System::Print( "Vulkan error: [%s], code: %d: %s\n", pLayerPrefix, msgCode, pMsg );
        else if (flags & VK_DEBUG_REPORT_WARNING_BIT_EXT)
            ae3d::System::Print( "Vulkan warning: [%s], code: %d: %s\n", pLayerPrefix, msgCode, pMsg );

        return VK_FALSE;

    void Setup( VkInstance instance )
        CreateDebugReportCallback = (PFN_vkCreateDebugReportCallbackEXT)vkGetInstanceProcAddr( instance, "vkCreateDebugReportCallbackEXT" );
        DestroyDebugReportCallback = (PFN_vkDestroyDebugReportCallbackEXT)vkGetInstanceProcAddr( instance, "vkDestroyDebugReportCallbackEXT" );
        dbgBreakCallback = (PFN_vkDebugReportMessageEXT)vkGetInstanceProcAddr( instance, "vkDebugReportMessageEXT" );

        VkDebugReportCallbackCreateInfoEXT dbgCreateInfo;
        dbgCreateInfo.pNext = nullptr;
        dbgCreateInfo.pfnCallback = (PFN_vkDebugReportCallbackEXT)messageCallback;
        dbgCreateInfo.pUserData = nullptr;
        VkResult err = CreateDebugReportCallback( instance, &dbgCreateInfo, nullptr, &debugReportCallback );


When creating Vulkan instance, I append VK_EXT_DEBUG_REPORT_EXTENSION_NAME into instanceCreateInfo.ppEnabledExtensionNames.
When creating Vulkan device, I pass validation layers like this: 
deviceCreateInfo.enabledLayerCount = debug::validationLayerCount;
deviceCreateInfo.ppEnabledLayerNames = debug::validationLayerNames;

Use debug names

Debug names appear in graphics debugger tools and validation layer messages so they help you find the object.


You'll need to make sure extension KHR_debug is available before using these functions:
glObjectLabel( GL_TEXTURE, textureHandle, nameLength, name );
glObjectLabel( GL_PROGRAM, shaderHandle, nameLength, name );
glObjectLabel( GL_FRAMEBUFFER, fboHandle, nameLength, name );



ID3D12Resource* texture = ...;
texture->SetName( L"texture" );

If you need to convert a const char* into an LPCWSTR you can do it like this: 
wchar_t wstr[ 128 ];
std::mbstowcs( wstr, my_string.c_str(), 128 );
texture->SetName( wstr );


Many objects have a .label property:
metalTexture.label = @"texture";

Use tools

These tools can be used to verify the rendering process by inspecting textures, render targets, buffers, rasterizer state etc.
RenderDoc is a good debugger for D3D11, OpenGL and Vulkan.
For D3D12 Visual Studio's own debugger is good. You can get it by installing "graphics tools" in Windows 10: Settings->System->Apps & Features -> Manage optional features
OpenGL ES and Metal users on Mac will probably use Xcode's debugger.
AMD and NVIDIA also have tools for this.

Shader debugging

GLSL shaders can be compiled and checked for errors by glslangValidator. You can make it a part of your build process for extra credit. You can also use general static analysis tools like PVS-Studio or CppCheck. Using these tools I have found uninitialized variables in rendering code etc. Writing a shader hot-reloading system is not a big task but it pays off: Imagine debugging a video blitting shader that shows wrong colors. You can pause your game on a frame, modify the shader and see the results in that video frame instantly. You can also make the system take a screenshot before and after recompilation to more easily compare the results in an external program like Photoshop.

Test on multiple GPUs, even from the same vendor

There are differences in how textures are initialized (garbage or white/black etc.), resource transitions are done, flags are handled etc.


Many things can and will go wrong when rendering but there are features like validation layers and graphics debuggers that make finding the problem easier.