Dr. Malware
Dr. Malware is a data-oriented arcade game built without a commercial engine, using Vulkan for rendering and the EnTT entity component system for gameplay.
I architected a modular IMGUI-based UI framework and integrated it into the Vulkan render pipeline, giving the team a reusable system for building menus, HUD elements, and debug tooling.
Code
IMGUI Integration into the Vulkan Pipelinec++
IMGUI gets its own descriptor pool (separate from the 3D pipeline's) and hooks into the engine's existing Vulkan surface reusing the render pass, queue family, and swapchain image count, so the UI renders into the same command buffer as the 3D scene each frame.
// Dedicated descriptor pool for imgui so we don't exhaust the 3D pipeline pool
VkDescriptorPool& imguiPool = vulkanRenderer.imguiDescriptorPool;
VkDescriptorPoolSize pool_sizes[] =
{
{ VK_DESCRIPTOR_TYPE_SAMPLER, 1000 },
{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 },
{ VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000 },
// ...one entry per descriptor type
};
VkDescriptorPoolCreateInfo pool_info = {};
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
pool_info.maxSets = 1000 * (uint32_t)IM_ARRAYSIZE(pool_sizes);
pool_info.poolSizeCount = (uint32_t)IM_ARRAYSIZE(pool_sizes);
pool_info.pPoolSizes = pool_sizes;
if (vkCreateDescriptorPool(vulkanRenderer.device, &pool_info, nullptr, &imguiPool) != VK_SUCCESS)
{
throw std::runtime_error("Failed to create ImGui descriptor pool!");
}
// Hook ImGui into the engine's existing Vulkan surface
VkInstance instance;
vulkanRenderer.vlkSurface.GetInstance(reinterpret_cast<void**>(&instance));
uint32_t queueFamily = ImGui_ImplVulkanH_SelectQueueFamilyIndex(vulkanRenderer.physicalDevice);
VkQueue queue;
vkGetDeviceQueue(vulkanRenderer.device, queueFamily, 0, &queue);
VkRenderPass imguiRenderPass;
vulkanRenderer.vlkSurface.GetRenderPass(reinterpret_cast<void**>(&imguiRenderPass));
unsigned int imageCount;
vulkanRenderer.vlkSurface.GetSwapchainImageCount(imageCount);
ImGui_ImplVulkan_InitInfo init_info = {};
init_info.Instance = instance;
init_info.PhysicalDevice = vulkanRenderer.physicalDevice;
init_info.Device = vulkanRenderer.device;
init_info.QueueFamily = queueFamily;
init_info.Queue = queue;
init_info.DescriptorPool = imguiPool;
init_info.MinImageCount = imageCount;
init_info.ImageCount = imageCount;
init_info.PipelineInfoMain.RenderPass = imguiRenderPass;
init_info.PipelineInfoMain.Subpass = 0;
init_info.PipelineInfoMain.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
init_info.CheckVkResultFn = check_vk_result;
ImGui_ImplVulkan_Init(&init_info);
// ...then each frame, inside Update_VulkanRenderer:
ImGui_ImplVulkan_NewFrame();
ImGui::NewFrame();
UI::Update(registry, entity);
ImGui::Render();
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), commandBuffer);
vulkanRenderer.vlkSurface.EndFrame(true);Modular Screen System & Input Bridgec++
The UI framework runs as an EnTT system and each screen is a self-contained draw function keyed off one state enum.
// UIComponents.h contains everything a screen needs to draw itself.
struct UIContext
{
std::shared_ptr<const GameConfig> config;
entt::registry& registry;
entt::entity& entity; // graphics entity
UI::UIManager& manager;
UTIL::Input& input;
GW::SYSTEM::GWindow& window;
ImGuiIO& io;
float width, height;
float fontScale;
};
// UIManager.cpp
void Update(entt::registry& registry, entt::entity entity)
{
auto& ui = registry.ctx().get<UI::UIManager>();
GW::INPUT::GInput& ginput = ui.input->immediateInput;
ImGuiIO& io = ImGui::GetIO();
float mx, my;
ginput.GetMousePosition(mx, my);
io.MousePos = ImVec2(mx, my);
float state = 0;
io.MouseDown[0] = (ginput.GetState(G_BUTTON_LEFT, state) == GW::GReturn::SUCCESS && state > 0.0f);
io.MouseDown[1] = (ginput.GetState(G_BUTTON_RIGHT, state) == GW::GReturn::SUCCESS && state > 0.0f);
auto window = registry.get<GW::SYSTEM::GWindow>(entity);
unsigned int width = 0;
unsigned int height = 0;
window.GetClientWidth(width);
window.GetClientHeight(height);
// Resolution-independent UI scale
float fWidth = static_cast<float>(width);
float fHeight = static_cast<float>(height);
float fontScale = (std::min)(fWidth / ui.baseWidth, fHeight / ui.baseHeight);
UI::UIContext context = { ui.config, registry, entity, ui, *ui.input,
window, io, fWidth, fHeight, fontScale };
switch (ui.state)
{
case UIManager::Screen::MAIN_MENU: DrawMainMenuScreen(context); break;
case UIManager::Screen::IN_GAME_HUD: DrawInGameHUD(context); break;
case UIManager::Screen::PAUSE_MENU: DrawPauseMenu(context); break;
case UIManager::Screen::GAME_OVER: DrawGameOverScreen(context); break;
// ...leaderboard, options, win, and credits screens
}
}GPU Texture Upload for UI Imagesc++
Adapted from the Dear ImGui Vulkan example and reworked to work with the EnTT registry.
bool LoadTextureFromFile(entt::registry& registry, entt::entity& entity, const char* filename, TextureData* tex_data)
{
auto& vulkanRenderer = registry.get<VulkanRenderer>(entity);
// Specifying 4 channels forces stb to load the image in RGBA which is an easy format for Vulkan
tex_data->channels = 4;
unsigned char* image_data = stbi_load(filename, &tex_data->width, &tex_data->height, 0, tex_data->channels);
if (image_data == NULL)
return false;
size_t image_size = tex_data->width * tex_data->height * tex_data->channels;
// ...create the VkImage in device-local memory, plus its image view and sampler
// Create Descriptor Set using ImGUI's implementation
tex_data->DS = ImGui_ImplVulkan_AddTexture(tex_data->sampler, tex_data->imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
// ...create a host-visible staging buffer, memcpy the pixels in, flush the mapped range
// Transition the image for transfer, copy the staging buffer in, then hand it to the fragment shader
VkImageMemoryBarrier copy_barrier[1] = {};
copy_barrier[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
copy_barrier[0].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
copy_barrier[0].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
copy_barrier[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
copy_barrier[0].image = tex_data->image;
copy_barrier[0].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
copy_barrier[0].subresourceRange.levelCount = 1;
copy_barrier[0].subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(command_buffer, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 1, copy_barrier);
VkBufferImageCopy region = {};
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.layerCount = 1;
region.imageExtent.width = tex_data->width;
region.imageExtent.height = tex_data->height;
region.imageExtent.depth = 1;
vkCmdCopyBufferToImage(command_buffer, tex_data->uploadBuffer, tex_data->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion);
VkImageMemoryBarrier use_barrier[1] = {};
use_barrier[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
use_barrier[0].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
use_barrier[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
use_barrier[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
use_barrier[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
use_barrier[0].image = tex_data->image;
use_barrier[0].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
use_barrier[0].subresourceRange.levelCount = 1;
use_barrier[0].subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, use_barrier);
// ...submit the one-time command buffer and wait for the upload to finish
return true;
}What I Did
UI Framework & Rendering Integration
- Architected a modular IMGUI-based UI framework and integrated it into the Vulkan render pipeline.
- Designed the framework to be reusable across menus, HUD, and debug tooling.
Data-Oriented Gameplay
- Built gameplay on the EnTT entity component system following data-oriented design principles.
- Worked directly against Vulkan without a commercial engine, handling the systems an engine would normally provide.