I believe a good game-dev environment should be capable of supporting all-platform philosophy and this can be achieved by being open enough to strive for enough learning and compassion.
The purpose of this blog-post series is to explore and show just that. We begin with the study of a cross-platform real-time graphics driver.
Vulkan is a cross-platform 3D graphics API and graphics driver meant for Real Time rendering. The best feature, for me personally, is the degree of control provided with the fortune of low-level programming tools which clears up the graphics rendering pipeline concept for enthusiasts and delivers major lesson for the uninitiated (who survive at the convenience of easy GL
type function calls, not to say I’m not Glad, pun intended). Our friend Cherno is a great propagator of the API who introduced me to Vulkan. But we gonna extend his legacy and put the concept of cross-platform
to test with all the stress-tests known to tech-kind.
Along with the official tutorial I have consulted Travis Vroman’s videos and some off-mainstream videos. In context of Karma, we don’t need to hook Vulkan as sole beneficiary but rather as any other regular library with enough fluidity of switching like so
#include "RendererAPI.h"
namespace Karma
{
RendererAPI::API RendererAPI::s_API = RendererAPI::API::OpenGL;
}
Here we see it is matter of single line to change the rendering API for Karma. Clearly I plan to implement Metal/DirectX in future. Remember we need a decoupled way of hooking the library for cross-platform reasons. This results in the following folder hierarchy generating enough basis for the abstraction we are striving for
KarmaEngine
│
└───Karma
│ │
│ └───src
│ │ │
│ │ └───Karma
│ │ │ │
│ │ │ └───Animations
│ │ │ └───Events
│ │ │ └───Renderer
│ │ │ Input.h
│ │ │ Core.h
│ │ │ Log.h
│ │ │ ...
│ │ │
│ │ └───Platform
│ │ │
│ │ └───Linux
│ │ └───Mac
│ │ └───OpenGL
│ │ └───VUlkan
│ │ └───Windows
│ │
│ │ Karma.h
│ │ ...
│ │
│ └───vendor
│ │ │
│ │ └───assimp
│ │ └───Glad
│ │ └───GLFW
│ │ └───GLM
│ │ └───glslang
│ │ └───ImGui
│ │ └───spdlog
│ │ └───stb
│ │ ...
│
└───Application
│
└───src
│ KarmaApp.cpp
│ ...
The entities
thus formed are equipped with the ability to hook with high specificity and moral values (minding own business and not interfering with others in any sense). Then it becomes my job to deal with the decisions, for instance, how much ground to cover with how much depth such as to maintain the overall harmony in Karma. At this point in time, the following flow-chart captures the essence of Karma

Clearly, Vulkan is part of Rendering Engine, which, is an abstraction that can be visualized as a magical (not mystic though) box with the following diagram

The layer takes care of what platform we are running on and which rendering API we are rocking with. Then the concept of cross matching (within the scope of API-Platform support alliances) is obvious. Let me outline of the Rendering Engine (RE) functioning. In a typical Rendering loop, the following lines of code are a must
virtual void OnUpdate(float deltaTime) override
{
Karma::RenderCommand::SetClearColor(/* Some Color */);
Karma::RenderCommand::Clear();
Karma::Renderer::BeginScene();
/* Process materials and Bind Vertex Array. */
Karma::Renderer::Submit(/* Some Vertex Array */);
Karma::Renderer::EndScene();
}
Now we would want our APIs (Vulkan/OpenGL or whatever) to conform to these simple static function calls. That is where the RE magic comes in. No matter what the working philosophy of graphics API is, we need to channel that into a working set of routines such that the above OnUpdate
function, called in the game loop, which can be identified with Karma's Rendering Handle
in the flow chart, makes sense (works). Let me define and explain the actors of this play
- Vertex Array: Can be imagined synonymous to a well behaved container of
Mesh
andMaterial
(defined here). Each API should have its corresponding instantiation of Vertex Array. - Render Command: Is a static class with the rendering structure (curtsey TheCherno) and with the runtime-polymorphism responsibility to direct the calls to appropriate API (specified here). It holds the reference to
RendererAPI
which could be Vulkan or OpenGL after doing their initialisation specified inRendererAPI.cpp
. - Renderer: A data-oriented static class containing the
scene
information (for instance camera) for relevant processing, and, rendering API information for Karma.
There is an entire branch in which we attempt to hook Vulkan API and which succeeded almost as expected (with some reservations and I plan to do a stress test with large number (millions perhaps billions of triangles)). But let me first chalk out the partial-abstractions acting like an interface between the static routines, in the above block, and low level Vulkan commands. The following are the Vulkan adaptations of Karma’s abstract classes.
- Vulkan Shader (derived from Shader)
- Vulkan Buffer (derived from Buffer)
- Vulkan Context (derived from Graphics Context)
- Vulkan RendererAPI (derived from RendererAPI)
- Vulkan VertexArray (derived from VertexArray)
Please note I am using the video as raw material and vulkan-tutorial for regular checks in what I understand and write henceforth.
The goal for any decent graphics API is to provide means (tangible and intangible) to map the coordinates of a triangle (read my blog-post for underlining importance of triangles in Real Time rendering) to the pixels on monitor or screen. This is it! What basically creates the market for various APIs IS the need for a hardware friendly algorithm which is considerate enough not only for scaling to billion triangles but also for providing natural fit to the taste of the programmer. Taste may include various levels of legitimate control based on what the programmer may think serves the purpose.
To be continued…