← All work

Personal Project 2025

artisDX

A personal DirectX 12 renderer with shader reflection, physically based materials, and an ImGui GUI.

  • C++
  • D3D12
  • Win32
  • ImGUI

artisDX is my toy engine I started to learn a modern graphics API as well as modern rendering concepts and techniques. For that I built a core command and resource infrastructure, added GLB model loading and implemented physically based materials, normal mapping and directional shadows. Its a continuous work in progress, currently I am working on one of the big advantages of modern APIs: multithreaded command submission.

Implementation - Shader reflection

One hassle I explicitly wanted to avoid was maintaining resource binding layouts by hand for every shader pass. For that I use a feature provided by the DirectX Shader Compiler (DXC): shader reflection. After shader compilation I extract the binding layout for buffers, textures and samplers from the shaders BLOB and generate matching root signatures from that information.

ShaderPass.cpp - Reflected resource bindingsC++
for (uint32_t i = 0; i < shaderDesc.BoundResources; ++i)
{
    D3D12_SHADER_INPUT_BIND_DESC binding{};
    ThrowIfFailed(reflection->GetResourceBindingDesc(i, &binding),
        context + ": GetResourceBindingDesc failed at resource " + std::to_string(i));
    D3D12_DESCRIPTOR_RANGE1 range{};
    // ... validate the binding and map its resource type ...

    bindings.emplace(binding.Name, static_cast<uint32_t>(ranges.size()));
    range.NumDescriptors = binding.BindCount;
    range.BaseShaderRegister = binding.BindPoint;
    range.RegisterSpace = binding.Space;
    range.OffsetInDescriptorsFromTableStart = 0;
    ranges.emplace_back(stage, range);
}
// ... generate one root descriptor table per range ...

Alongside the root signature I build a map from shader resource names to their root parameter indices. Through that we can bind resources using the same names as in the shader, without needing to know where they ended up in the generated layout. Binding is as simple as:

Material.cpp - Binding by shader nameC++
if (auto slot = shaderPass.GetRootParameterIndex("nameInShader"))
    commandList->SetGraphicsRootDescriptorTable(slot.value(), _cbvMaterialFactorsGpuHandle);

Implementation - GUI

For displaying the scene and debugging I added an ImGui interface with dockable windows. The design I have chosen enables to add an object to the GUI as long as it implements IGUIComponent. Since each object needs different controls, each one implements its own DrawGUI function through a shared interface and registers with the GUI. The registry stores weak references, so adding an object to the GUI does not keep it alive after the scene releases it.

IGUIComponent.cpp / GUI.cpp - Registered controlsC++
void IGUIComponent::RegisterWithGUI()
{
    GUI::RegisterComponent(weak_from_this());
}

std::vector<std::weak_ptr<IGUIComponent>> guiComponents;

std::vector<std::shared_ptr<IGUIComponent>> components;
components.reserve(GUI::guiComponents.size());
for (auto const& weak : GUI::guiComponents)
{
    if (auto shared = weak.lock())
        components.push_back(std::move(shared));
}

for (auto const& component : components)
{
    component->DrawGUI();
}

The controls then sit directly next to the data they modify. For example, a shader pass only needs to expose its enable flag to let me switch that pass on and off through the GUI.

ShaderPass.cpp - Render-pass controlC++
void ShaderPass::DrawGUI()
{
    ImGui::Begin(_name.c_str());
    ImGui::Checkbox("Enable Pass", &_usePass);
    ImGui::End();
}

More videos and images