Architecture
Project Structure
build_xxx_xxx // Generated by the installer.
deps // Contains all dependencies.
src // Contains the high level API
/d3d12 // Contains the d3d12 render system implementations and all other helper files for d3d12.
/frame_graph // Contains all the code for the frame graph. Should be platform independend.
/render_tasks // Contains all render tasks. Can contain d3d12 and render tasks specifically made for a certain API.
/scene_graph // Contains all the code for the scene graph. Should be platform independend.
.gitignore
install.bat // Run this to generate all the project files and download dependencies.
CMakeLists.txt
The renderer is build from 3 different parts. The high level API, The graphics library implementation specific API and a optional low level API. The goal of this architecture is to keep the renderer “simple stupid” to allow rapid development and giving the team members a easy learning curve. Yet the architecture still allows us to write very performant code.
High Level API
The high level API is located in the src
, frame_graph
, scene_graph
.
Render System
The render system handles everything related to rendering. To implement a new renderer you can extend from wr::RenderSystem
and implement its pure virtual functions.
Resource Pools
Resource pools store and load resources in a efficient manner for you. It is recommended to load assets into a single pool which are used for a single purpose. So lets say that purpose doesn’t exist anymore you can evict
the entire pool at once from memory. Do you need the pool again? Than you can make the entire pool resident again. This prevents the user from having to micro manage GPU resources.
Implementing a resource pool for your specific graphics API is done by implementing the Load
, Evict
and MakeResident
virtual functions. The user can create a resource pool by calling CreateResourcePool
on the render system. Note that the render system doesn’t store the resource pool.
The idea is that the graphics API specific implementation creates a heap and allocates resources into that heap. If your graphics API doesn’t support this behaviour you can just implement it the old fashioned way.
Here is a UML diagram of 2 types of pools:
Frame Graph
We use a custom implementation of the frostbite frame graph system. We choose to go custom to reduce overhead since we don’t need a lot of the frostbite features since our scope is way smaller.
Implementing a render task for the frame graph is done by creating 2 functions with the following signatures: (RenderSystem&, Task<DeferredTaskData>&, DeferredTaskData&)
and (RenderSystem&, Task<DeferredTaskData>&, SceneGraph&, DeferredTaskData&)
. Those functions are allowed to contain API specific code. We are not abstracting it away because we want to keep our codebase “simple stupid” and performant. Resources created and used by the render task should be stored in a structure. This structure can than be accessed in the render task functions. Finally it is recommended to write a “getter” function for the render task.
Here is an example of a empty render task implementation:
struct DeferredTaskData
{
bool in_boolean;
int out_integer;
};
inline void SetupDeferredTask(RenderSystem & render_system, Task<DeferredTaskData> & task, DeferredTaskData & data)
{
auto& n_render_system = static_cast<D3D12RenderSystem&>(render_system);
}
inline void ExecuteDeferredTask(RenderSystem & render_system, Task<DeferredTaskData> & task, SceneGraph & scene_graph, DeferredTaskData & data)
{
auto& n_render_system = static_cast<D3D12RenderSystem&>(render_system);
}
inline std::unique_ptr<Task<DeferredTaskData>> GetDeferredTask()
{
auto ptr = std::make_unique<Task<DeferredTaskData>>(nullptr, "Deferred Render Task", &SetupDeferredTask, &ExecuteDeferredTask);
return ptr;
}
This design allows us to implement inlined render tasks which are just as efficient as virtual functions if the compiler can’t inline it but in general it is more likely to be inlined compared to virtual functions making this approach faster. This also gives us a bit more freedom and removes the need for defining classes thus keeping our file structure clean.
Here is a high level UML of the frame graph design:
Scene Graph
We are also rolling a custom scene graph implementation. This is again done for performance and extensibility. The implementation of the different nodes (Rendering, Initialization and etc) are implemented inside of the render system and that bound using the LINK_NODE_FUNCTION
macro. This allows for lots of flexibility for the RenderSystem
implementation.
// Note that Init_MeshNode & Render_MeshNode are member functions of wr::D3D12RenderSystem.
LINK_NODE_FUNCTION(wr::D3D12RenderSystem, wr::MeshNode, Init_MeshNode, Render_MeshNode)
LINK_NODE_FUNCTION(wr::D3D12RenderSystem, wr::AnimNode, Init_AnimNode, Render_AnimNode)
Here is an example of how to build the scene graph from code:
auto render_system = std::make_shared<wr::D3D12RenderSystem>();
auto scene_graph = std::make_shared<wr::SceneGraph>(render_system);
scene_graph->CreateChild<wr::MeshNode>(/* ... */);
auto some_node = scene_graph->CreateChild<wr::MeshNode>(/* ... */);
scene_graph->CreateChild<wr::MeshNode>(some_node, /* ... */);
scene_graph->CreateChild<wr::AnimNode>(/* ... */);
And a high level UML diagram (Excludes the macro and only shows a MeshNode
):
Low Level API (D3D12)
The idea of the low level D3D12 API is to create a near zero overhead abstraction that makes writing D3D12 code more pleasant. The low level D3D12 API is wrapped inside of the namespace d3d12
.
d3d12_structs.hpp
contains the different structures abstracting D3D12 and if necessary descriptors which are used to initialize some structures.d3d12_functions.hpp
contains the different function definitions.d3d12_functions.cpp
implements the functions.d3d12_defines.hpp
contains definitions specific to D3D12.d3d12_enums.hpp
contains enum’s specific to D3D12.
The reason for this approach is that it allows for rapid development and a easy to learn, write and understand API. Another benefit is that it can be mapped relatively easy to a Vulkan (or other low level API) implementation as well. Only minor refactoring would have to happen.
Showing a class diagram of this wouldn’t be very interesting since there are no classes involved. Only a couple of “dumb” structures and functions.
Please note that the low level API is optional when implementing a new graphics API. You can design it differently for each graphics API or choose to not use any abstraction.