In the last post, we introduced suppport for the Vulkan Memory Allocator to take care of our memory allocations. With this in place, we can now try to extend a bit on our display system and try to vertex buffers to provided the data we want to draw instead of hardcoding the value in our shader. And in this process we can start the design a subsystem to create various simple shapes like a plane, a cube, a sphere, etc.
class NVVULKAN_EXPORT VulkanVertexBuffer : public VulkanObject { NV_DECLARE_NO_COPY(VulkanVertexBuffer) NV_DECLARE_NO_MOVE(VulkanVertexBuffer) public: struct AttributeDesc { U32 location; VkFormat format; U32 offset; }; using AttributeDescList = nv::Vector<AttributeDesc>; /** Constructor used to create a dedicated buffer for this Vertex buffer. */ VulkanVertexBuffer( VulkanDevice* dev, U64 size, VkBufferUsageFlags usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); /** Constructor used to use an existing Buffer from a given start offset * position. */ VulkanVertexBuffer(VulkanBuffer* buffer, U64 size, U64 startOffset); ~VulkanVertexBuffer() override; /** Add a vertex attribute */ void add_attribute(U32 location, VkFormat format); /** Retrieve the vertex size*/ auto get_vertex_size() const -> U32 { return _vertexSize; } protected: /** The actual Buffer object associated to this vertx buffer.*/ nv::RefPtr<VulkanBuffer> _buffer; /** Offset start position inside the underlying Buffer object. */ U64 _bufferStartOffset{0}; /** Total size in byte of this VertexBuffer */ U64 _size{0}; /** Size of a single vertex in bytes (as computed from the attributes.) */ U32 _vertexSize{0}; /** Attribute descriptions */ AttributeDescList _attributeDescs; };
/** Add an rgba32f attribute **/ void add_rgba32f(U32 location) { add_attribute(location, VK_FORMAT_R32G32B32A32_SFLOAT); }
vertSize = 5*4 -- 5 floats per vertex. vbuf = nvk.VulkanVertexBuffer(vertSize*1000) vbuf:add_rgb32f(0) -- location 0, vec3 position vbuf:add_rg32f(1) -- location 1, vec2 uv
auto VulkanPipelineVertexInputStateCreateInfo::addVertexBuffer( const VulkanVertexBuffer& vbuf, I32 bindingIndex) -> VulkanPipelineVertexInputStateCreateInfo& { // Check if we should use an automatic binding index: if (bindingIndex == -1) { bindingIndex = (I32)getCurrentBindingDescList().size(); } // Add the binding: addBindingDesc(bindingIndex, vbuf.get_vertex_size(), vbuf.get_input_rate()); // Next we also add all the attributes provided by this vertex buffer: const auto& attribs = vbuf.get_attributes(); for (const auto& attrib : attribs) { addAttributeDesc(attrib.location, attrib.format, attrib.offset, bindingIndex); } return *this; }
/** write data into this vertex buffer */ void set_data(const void* data, U64 dataSize, U64 startPosition = nv::U64_MAX);
auto create_buffer(U64 size, VkBufferUsageFlags usage, BufferCreateFlags flags = BUFFER_CREATE_FLAG_NONE, U32 devIdx = 0) -> U64;
/** Check if this buffer is mappable on host **/ auto is_host_visible() const -> bool;
// Use the VMA allocator to create this resource: VmaAllocationCreateInfo allocInfo = {}; allocInfo.usage = VMA_MEMORY_USAGE_AUTO; switch (flags) { case BUFFER_CREATE_FLAG_SEQUENTIAL_HOST_WRITE: allocInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT; _hostVisible = true; break; case BUFFER_CREATE_FLAG_RANDOM_HOST_ACCESS: allocInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT; _hostVisible = true; break; default: break; } auto* allocator = _device->get_vma_allocator(); CHECK_VK_MSG(vmaCreateBuffer(allocator, &buffer_create_info, &allocInfo, &_buffer, &_allocation, nullptr), "Cannot create buffer"); CHECK(_buffer != VK_NULL_HANDLE, "Invalid VkBuffer object."); if (_hostVisible) { // Check the memory properties: VkMemoryPropertyFlags props{}; vmaGetAllocationMemoryProperties(allocator, _allocation, &props); // Check if we have the coherent bit: _hostCoherent = (props & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0; logDEBUG("Host visible memory is {}", _hostCoherent ? "coherent" : "non-coherent"); }
void VulkanBuffer::write_data(const void* data, U64 dataSize, U64 startPosition) { // Check that this buffer is host visible: CHECK(_hostVisible, "Cannot write data in non-host visible buffer."); // get the start position where to write data: nv::U8* ptr = _mappedData + startPosition; // Write the data: memcpy((void*)ptr, data, dataSize); // Flush the data if needed: if (!_hostCoherent) { _device->flush_allocation(_allocation, startPosition, dataSize); } }
void VulkanVertexBuffer::write_data(const void* data, U64 dataSize, U64 startPosition) { // So we need to write the given data, at the given start position in our // buffer. // CHeck that we can write data into the underlying buffer: CHECK(_buffer->is_host_visible(), "Cannot write data into non host visible vertex buffer."); if (startPosition == nv::U64_MAX) { // Use the current position in this Vertex position as start position: startPosition = _currentPosition; } // Otherwise we write that data taking into account our own offset from the // start of the underlying buffer: U64 offset = _bufferStartOffset + startPosition; // Write the data from the given offset position in the underlying buffer: _buffer->write_data(data, dataSize, offset); // Save our new current position: _currentPosition = startPosition + dataSize; }
auto VulkanVertexBuffer::write_float_array(const nv::FloatList& data, U64 startPosition, U64 inputOffset, U64 inputSize) -> U64 { if (inputSize == nv::U64_MAX) { // Use the remaining size from the input: inputSize = data.size() - inputOffset; } // Check that we are getting out of the input range: CHECK((inputOffset + inputSize) <= data.size(), "Out of input data range."); // Get our data pointer from the input: const float* ptr = data.data(); // Apply the niput offset (in number of floats): ptr += inputOffset; // Then request the copy, and return the current position: return write_data((const void*)ptr, inputSize * sizeof(nv::Float), startPosition); }
auto create_vertex_buffer( U64 size, BufferCreateFlags flags = BUFFER_CREATE_FLAG_NONE, VkVertexInputRate inputRate = VK_VERTEX_INPUT_RATE_VERTEX, VkBufferUsageFlags usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT) -> nv::RefPtr<VulkanVertexBuffer>;
auto VulkanBuffer::create_vertex_buffer(U64 size, U64 startOffset, VkVertexInputRate inputRate) -> nv::RefPtr<VulkanVertexBuffer> { return nv::create_ref_object<VulkanVertexBuffer>(this, size, startOffset, inputRate); }
local vbuf = self.vkeng:create_vertex_buffer(4*6, nvk.BufferCreateFlags.SEQUENTIAL_HOST_WRITE)
vbuf:add_rg32f(0)
vbuf:write_float_array { 0.0, -0.5, 0.5, 0.5, -0.5, 0.5 }
-- Assign the vertex buffer: local is = cfg:getCurrentVertexInputState() is:addVertexBuffer(vbuf)
-- Create the ShaderModules from our test GLSL source file function Class:createGLSLShaderModules() -- Here we should be able to load a GLSL source file: -- self.vert_shader = self.vkeng:create_vertex_shader("tests/test_simple.glsl") -- self.frag_shader = self.vkeng:create_fragment_shader("tests/test_simple.glsl") -- Read the position fro vertexbuffer: self.vert_shader = self.vkeng:create_vertex_shader("tests/test_vertexbuffer.glsl") self.frag_shader = self.vkeng:create_fragment_shader("tests/test_vertexbuffer.glsl") logDEBUG("Created shader modules.") end
-- Bind the vertex buffer: cbuf:bind_vertex_buffer(vbuf, 0)
-- write the data: vbuf:write_float_array { 0.0, -0.8, 0.5, 0.5, -0.5, 0.5 }
-- Prepare our vertex buffer here, with support to write to it: local vbufIdx = self.vkeng:create_vertex_buffer(4 * 3 * 5, nvk.BufferCreateFlags.SEQUENTIAL_HOST_WRITE) local vbuf = self.vkeng:get_vertex_buffer(vbufIdx) -- Specify the vertex attributes: vbuf:add_rg32f(0) -- vec2 position at location=0 vbuf:add_rgb32f(1) -- vec3 color at location=2 -- write the data: vbuf:write_float_array { 0.0, -0.5, 1.0, 0.0, 0.0, 0.5, 0.5, 0.0, 1.0, 0.0, -0.5, 0.5, 0.0, 0.0, 1.0 }
#ifdef _VERTEX_ layout(location = 0) in vec2 position; layout(location = 1) in vec3 color; layout(location=0) out vec3 vertColor; void main() { gl_Position = vec4(position, 0.0, 1.0); vertColor = color; } #endif #ifdef _FRAGMENT_ layout(location=0) in vec3 fragColor; layout(location=0) out vec4 outColor; void main() { outColor = vec4(fragColor, 1.0); } #endif
auto rgba_to_u32(U8 r, U8 g, U8 b, U8 a) -> U32 { // Note: we don't need to take into account the byte order below: // for RGBA we always want to have the final order R,G,B,A in memory. U32 val = 0; U8* ptr = (U8*)&val; *ptr++ = r; *ptr++ = g; *ptr++ = b; *ptr++ = a; return val; } auto rgba_to_f32(U8 r, U8 g, U8 b, U8 a) -> Float { Float val = 0; U8* ptr = (U8*)&val; *ptr++ = r; *ptr++ = g; *ptr++ = b; *ptr++ = a; return val; }
-- Prepare our vertex buffer here, with support to write to it: local vbufIdx = self.vkeng:create_vertex_buffer(4 * 3 * 3, nvk.BufferCreateFlags.SEQUENTIAL_HOST_WRITE) local vbuf = self.vkeng:get_vertex_buffer(vbufIdx) -- Specify the vertex attributes: vbuf:add_rg32f(0) -- vec2 position at location=0 vbuf:add_rgba8(1) -- vec4 color at location=2 -- write the data: vbuf:write_float_array { 0.0, -0.5, nv.rgba_to_f32(255, 0, 0, 255), 0.5, 0.5, nv.rgba_to_f32(0, 255, 0, 255), -0.5, 0.5, nv.rgba_to_f32(0, 0, 255, 255), }
And now I think this is good enough for this session See you next time!