Skip to content

A simple abstraction of Vulkan. Includes a multithreaded resource loading demo.

License

Notifications You must be signed in to change notification settings

ryandw11/SimpleVulkanRenderer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SimpleVulkanRenderer

SimpleVulkanRenderer is, as the name suggests, a simple Vulkan rendering engine designed to abstract away some of Vulkan's boiler plate code. This engine is also designed around allow for multi-threaded rendering.

General Structure

Most abstractions are named based on their Vulkan counter parts. For example, the wrapper for VkBuffer is VulkanBuffer.

Usage

The SimpleVulkanRenderer has the ability to auto initalize a Vulkan program so that you can focus on developing your application.

void main()
{
    auto renderer = std::make_shared<VulkanRenderer>();

    VulkanAutoInitSettings autoInitSettings;
    autoInitSettings.InstanceInfo.ApplicationName = "Test Application";
    autoInitSettings.InstanceInfo.ApplicationVersion = VK_MAKE_VERSION(1, 0, 0);
    autoInitSettings.SetupDebug = true;
    autoInitSettings.WindowHeight = HEIGHT;
    autoInitSettings.WindowWidth = WIDTH;
    autoInitSettings.WindowName = "Test Renderer Application";
}

First, a VulkanRenderer shared pointer should be constructed to act as the main renderer.
Then the VulkanAutoInitSettings struct is used configure the settings of the Vulkan program. SetupDebug will enable Vulkan validation layers and spit out any logs into the console.

SimpleVulkanRenderer will also take care of the construction of Physical and Logical devices. The GPU that supports all of the desired features will be selected to be used.
The SimpleVulkanRenderer by default will create a graphics and presentation queue for use with the default renderer. Additional queues can be created by using the CustomQueues property of the VulkanAutoInitSettings. It is important to note that the SimpleVulkanRenderer will assume that you want distinct queues for each queue descriptor.

VulkanQueueDescriptor queueDescriptor;
queueDescriptor.Type = COMPUTE_QUEUE;
queueDescriptor.Priority = 0.7f;
queueDescriptor.Name = "ResourceLoadingQueue";
autoInitSettings.CustomQueues.push_back(queueDescriptor);

Then, renderer->AutoInitalize() is called to make use of the auto initalization feature.

int main() {
    VulkanAutoInitSettings autoInitSettings;
    autoInitSettings.InstanceInfo.ApplicationName = "Test Application";
    autoInitSettings.InstanceInfo.ApplicationVersion = VK_MAKE_VERSION(1, 0, 0);
    autoInitSettings.SetupDebug = true;
    autoInitSettings.WindowHeight = HEIGHT;
    autoInitSettings.WindowWidth = WIDTH;
    autoInitSettings.WindowName = "Test Renderer Application";

    renderer->AutoInitialize(
        autoInitSettings,
        [](auto descriptorLayout) { 
            /* Create Default Descriptor Layout */
        },
        []() { 
            /* Pipeline Creation Stage */
            GraphicsPipelineDescriptor pipeline;
            pipeline.VertexShader = CreateVertexShader();
            pipeline.FragmentShader = CreateFragmentShader();
            return pipeline;
        },
        []() {
            /* General Loading Stage */
            SetupBuffers();
        },
        [](auto setBuilder) {
            /* Create the default descriptor sets for each framebuffer. */
        }
    );
}

AutoInitalize takes in 4 callback functions that are used to configure the program during the initalization process.

  1. The first callback creates the descriptor layouts that are used by the vertex and fragment shaders. Things like uniform buffer and image sampler bindings are defined here.
[](auto descriptorLayout) {
    /* Create Default Descriptor Layout */
    // Describes the uniforms that are used in the shaders.
    descriptorLayout->UniformBufferBinding(/*Binding*/ 0, /*Count*/ 1, VK_SHADER_STAGE_VERTEX_BIT /* Shader Stage */);
    descriptorLayout->ImageSamplerBinding(1, 1, VK_SHADER_STAGE_FRAGMENT_BIT);
}
  1. The second callback allows you to customize the default graphics pipeline. The callback expects you to return the GraphicsPipelienDescriptor struct. The vertex and fragment shaders are required to be defined here.
[]() { 
/* Pipeline Creation Stage */
    GraphicsPipelineDescriptor pipeline;
    pipeline.VertexShader = CreateVertexShader();
    pipeline.FragmentShader = CreateFragmentShader();
    return pipeline;
}
  1. The third callback is a general loading stage for lading data and setting up buffers. Generally buffers that are used for descriptor sets are defined and created here.

  2. The fourth callback describes the default descriptor sets for the each framebuffer (by default the SimpleVulkanRenderer uses 2 framebuffers).

[](auto setBuilder) { 
    /* Create the default descriptor sets for each framebuffer. */
    VulkanTexture texture("textures/texture.jpg", renderer, renderer->mBufferUtilities);
    setBuilder->DescribeBuffer(0, 0, mappedUniformBuffers, sizeof(UniformBufferObject));
    setBuilder->DescribeImageSample(1, 0, texture.ImageView(), texture.Sampler());
}

Buffer Utilities

SimpleVulkanRenderer provides buffer utilities to make managing VkBuffers easier.

auto bufferUtils = renderer->mBufferUtilities;

You can create a buffer with CreateBuffer and map them to memory with MapMemory. A VulkanMappedBuffer can store both a VkBuffer and its memory.

VulkanMappedBuffer modelMatrixBuffer;
bufferUtils->CreateBuffer(sizeof(glm::mat4) * 2, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, modelMatrixBuffer, modelMatrixBuffer);

// Map the buffer's memory:
bufferUtils->MapMemory(modelMatrixBuffer, 0, sizeof(glm::mat4) * 2, 0, modelMatrixBuffer.DirectMappedMemory());

// Copy data to the buffer.
memcpy(modelMatrixBuffer[currentImage].MappedMemory(), &modelMatrix, sizeof(glm::mat4));

Rendering

To start drawing an image, StartFrameDrawing() needs to be called.

auto currentImage = renderer->StartFrameDrawing();

StartFrameDrawing() returns the index of the swapchain image that is to be drawn to.

After that commands can be recorded into the frame command buffer.

auto frameCommandBuffer = renderer->GetFrameCommandBuffer();
frameCommandBuffer->Reset();
frameCommandBuffer->StartCommandRecording();

Reset() resets the command buffer so the commands from the previous frame are erased.
StartCommandRecording() sets the command buffer to start recording.

Then, the render pass to use needs to be defined. SimpleVulkanRender will create a default render pass with the auto initalizaiton process.

auto frameBuffer = renderer->SwapChain()->FrameBuffers()[currentImage];
auto extent = renderer->SwapChain()->Extent();
VkClearColorValue clearColor = {164 / 255.0, 236 / 255.0, 252 / 255.0, 1.0}; // Sky Blue
frameCommandBuffer->StartRenderPass(renderer->RenderPass(), frameBuffer, extent, clearColor);

The graphics pipeline can be binded using BindPipeline.

frameCommandBuffer->BindPipeline(renderer->PrimaryGraphicsPipeline()->Pipeline());

The final setup for rendering, is to setup the viewport scissoring.

frameCommandBuffer->SetViewportScissor(renderer->SwapChain()->Extent());

An object can be drawn by binding the vertex and index buffers.

frameCommandBuffer->BindVertexBuffer(object->VertexBuffer());
frameCommandBuffer->BindIndexBuffer(object->IndexBuffer());
frameCommandBuffer->BindVertexBuffer(object->ModelBuffer(), 0 /* offset */, 1 /* First Binding */); // Bind the model buffer.
frameCommandBuffer->BindDescriptorSet(renderer->PrimaryGraphicsPipeline()->PipelineLayout(), renderer->DescriptorHandler()->DescriptorSetBuilder()->GetBuiltDescriptorSets()[currentImage]);
frameCommandBuffer->DrawIndexed(object->IndiciesSize());

At the end of drawing the frame, the command recording needs to be closed:

frameCommandBuffer->EndRenderPass();
frameCommandBuffer->EndCommandRecording();

Then, we mark the end of frame drawing:

renderer->EndFrameDrawing(currentImage);

Included Demo

The SimpleVulkanRenderer comes with demo that shows off Vulkan's multithreading capabilities with resource loading.

The demo generates 16x16x16 voxel chunks in a thread pool. The entry point for the demo is the main.cpp file.
The number of resource threads can be changed by modifing the NUM_RESOURCE_THREADS constant at the top of the file. NOTE: The demo assumes that each thread can have it's own compute queue. Make sure NUM_RESOURCE_THREADS does not exceed the number of compute queues that your GPU has available.

The size of a voxel chunk (by default 16 x 16 x 16) can be changed in the DemoConsts.hpp header file with the constant CHUNK_VOXEL_COUNT.

Resource Loading System

The diagram below shows the flow of how resources are loaded onto the GPU concurrently.
Flow diagram showing the resource loading
The demo uses a simplified version of what is shown above.

Images


Greedy Mesh Algorithm

To test resource loading, a primitive version of the Greedy Mesh Algorithm is used to optimize the mesh of each voxel chunk:

Used Resources

About

A simple abstraction of Vulkan. Includes a multithreaded resource loading demo.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages