Rotating squaregit status Courtesy of the UBO/descriptor set

parent 10df13b5
......@@ -16,7 +16,7 @@ SHADIR = shaders
TARGET = learn
LIBS = -L$(VULKAN_SDK_PATH)/lib `pkg-config --static --libs glfw3` -lvulkan
CXXFLAGS = -Wall -I$(VULKAN_SDK_PATH)/include -std=c++14 -O3 -DNDEBUG
CXXFLAGS = -Wall -I$(VULKAN_SDK_PATH)/include -std=c++14 -O3
CXX = g++
LINKER = g++
......@@ -39,6 +39,17 @@ INCLUDES := $(wildcard $(SRCDIR)/*.h)
OBJECTS := $(SOURCES:$(SRCDIR)/%.cpp=$(OBJDIR)/%.o)
#Main Rules
.PHONY: all debug release
all: debug
# Debug needs to add an extra CXX flag.
debug: CXXFLAGS += -DNDEBUG
debug: $(BINDIR)/$(TARGET)
release: $(BINDIR)/$(TARGET)
#Worker Rules
$(BINDIR)/$(TARGET): $(OBJECTS) $(V_OUT) $(F_OUT)
@$(LINKER) $(OBJECTS) $(LFLAGS) -o $@
......
......@@ -4,6 +4,13 @@
//GLSL functions using global variables.
//We're gonna update the model view projection matrix every frame.
layout(binding = 0) uniform UniformBufferObject { //We reference a binding in the descriptor layout.
mat4 model;
mat4 view;
mat4 proj;
} ubo;
// Get data from the vertex buffer, index 0/1.
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor; //Vertex Attributes.
......@@ -19,8 +26,10 @@ out gl_PerVertex {
void main() {
// gl_VertexIndex contains the index of the current vertex.
// Usually indexing into the vertex buffer - here, into the hardcoded positions array.
gl_Position = vec4(inPosition, 0.0, 1.0);
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0); //Compute final camera-projected position of vert in clip coordinates.
fragColor = inColor; //Set the per-vertex fragment color. The fragment shader will interpolate.
//The position is made from these positions with dummy z and w components, to make clip coordinates.
//gl_Position is, as a global variable, the output.
......
#define GLFW_INCLUDE_VULKAN //Let GLFW include Vulkan.
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> //Can generate matrix transforms.
#include <chrono> //Timing lib.
#include <iostream>
#include <stdexcept>
#include <functional>
......@@ -169,6 +171,21 @@ struct Vertex {
//Resource Descriptors - A way for shaders to access resources like buffers or images.
// Three Steps: Specify Descriptor Layout (during pipeline creation), Allocate Descriptor Set (from a descriptor pool), Bind Descriptor Set (during rendering).
//Descriptor Layout: The types of resources that are going to be accessed by the pipeline - just like a render pass specifies the types of attachments that'll be accessed.
//Descriptor Set: Specifies the buffer/image resources bound to the descriptors - like framebuffers specify image views to bind to render pass attachments.
// It's then bound for the drawing commands, like the vertex buffers and the relevant framebuffer.
//There are many differnet kinds of descriptors. We'll make a camera (model view projection) matrix using a Uniform Buffer Object (UBO):
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj; //GLM types are binary compatible with the way the shader expects it - we can easily memcpy it onwards.
};
// Interleaved - each Vertex struct entry now has { vec2 pos, vec3 color }
const std::vector<Vertex> vertices = {
{{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}},
......@@ -195,6 +212,8 @@ class HelloTriangleApplication {
}
private :
bool quitApp = false; //Set to true to quit gracefully after drawing the next frame.
GLFWwindow* window; //Pointer to the GLFW window.
VkInstance instance; //The Vulkan instance itself.
VkDebugReportCallbackEXT callback; //Even the debug callback is managed with a created/destroyed handle...
......@@ -208,6 +227,10 @@ class HelloTriangleApplication {
std::vector<VkImageView> swapChainImageViews; //Handles for accessing the swap chain images.
std::vector<VkFramebuffer> swapChainFramebuffers; //The framebuffers that bind the color attachments from the render pass.
VkDescriptorPool descriptorPool; //The descriptor pool allocating descriptor sets (UBOs).
VkDescriptorSetLayout descriptorSetLayout; //The descriptor set layout for our UBO.
VkDescriptorSet descriptorSet; //The UBO descriptor set itself.
VkRenderPass renderPass; //The render pass, with subpasses that draw the things we see!
VkPipelineLayout pipelineLayout; //The pipeline layout.
VkPipeline graphicsPipeline; //The graphics pipeline!
......@@ -226,9 +249,13 @@ class HelloTriangleApplication {
// That makes data more cache-friendly, and one can even reuse the same memory chunk if not used during the same render ops - called "aliasing".
VkBuffer vertexBuffer; //The buffer that holds vertex data, which we can access.
VkDeviceMemory vertexBufferMemory; //The handle to the memory where the vertex buffer is actually stored (accessed from the vert buffer).
VkBuffer indexBuffer; //The buffer holding indexes to the vertex data, so we can access vert data with less redundancy.
VkDeviceMemory indexBufferMemory;
VkBuffer uniformBuffer; //The buffer containing the UBO data.
VkDeviceMemory uniformBufferMemory; //The memory attached.
static void onWindowResized(GLFWwindow* window, int width, int height) {
//Handles GLFW resize events.
......@@ -240,6 +267,14 @@ class HelloTriangleApplication {
app->recreateSwapChain();
}
static void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
HelloTriangleApplication* app = reinterpret_cast<HelloTriangleApplication*>(glfwGetWindowUserPointer(window));
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) { //Exit the app when we press ESC!
app->quitApp = true;
}
}
void initWindow() {
//Create a GLFW window.
......@@ -249,9 +284,14 @@ class HelloTriangleApplication {
window = glfwCreateWindow(INIT_WIDTH, INIT_HEIGHT, "Vulkan Learning!", nullptr, nullptr); //Make the window!
//When the window is resized, we need to recreate the swapchain. To do so, we catch the event:
glfwSetWindowUserPointer(window, this); //We store an arbitrary pointer to the entire application in the window!
//We store an arbitrary pointer to the entire application in the window! So we can modify it from within.
glfwSetWindowUserPointer(window, this);
//Resize Callback: When the window is resized, we need to recreate the swapchain. To do so, we catch the event:
glfwSetWindowSizeCallback(window, HelloTriangleApplication::onWindowResized); //Only accepts a function pointer with specific arguments, as an argument.
//Input Events
glfwSetKeyCallback(window, keyCallback);
}
bool checkValidationLayerSupport() {
......@@ -1112,7 +1152,11 @@ class HelloTriangleApplication {
rasterizer.lineWidth = 1.0f; //Thickness of lines in fragments. Thicker than 1.0f requires enabling the wideLines GPU feature.
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; //Backface culling. Can be set to front, back, or disabled.
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; //The vertex order for faces to be considered front-facing.
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; //The vertex order for faces to be considered front-facing.
// We would be using CLOCKWISE - except, because we did a y-flip in the projection matrix, vertices are being drawn in clockwise order.
// This makes backface culling kick in backwards - drawing nothing. So, because of the clip, we must determine front-facingness with COUNTER_CLOCKWISE.
// The internal algorithms must use vertex order to determine front/back facingness...
rasterizer.depthBiasEnable = VK_FALSE; //Can alter depth values using a bias/constant values below.
......@@ -1194,8 +1238,8 @@ class HelloTriangleApplication {
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0;
pipelineLayoutInfo.pSetLayouts = nullptr;
pipelineLayoutInfo.setLayoutCount = 1; //We're using one "uniform" value - a UBO descriptor set to pass the transform matrix to the vertex shader.
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
pipelineLayoutInfo.pushConstantRangeCount = 0; //Push constants are another way of passing dynamic values to shaders.
pipelineLayoutInfo.pPushConstantRanges = 0;
......@@ -1366,6 +1410,13 @@ class HelloTriangleApplication {
vkCmdBindIndexBuffer(commandBuffers[i], indexBuffer, 0, VK_INDEX_TYPE_UINT32); //We can only bind one index buffer for use when drawing.
// /\ Fields: command buffer, offset, # of bindings to specify vert buffers for, array of vertex buffers to bind, byte offsets to read vertex data from.
//Bind our description set, so the shaders can use its values (the UBO holding the projection matrix) while drawing!
vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);
//Descriptor sets are not unique to graphics pipelines. We have to specify that we want to use a graphics pipeline, not a compute pipeline.
// The descriptors are based on the pipelineLayout pipeline layout.
// Next three: Index of the first descriptor set, number of sets to bind, arrya of sets to bind.
// Last two: Array of offsets for dynamic descriptors.
//Buffer to record to, indicesCount, instanceCount (1 if not doing that), firstIndex (index buffer offset), indexOffset (offset to add to the indices), firstInstance (instance offset).
vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size()), 1, 0, 0, 0); //vertexCount is now dynamic to vertices.
......@@ -1574,6 +1625,91 @@ class HelloTriangleApplication {
vkFreeMemory(device, stagingBufferMemory, nullptr); //Kill the temp staging buffer and its memory.
}
void createDescriptorSetLayout() {
VkDescriptorSetLayoutBinding uboLayoutBinding = {};
uboLayoutBinding.binding = 0; //This is the UBO binding index we access from the shader.
uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; //This is a UBO binding.
uboLayoutBinding.descriptorCount = 1; //We'll only make one descriptor. This is the # of values in the array (we can use an array of UBOs).
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; //In which shader stages the descriptor is going to be referenced.
//Can be a combination of VkShaderStage flags, or the value VK_SHADER_STAGE_ALL_GRAPHICS (to use it in all).
uboLayoutBinding.pImmutableSamplers = nullptr; //Only relevant for image sampling related descriptors.
VkDescriptorSetLayoutCreateInfo layoutInfo = {}; //Creation struct for the Descriptor Set
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1; //Only one binding.
layoutInfo.pBindings = &uboLayoutBinding; //The one layout binding we have, corresponding to our UBO.
//Make the descriptor set layout, with its bindings.
if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {
throw std::runtime_error("Failed to create descriptor set layout!");
}
}
void createUniformBuffer() {
//Make a buffer visible to the CPU. Since we're updating every single frame, a staging buffer would just slow it down with unneccessary overhead.
VkDeviceSize bufferSize = sizeof(UniformBufferObject);
createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, uniformBuffer, uniformBufferMemory);
}
void createDescriptorPool() {
VkDescriptorPoolSize poolSize = {};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; //Our descriptor sets will contain one type.
poolSize.descriptorCount = 1; //Our descriptor sets will contain one descriptor type.
VkDescriptorPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1; //We have one VkDescriptorPoolSize.
poolInfo.pPoolSizes = &poolSize;
poolInfo.maxSets = 1; //Max # of descriptor sets that will be allocated.
//There is an optional flag: VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, describing whether it can be freed
poolInfo.flags = 0;
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
throw std::runtime_error("Failed to create descriptor pool!");
}
}
void createDescriptorSet() {
//First, we need to allocate the descriptor sets from the descriptor pool.
VkDescriptorSetLayout layouts[] = {descriptorSetLayout}; //The descriptor set layout to base the descriptor set on.
VkDescriptorSetAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool; //The descriptor pool to allocate from.
allocInfo.descriptorSetCount = 1; //How many descriptor sets to allocate.
allocInfo.pSetLayouts = layouts; //The descriptor set layours to base the descriptor sets being allocated on.
if (vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet) != VK_SUCCESS) {
throw std::runtime_error("Failed to allocate descriptor set (UBO)!");
}
//Descriptor sets are automatically freed when descriptor set pools are destroyed.
//Now. The descriptor set has been allocated. But, the descriptors in the descriptor set needs to be configured still.
// Our descriptor refers to a buffer.
VkDescriptorBufferInfo bufferInfo = {};
bufferInfo.buffer = uniformBuffer; //The buffer this descriptor refers to.
bufferInfo.offset = 0; //The offset in the buffer.
bufferInfo.range = sizeof(UniformBufferObject); //How big the used snippet of the buffer is.
//We update the configuration of descriptors:
VkWriteDescriptorSet descriptorWrite = {};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSet; //The descriptor set to update.
descriptorWrite.dstBinding = 0; //The binding in the updated descriptor set. Our UBO has binding 0.
descriptorWrite.dstArrayElement = 0; //Descriptors can be arrays; we're not using an array, so the index simply 0 (pointer[0] == *pointer).
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; //The type of the descriptor.
descriptorWrite.descriptorCount = 1; //There's one descriptor.
//One can update multiple descriptors in an array at once; ^^ specifies how many array elements we wish to update.
descriptorWrite.pBufferInfo = &bufferInfo; //Used for descriptors that refer to buffer data. As UNIFORM_BUFFER does.
vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
}
void initVulkan() {
createInstance(); //The Vulkan instance is the link between application and Vulkan.
setupDebugCallback(); //Setup the debug callback that interfaces with the validation layer.
......@@ -1583,11 +1719,15 @@ class HelloTriangleApplication {
createSwapChain(); //Create the swap chain, with optimal settings, that'll swap images to the surface!
createImageViews(); //Create the image views, describing how to access images and how to treat them.
createRenderPass(); //Create the render pass, specifying the color/depth buffers, samples for each, and how to handle them while rendering.
createDescriptorSetLayout(); //We need to specify a descriptor set layout from which to allocate the UBO.
createGraphicsPipeline(); //The graphics pipeline is completely immutable - we have to build it from scratch, for performance.
createFramebuffers(); //Make the framebuffers for all the images in the swap chain - and use the one that corresponds to the retrieved image at draw time.
createCommandPool(); //Commands, like draw/memory transfer, are executed by command buffer objects. All the hard setup can be done in multiple threads, then!
createVertexBuffer(); //Vertex buffers store vertex data to be read by the GPU.
createIndexBuffer(); //Index buffers store indices to the vertex buffer, to reduce redundancy.
createUniformBuffer(); //Uniform buffers store data accessible from the shaders.
createDescriptorPool(); //Descriptor sets must be allocated from a pool to be used.
createDescriptorSet(); //Allocate descriptor sets from the descriptor pool!
createCommandBuffers(); // We need one command buffer for each image in the swap chain, because they bind to framebuffers.
createSemaphore(); //Create the sephamores we need to synchronize things.
}
......@@ -1672,10 +1812,54 @@ class HelloTriangleApplication {
vkQueueWaitIdle(presentQueue);
}
void updateUniformBuffer() {
//Updates the projection matrix to spin the geometry.
// We want 90 degrees per second regardless of framerate!
static auto startTime = std::chrono::high_resolution_clock::now(); //Calculate a precise time.
auto currentTime = std::chrono::high_resolution_clock::now();
float time = std::chrono::duration_cast<std::chrono::microseconds>(currentTime - startTime).count() / 1e6f;
UniformBufferObject ubo = {};
//The model (object's own) rotation will be a simple rotation around the z axis using the time variable.
ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
// glm::rotate(existing transform, rotation angle, rotation axis).
// Our existing transform is an identity matrix, the angle is time * 90 degrees (corresponding to framerate), then the Z axis.
//The view (our position and direction) transform has us looking at the geometry from above, at a 45 degree angle.
ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));
// glm::lookAt(eye position, center (lookat) position, "up" axis.
// The projection is a perspective projection with a 45 degree vertical field of view.
ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f);
// glm::perspective(vert FOV, aspect ratio, near view plane, far view plane).
// We must use the current swap chain extent to find the width/height of the image regardless of resizing.
ubo.proj[1][1] *= -1; //GLM was designed for OpenGL, where the Y coordinate is inverted. We must flip the sign in the matrix, or render things upside down.
void* data;
vkMapMemory(device, uniformBufferMemory, 0, sizeof(ubo), 0, &data);
memcpy(data, &ubo, sizeof(ubo)); //Do the copy.
vkUnmapMemory(device, uniformBufferMemory);
//Using a UBO is NOT the most efficient way to pass frequently changing values to a shader.
// A more efficient way to pass a small buffer of data is Push Constants.
}
void mainLoop() {
//Time to bring it all together :D
while (!glfwWindowShouldClose(window)) {
glfwPollEvents(); //Loops and checks for events until the window "should close".
//Handle any new state as a result of triggered events.
if (quitApp) {
break;
}
//Draw the next frame.
updateUniformBuffer();
drawFrame(); //Draw frames as fast as possible :P.
}
......@@ -1685,6 +1869,12 @@ class HelloTriangleApplication {
void cleanup() {
cleanupSwapChain(); //Does swapchain related cleanup.
vkDestroyDescriptorPool(device, descriptorPool, nullptr);
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); //Kill the descriptor set layout.
vkDestroyBuffer(device, uniformBuffer, nullptr); //Kill the UBO buffer. It does not depend on the swap chain.
vkFreeMemory(device, uniformBufferMemory, nullptr); //We need to free all allocated memory manually. This is the memory holding the UBO buffer.
vkDestroyBuffer(device, indexBuffer, nullptr); //Kill the index buffer. It does not depend on the swap chain.
vkFreeMemory(device, indexBufferMemory, nullptr); //We need to free all allocated memory manually. This is the memory holding the index buffer.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment