The next Vulkan implementation we will author is to load our static mesh asset files. To do this we will need to learn a bit about Vulkan buffers which are the data structures that will represent the vertices and indices of our mesh data inside Vulkan. We followed a similar approach in our OpenGL implementation where we took the basic mesh data from our ast::Mesh
class and fed it into OpenGL specific buffers.
The (rather terse) Vulkan doco for buffers is here: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkBuffer.html.
Specifically in this article we will:
We will be using Vulkan buffers to represent the vertices and indices of our static mesh assets and in the next article to help us load textures.
To create a buffer we will first construct a temporary staging buffer to hold our asset data and act as a data source for a device local buffer - the device local buffer is ultimately the one we will use, discarding the staging buffer along the way.
A Vulkan buffer actually doesn’t hold any data itself - instead we must bind it to an accompanying device memory object which we ourselves create. Our staging buffer also requires us to manually perform our own memory copy operations on its backing memory to populate its data whereas our device local buffer will instead require us to invoke a command buffer asking Vulkan to perform the memory data operations for us.
We will kick off by authoring a new class to encapsulate a Vulkan buffer object. Create the files vulkan-buffer.hpp
and vulkan-buffer.cpp
in the Vulkan source folder. Edit the header file with the following:
#pragma once
#include "../../core/graphics-wrapper.hpp"
#include "../../core/internal-ptr.hpp"
#include "vulkan-command-pool.hpp"
#include "vulkan-device.hpp"
#include "vulkan-physical-device.hpp"
namespace ast
{
struct VulkanBuffer
{
VulkanBuffer(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const vk::DeviceSize& size,
const vk::BufferUsageFlags& bufferFlags,
const vk::MemoryPropertyFlags& memoryFlags,
const void* dataSource);
const vk::Buffer& getBuffer() const;
static ast::VulkanBuffer createDeviceLocalBuffer(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanCommandPool& commandPool,
const vk::DeviceSize& size,
const vk::BufferUsageFlags& bufferFlags,
const void* dataSource);
private:
struct Internal;
ast::internal_ptr<Internal> internal;
};
} // namespace ast
Apart from the physicalDevice
and device
arguments in the constructor we also need:
size
: Represented by the vk::DeviceSize
type, this defines the length of the buffer storage space required in bytes and is in fact an alias for the uint64_t
type.bufferFlags
: Defines which ways this buffer can be used - it could be a transfer source, a vertex buffer, an index buffer or myriad of other types. Read the doco for more: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkBufferUsageFlagBits.html.memoryFlags
: Defines how the buffer should store its data - for example device local or host visible. See the docs for more: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkMemoryPropertyFlagBits.html. For our use case our staging buffer will need to use host visible memory to allow us to put data into its backing memory ourselves, whereas our device local buffer will force us to use a command buffer to put the data into it unlike host visible.dataSource
: The raw memory pointer of where to find the bytes of data that should be copied into the memory that backs our buffer. We will accept nullptr
values to be passed in for this argument which I’ll explain shortly.The getBuffer
function will be needed later but really just acts as a proxy to the internally stored Vulkan buffer.
The createDeviceLocalBuffer
function is a little interesting in that it is a static function which we haven’t really written many of in this series. It is basically a convenience factory function which takes in all the configuration for a buffer that should end up in device local memory and orchestrates all the staging guff to produce a device local buffer.
Now edit vulkan-buffer.cpp
to start the implementation - we’ll do it a chunk at a time:
#include "vulkan-buffer.hpp"
using ast::VulkanBuffer;
namespace
{
vk::UniqueBuffer createBuffer(const ast::VulkanDevice& device,
const vk::DeviceSize& size,
const vk::BufferUsageFlags& bufferFlags)
{
vk::BufferCreateInfo info{
vk::BufferCreateFlags(), // Flags
size, // Size
bufferFlags, // Buffer usage flags
vk::SharingMode::eExclusive, // Sharing mode
0, // Queue family index count
nullptr}; // Queue family indices
return device.getDevice().createBufferUnique(info);
}
vk::UniqueDeviceMemory allocateMemory(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const vk::Buffer& buffer,
const vk::MemoryPropertyFlags& memoryFlags)
{
vk::MemoryRequirements memoryRequirements{
device.getDevice().getBufferMemoryRequirements(buffer)};
uint32_t memoryTypeIndex{
physicalDevice.getMemoryTypeIndex(memoryRequirements.memoryTypeBits, memoryFlags)};
vk::MemoryAllocateInfo info{
memoryRequirements.size, // Allocation size
memoryTypeIndex}; // Memory type index
return device.getDevice().allocateMemoryUnique(info);
}
} // namespace
struct VulkanBuffer::Internal
{
const vk::UniqueBuffer buffer;
const vk::UniqueDeviceMemory deviceMemory;
Internal(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const vk::DeviceSize& size,
const vk::BufferUsageFlags& bufferFlags,
const vk::MemoryPropertyFlags& memoryFlags,
const void* dataSource)
: buffer(::createBuffer(device, size, bufferFlags)),
deviceMemory(::allocateMemory(physicalDevice, device, buffer.get(), memoryFlags))
{
// Take the buffer and the allocated memory and bind them together.
device.getDevice().bindBufferMemory(buffer.get(), deviceMemory.get(), 0);
// Take the datasource and copy it into our allocated memory block.
if (dataSource)
{
void* mappedMemory{device.getDevice().mapMemory(deviceMemory.get(), 0, size)};
std::memcpy(mappedMemory, dataSource, static_cast<size_t>(size));
device.getDevice().unmapMemory(deviceMemory.get());
}
}
};
VulkanBuffer::VulkanBuffer(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const vk::DeviceSize& size,
const vk::BufferUsageFlags& bufferFlags,
const vk::MemoryPropertyFlags& memoryFlags,
const void* dataSource)
: internal(ast::make_internal_ptr<Internal>(physicalDevice, device, size, bufferFlags, memoryFlags, dataSource)) {}
const vk::Buffer& VulkanBuffer::getBuffer() const
{
return internal->buffer.get();
}
ast::VulkanBuffer VulkanBuffer::createDeviceLocalBuffer(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanCommandPool& commandPool,
const vk::DeviceSize& size,
const vk::BufferUsageFlags& bufferFlags,
const void* dataSource)
{
// TODO: Patience padawan!
}
Buffer
We’ll start with the const vk::UniqueBuffer buffer
field which is constructed through the createBuffer
free function:
namespace
{
vk::UniqueBuffer createBuffer(const ast::VulkanDevice& device,
const vk::DeviceSize& size,
const vk::BufferUsageFlags& bufferFlags)
{
vk::BufferCreateInfo info{
vk::BufferCreateFlags(), // Flags
size, // Size
bufferFlags, // Buffer usage flags
vk::SharingMode::eExclusive, // Sharing mode
0, // Queue family index count
nullptr}; // Queue family indices
return device.getDevice().createBufferUnique(info);
}
Creating a buffer shell is fairly straight forward, really just populating a vk::BufferCreateInfo
object and asking our logical device to make one.
Device memory
The const vk::UniqueDeviceMemory deviceMemory
field represents the backing memory object that will be bound to the buffer. The device memory is allocated during construction, but not populated with any data:
namespace
{
...
vk::UniqueDeviceMemory allocateMemory(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const vk::Buffer& buffer,
const vk::MemoryPropertyFlags& memoryFlags)
{
vk::MemoryRequirements memoryRequirements{
device.getDevice().getBufferMemoryRequirements(buffer)};
uint32_t memoryTypeIndex{
physicalDevice.getMemoryTypeIndex(memoryRequirements.memoryTypeBits, memoryFlags)};
vk::MemoryAllocateInfo info{
memoryRequirements.size, // Allocation size
memoryTypeIndex}; // Memory type index
return device.getDevice().allocateMemoryUnique(info);
}
} // namespace
We use the buffer
argument to ask the logical device to identify what kind of memory the buffer requires. This will be influenced by whether the buffer specified it should have device local memory or not:
vk::MemoryRequirements memoryRequirements{
device.getDevice().getBufferMemoryRequirements(buffer)};
Once we know the requirements of the buffer’s memory we use the getMemoryTypeIndex
function of our existing physical device class to discover what pool of memory in the hardware should be used:
uint32_t memoryTypeIndex{
physicalDevice.getMemoryTypeIndex(memoryRequirements.memoryTypeBits, memoryFlags)};
Both of those bits of information can then be used to describe how we need the logical device to allocate the memory, which we subsequently do:
vk::MemoryAllocateInfo info{
memoryRequirements.size, // Allocation size
memoryTypeIndex}; // Memory type index
return device.getDevice().allocateMemoryUnique(info);
Important: I’ll just mention again, allocating the device memory object does not actually put any data into it - that’s something we have to do ourselves.
We next need to bind the allocated device memory to the buffer and if we were given a pointer to a datasource, copy it into the device memory object. We do this inside the body of the Internal
constructor.
The binding is performed by asking the logical device to bindBufferMemory
, passing the buffer, the memory and the offset in the memory object to bind to. The offset enables the possibility of having a single memory object which can be a shared data source for different buffers, though we won’t be using this feature:
device.getDevice().bindBufferMemory(buffer.get(), deviceMemory.get(), 0);
We will only be copying the data source if it isn’t null hence the if
statement. This will be important when we implement the static factory function where we want the staging buffer to copy the data source into its backing memory, but the device local buffer should not as it will be populated via a command buffer operation using the staging buffer as a source instead.
Assuming we have a non null datasource, the memory is mapped through the mapMemory
function of our logical device, effectively mounting it for us to use. This is followed by whatever standard mechanisms we choose to copy the data from the dataSource
into the mappedMemory
- we are using std::memcpy
. When we are finished we must unmapMemory
to unmount it again:
if (dataSource)
{
void* mappedMemory{device.getDevice().mapMemory(deviceMemory.get(), 0, size)};
std::memcpy(mappedMemory, dataSource, static_cast<size_t>(size));
device.getDevice().unmapMemory(deviceMemory.get());
}
Note: If our buffer was specified to use device local memory, we would not be permitted to perform the memory mapping and copying operations which is the whole reason for needing to stage device local buffers.
Create device local factory function
The buffer class is actually complete however to avoid writing duplicated boilerplate code whenever we want a device local buffer we will offer the createDeviceLocalBuffer
static factory function to encapsulate the manufacturing of them. We start off by passing in all the required collaborators and configurations:
ast::VulkanBuffer VulkanBuffer::createDeviceLocalBuffer(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanCommandPool& commandPool,
const vk::DeviceSize& size,
const vk::BufferUsageFlags& bufferFlags,
const void* dataSource)
{
First off we spin up a new instance of our buffer class which will be our staging buffer:
ast::VulkanBuffer VulkanBuffer::createDeviceLocalBuffer(...)
{
ast::VulkanBuffer stagingBuffer{
physicalDevice,
device,
size,
vk::BufferUsageFlagBits::eTransferSrc,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent,
dataSource};
Take note of the following important arguments for creating the staging buffer:
vk::BufferUsageFlagBits::eTransferSrc
: This establishes to Vulkan that at a later time this buffer will be used as the source for a transfer operation, which for us will be into the final device local buffer.vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
: This enables the backing memory for the staging buffer to be visible to our application code and to ensure that changes to its memory are coherent - in that they will be applied immediately.dataSource
: This is the actual bytes of data to store in the staging buffer memory which for a mesh object would be its vertices or indices.Next we create the device local buffer which will later be returned as the result of this function:
ast::VulkanBuffer VulkanBuffer::createDeviceLocalBuffer(...)
{
...
ast::VulkanBuffer deviceLocalBuffer{
physicalDevice,
device,
size,
vk::BufferUsageFlagBits::eTransferDst | bufferFlags,
vk::MemoryPropertyFlagBits::eDeviceLocal,
nullptr};
Note the following differences to the staging buffer:
vk::BufferUsageFlagBits::eTransferDst | bufferFlags
: At minimum this buffer can be used as a transfer destination which is needed to be able to transfer from our staging buffer into this buffer. The bufferFlags
allows the caller to supply any additional flags, such as defining the buffer as a vertex buffer or an index buffer. We will see how this is used later in the article when we use this function in our Vulkan mesh class.vk::MemoryPropertyFlagBits::eDeviceLocal
: This is what causes the backing memory to be device local, and is also what will prevent us from writing directly to its memory.nullptr
: We deliberately pass nullptr
as a data source so our buffer constructor doesn’t attempt to copy any data into the device memory.Now that we have both a staging buffer whose backing memory is populated with the actual data, and the device local buffer which has allocated device memory but hasn’t populated it, we use a command buffer to ask Vulkan to perform the transfer operation taking the staging memory and writing it into the device local memory:
ast::VulkanBuffer VulkanBuffer::createDeviceLocalBuffer(...)
{
...
vk::UniqueCommandBuffer commandBuffer{commandPool.beginCommandBuffer(device)};
vk::BufferCopy copyRegion{
0, // Source offset
0, // Destination offset
size}; // Size
commandBuffer->copyBuffer(stagingBuffer.getBuffer(), deviceLocalBuffer.getBuffer(), 1, ©Region);
commandPool.endCommandBuffer(commandBuffer.get(), device);
Finally we return the device local buffer which should now be fully formed and populated with the correct data. The staging buffer will be destroyed as it goes out of scope of the function call:
ast::VulkanBuffer VulkanBuffer::createDeviceLocalBuffer(...)
{
...
return deviceLocalBuffer;
}
That’s it for our buffer class - we now have a way to generate Vulkan buffers and to manufacture a buffer that is automatically migrated into device local memory.
Our Vulkan mesh embodies a similar concept to our OpenGL mesh that we authored some time ago. Its responsibilities will be to create and own the lifecycle of the vertex and index data for a loaded mesh asset with a Vulkan implementation. We already have a basic ast::Mesh
class which is agnostic of OpenGL and Vulkan which we will use as the input for constructing a Vulkan mesh. Before we create the Vulkan mesh class we’ll make a small detour back to our basic mesh class to enhance it.
Edit core/mesh.hpp
and add the following two new function signatures:
namespace ast
{
struct Mesh
{
...
const uint32_t& getNumVertices() const;
const uint32_t& getNumIndices() const;
These two functions will just make it easier to know how many vertices and indices the mesh has. Now edit core/mesh.cpp
and update the Internal
structure to hold and initialise the number of vertices and indices:
struct Mesh::Internal
{
const std::vector<ast::Vertex> vertices;
const uint32_t numVertices;
const std::vector<uint32_t> indices;
const uint32_t numIndices;
Internal(const std::vector<ast::Vertex>& vertices, const std::vector<uint32_t>& indices)
: vertices(vertices),
numVertices(static_cast<uint32_t>(vertices.size())),
indices(indices),
numIndices(static_cast<uint32_t>(indices.size())) {}
};
Note the addition of the numVertices
and numIndices
fields along with their initialisers. Scroll to the bottom of the file and add the two public function implementations as well:
const uint32_t& Mesh::getNumVertices() const
{
return internal->numVertices;
}
const uint32_t& Mesh::getNumIndices() const
{
return internal->numIndices;
}
Close the mesh class then create vulkan-mesh.hpp
and vulkan-mesh.cpp
in the Vulkan source folder. Edit vulkan-mesh.hpp
with the following:
#pragma once
#include "../../core/graphics-wrapper.hpp"
#include "../../core/internal-ptr.hpp"
#include "../../core/mesh.hpp"
#include "vulkan-command-pool.hpp"
#include "vulkan-device.hpp"
#include "vulkan-physical-device.hpp"
namespace ast
{
struct VulkanMesh
{
VulkanMesh(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanCommandPool& commandPool,
const ast::Mesh& mesh);
const vk::Buffer& getVertexBuffer() const;
const vk::Buffer& getIndexBuffer() const;
const uint32_t& getNumIndices() const;
private:
struct Internal;
ast::internal_ptr<Internal> internal;
};
} // namespace ast
Our Vulkan mesh takes a regular mesh object as its input source along with a few Vulkan components needed to initialise itself with. We expose the vertex buffer, the index buffer and how many indices the Vulkan mesh has which will all be used in a later article in the rendering pipeline.
Edit vulkan-mesh.cpp
with the following:
#include "vulkan-mesh.hpp"
#include "vulkan-buffer.hpp"
#include <vector>
using ast::VulkanMesh;
namespace
{
ast::VulkanBuffer createVertexBuffer(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanCommandPool& commandPool,
const ast::Mesh& mesh)
{
return ast::VulkanBuffer::createDeviceLocalBuffer(physicalDevice,
device,
commandPool,
sizeof(ast::Vertex) * mesh.getNumVertices(),
vk::BufferUsageFlagBits::eVertexBuffer,
mesh.getVertices().data());
}
ast::VulkanBuffer createIndexBuffer(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanCommandPool& commandPool,
const ast::Mesh& mesh)
{
return ast::VulkanBuffer::createDeviceLocalBuffer(physicalDevice,
device,
commandPool,
sizeof(uint32_t) * mesh.getNumIndices(),
vk::BufferUsageFlagBits::eIndexBuffer,
mesh.getIndices().data());
}
} // namespace
struct VulkanMesh::Internal
{
const ast::VulkanBuffer vertexBuffer;
const ast::VulkanBuffer indexBuffer;
const uint32_t numIndices;
Internal(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanCommandPool& commandPool,
const ast::Mesh& mesh)
: vertexBuffer(::createVertexBuffer(physicalDevice, device, commandPool, mesh)),
indexBuffer(::createIndexBuffer(physicalDevice, device, commandPool, mesh)),
numIndices(mesh.getNumIndices()) {}
};
VulkanMesh::VulkanMesh(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanCommandPool& commandPool,
const ast::Mesh& mesh)
: internal(ast::make_internal_ptr<Internal>(physicalDevice, device, commandPool, mesh)) {}
const vk::Buffer& VulkanMesh::getVertexBuffer() const
{
return internal->vertexBuffer.getBuffer();
}
const vk::Buffer& VulkanMesh::getIndexBuffer() const
{
return internal->indexBuffer.getBuffer();
}
const uint32_t& VulkanMesh::getNumIndices() const
{
return internal->numIndices;
}
The vertexBuffer
field is an instance of our new buffer class populated with the vertex data of the mesh via the createVertexBuffer
function. In the createVertexBuffer
function you can see that we are using our new static factory function to generate a buffer, specifying a usage flag of vk::BufferUsageFlagBits::eVertexBuffer
to indicate that the buffer can be used in a pipeline as a source for vertex data. We also pass in the vector of vertex objects from the origin mesh as the byte data to fill the buffer with.
The indexBuffer
field is another instance of our buffer class but with a usage flag of vk::BufferUsageFlagBits::eIndexBuffer
to indicate that it can be used as a pipeline source of indices. Of course we feed the vector of indices into the factory function as the byte data to use.
We also snag the numIndices
from the source mesh and expose it publicly as we’ll need it later during rendering.
The rest of the class is mostly just boilerplate.
Our Vulkan asset manager is responsible for loading and caching mesh objects much the same as our OpenGL asset manager. We need to make a small adjustment to the header file for the Vulkan asset manager as creating a Vulkan mesh requires us to provide a command pool which we hadn’t needed before. Edit vulkan-asset-manager.hpp
and update the header includes to give access to our command pool class as well as our new Vulkan mesh class:
#include "../../core/asset-manifest.hpp"
#include "../../core/internal-ptr.hpp"
#include "vulkan-command-pool.hpp"
#include "vulkan-device.hpp"
#include "vulkan-mesh.hpp"
#include "vulkan-physical-device.hpp"
#include "vulkan-render-context.hpp"
Now edit the signature of the loadAssetManifest
function to include a command pool argument and also add a new function getStaticMesh
that will allow us to look up a cached Vulkan mesh instance:
namespace ast
{
struct VulkanAssetManager
{
...
void loadAssetManifest(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanRenderContext& renderContext,
const ast::VulkanCommandPool& commandPool,
const ast::AssetManifest& assetManifest);
const ast::VulkanMesh& getStaticMesh(const ast::assets::StaticMesh& staticMesh) const;
Note: We don’t need to add the command pool argument to the
reloadContextualAssets
because our Vulkan mesh instances won’t be affected by Vulkan lifecycle changes therefore won’t need to be reloaded.
Jump over to vulkan-asset-manager.cpp
to add the implementation. Start off by introducing a new createMesh
free function in the anonymous namespace which is able to construct a new Vulkan mesh object:
namespace
{
...
ast::VulkanMesh createMesh(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanCommandPool& commandPool,
const ast::assets::StaticMesh& staticMesh)
{
std::string meshPath{ast::assets::resolveStaticMeshPath(staticMesh)};
ast::log("ast::VulkanAssetManager::createMesh", "Creating static mesh from " + meshPath);
return ast::VulkanMesh(physicalDevice,
device,
commandPool,
ast::assets::loadOBJFile(meshPath));
}
} // namespace
The great news is that we can recycle all the mesh loading code that we authored a long time ago - this was always the intended outcome when we decided to create the ast::Mesh
abstraction so I think that decision paid off.
We start by resolving the file path of the mesh asset itself:
std::string meshPath{ast::assets::resolveStaticMeshPath(staticMesh)};
We then do a bit of logging output so we can see what assets are loading (probably should have done this in the OpenGL asset manager too) then return a new ast::VulkanMesh
object, using the ast::assets::loadOBJFile
function to build the ast::Mesh
source object.
Next we move down to the Internal
structure and add a another hash map to be our cache for static mesh objects:
struct VulkanAssetManager::Internal
{
std::unordered_map<ast::assets::Pipeline, ast::VulkanPipeline> pipelineCache;
std::unordered_map<ast::assets::StaticMesh, ast::VulkanMesh> staticMeshCache;
The loadAssetManifest
function inside the Internal
structure is already configured to load all the pipelines it finds in the asset manifest argument. We can add some more code to this function to do the same thing for static meshes, note that we have also added the commandPool
argument:
struct VulkanAssetManager::Internal
{
...
void loadAssetManifest(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanRenderContext& renderContext,
const ast::VulkanCommandPool& commandPool,
const ast::AssetManifest& assetManifest)
{
...
for (const auto& staticMesh : assetManifest.staticMeshes)
{
if (staticMeshCache.count(staticMesh) == 0)
{
staticMeshCache.insert(std::make_pair(
staticMesh,
::createMesh(physicalDevice, device, commandPool, staticMesh)));
}
}
}
The introduction of the command pool argument will give us syntax errors, so we need to also add the command pool argument to the loadAssetManifest
public function implementation and forward it to the internal structure:
void VulkanAssetManager::loadAssetManifest(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanRenderContext& renderContext,
const ast::VulkanCommandPool& commandPool,
const ast::AssetManifest& assetManifest)
{
internal->loadAssetManifest(physicalDevice, device, renderContext, commandPool, assetManifest);
}
Finally we add the implementation of the getStaticMesh
public function which simply returns the static mesh in the hash map cache for the type we are interested in:
const ast::VulkanMesh& VulkanAssetManager::getStaticMesh(const ast::assets::StaticMesh& staticMesh) const
{
return internal->staticMeshCache.at(staticMesh);
}
We are almost done, one more command pool argument to fix in the vulkan-context.cpp
file within the Internal
structure:
struct VulkanContext::Internal
{
...
void loadAssetManifest(const ast::AssetManifest& assetManifest)
{
assetManager.loadAssetManifest(physicalDevice, device, renderContext, commandPool, assetManifest);
}
Save your changes and run the program. You should see the following output - the screenshot is of my Windows machine but all the other platforms would show something similar:
If you resize the window to cause a Vulkan lifecycle change you can observe that although the pipeline is reloaded, the meshes are not:
Note the additional log output where the pipeline is loaded a second time:
ast::VulkanAssetManager::createPipeline: Creating pipeline: default
That’s static mesh loading out of the way for Vulkan - next stop is loading textures!
The code for this article can be found here.
Continue to Part 27: Vulkan load textures.
End of part 26