Vulkan Fluid Dynamics - Week 1

Vulkan Slide

Vulkan is the new hotness in graphics programming.

Above is the first slide when the Vulkan API was announced at GDC 2015. Clearly, compute is an important topic for Vulkan and graphics going forward. The term compute (also general programming on the graphics processing unit, or GPGPU) means to use the graphics card for general computations, not just rendering tasks. In the last decade, graphics cards have become more powerful than CPUs and have specialized to become extremely proficient at parallel processing. Tasks that can be parallelized can be computed quickly on the GPU and compute technology allows a programmer to do that.

I am starting a personal project to explore usage of compute shaders in Vulkan. One aspect of game development that can be effectively parallelized is particle systems. Each particle can be calculated in isolation. To add some complexity, I am going to be implementing a simple fluid dynamics algorithm to visualize the particles in an interesting way and add some computational tasks. The visualization will allow the user to toggle using compute on and off to see the performance benefits. I will be documenting my progress over the following weeks on this blog!

Update history:

References

There are two great particle system demos using compute by Sascha Willems (Source).



My reference paper for implementing fluid dynamics is here:

Progress

So far, I have integrated the GLFW library (which supports Vulkan windows), Vulkan (using the LunarG SDK), and IMGUI for making an interactive visualization.

I’m using the excellent Vulkan tutorial to help build a Vulkan pipeline. Vulkan is a verbose API (rendering a triangle is roughly 800 lines of code). Every aspect of the pipeline, from choosing a GPU to blend states, must be explicitly specified.

Managing Vulkan Objects

Vulkan, similarly to the COM in Windows, has a create and release model. Create functions are called through the API and memory is allocated behind the scenes. The programmer is responsible for freeing that memory via a corresponding Destroy function. Memory leaks are a common issue and can be difficult to debug, so I created a VHandler class that will destroy Vulkan objects automatically (like a smart pointer). Most of the code is from the same Vulkan tutorial, but has been removed in the current version for clarity. I like it as it quickly becomes cumbersome to have to clean up all of these objects manually.

It does this by using a std::function that is called in the destructor. There are only three different variations when deleting a Vulkan object. These manifest themselves as constructors to the VHandler.

  • The Vulkan object is deleted by itself.
  • The Vulkan object is deleted by a VkInstance.
  • The Vulkan object is deleted by a VkDevice.

In all of these options, there is always an VkAllocationCallbacks* parameter that is ignored and set to nullptr for this project.

template <typename T>
class VHandler
{
public:
VHandler()
: VHandler([](T, VkAllocationCallbacks*) {})
{
}

VHandler(std::function<void(T, VkAllocationCallbacks*)> destroyFunction)
: mVulkanObject(VK_NULL_HANDLE)
{
mDestroyFunction = [destroyFunction](T vulkanObject) { destroyFunction(vulkanObject, nullptr); };
}

VHandler(const VHandler<VkInstance>& instance, std::function<void(VkInstance, T, VkAllocationCallbacks*)> destroyFunction)
: mVulkanObject(VK_NULL_HANDLE)
{
mDestroyFunction = [&instance, destroyFunction](T vulkanObject) { destroyFunction(instance, vulkanObject, nullptr); };
}

VHandler(const VHandler<VkDevice>& device, std::function<void(VkDevice, T, VkAllocationCallbacks*)> destroyFunction)
: mVulkanObject(VK_NULL_HANDLE)
{
mDestroyFunction = [&device, destroyFunction](T vulkanObject) { destroyFunction(device, vulkanObject, nullptr); };
}

~VHandler()
{
Destroy();
}

...

private:
T mVulkanObject;
std::function<void(T)> mDestroyFunction;

void Destroy()
{
if (mVulkanObject != VK_NULL_HANDLE)
{
mDestroyFunction(mVulkanObject);
}

mVulkanObject = VK_NULL_HANDLE;
}
};


Finally, there are various operator overloads to allow the VHandler class to act like the Vulkan object that it’s wrapping, and a Replace function when the programmer explicitly wants to replace the wrapped Vulkan object.

...

VHandler<T>& operator=(const T& other)
{
if (mVulkanObject != other)
{
Destroy();
mVulkanObject = other;
}

return *this;
}

const T* operator&() const
{
return &mVulkanObject;
}

operator T() const
{
return mVulkanObject;
}

T* Replace()
{
Destroy();
return &mVulkanObject;
}

...

Compiling Shaders

Vulkan, like DirectX, loads compiled shader bytecode instead of shader source. Vulkan compiled shaders are specified in the SPIR-V format. There are several Vulkan shader compilers, and I am using glslangValidator which is provided by the LunarG SDK. I wrote a simple batch file to compile all of the shaders in the Shaders directory and output them to the target directory.

:: %1 - shader directory
:: %2 - glslangValidator path
:: %3 - output directory

@echo off
setlocal EnableDelayedExpansion

:: delete previously compiled shaders
pushd %3
del *.spv
popd

:: compile all shaders in the shader directory
cd %1
for %%f in (*) do (
set fileName=%3%%~nf
set fileType=%%~xf
set fileExtension=.spv
%2 -V %%f -o !fileName!_!fileType:~1!!fileExtension!
)


There is a pre-build event that calls this batch script with the required parameters.

Conclusion

I’m almost ready to render my first triangle in Vulkan! I have most of the pipeline and device creation code made, with presentation and drawing up next. As it is mostly boilerplate code I am not including it, but you can see the source in the Vulkan class. See you next update!

Check out the source code here: https://github.com/travisfoo/VulkanFluidDynamics

Update history: