gpu-fractal/main.c

581 lines
18 KiB
C

/*
* Copyright (c) Camden Dixie O'Brien
* SPDX-License-Identifier: AGPL-3.0-only
*/
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define CLAMP(x, min, max) MAX((min), MIN(x, (max)))
#define QUEUE_FAMILY_PROP_BUFFER_SIZE 16
#define EXT_PROP_BUFFER_SIZE 256
#define SURFACE_FMT_BUFFER_SIZE 64
#define PRESENT_MODE_BUFFER_SIZE 8
#define SWAPCHAIN_IMG_BUFFER_SIZE 4
#define MAX_SHADER_SIZE (4 * 1024)
typedef struct {
float width, height;
struct {
float x, y;
} shift;
float zoom;
} params_t;
static const char *layers[] = { "VK_LAYER_KHRONOS_validation" };
static const char *swapchain_ext_name = VK_KHR_SWAPCHAIN_EXTENSION_NAME;
static VkShaderModule load_shader_module(VkDevice dev, const char *path)
{
static char buf[MAX_SHADER_SIZE]
__attribute__((aligned(__alignof__(uint32_t))));
FILE *f = fopen(path, "rb");
const int size = fread(buf, 1, MAX_SHADER_SIZE, f);
assert(size > 0 && feof(f));
fclose(f);
const VkShaderModuleCreateInfo create_info = {
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = size,
.pCode = (uint32_t *)buf,
};
VkShaderModule mod;
const VkResult result
= vkCreateShaderModule(dev, &create_info, NULL, &mod);
assert(result == VK_SUCCESS);
return mod;
}
int main(void)
{
// Set up window and surface
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
GLFWwindow *window
= glfwCreateWindow(900, 900, "GPU Fractal", NULL, NULL);
assert(window);
// Initialise Vulkan instance.
const VkApplicationInfo app_info = {
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pApplicationName = "Vulkan Test",
.applicationVersion = VK_MAKE_VERSION(1, 0, 0),
.pEngineName = "No Engine",
.engineVersion = VK_MAKE_VERSION(1, 0, 0),
.apiVersion = VK_API_VERSION_1_0,
};
uint32_t glfw_ext_count = 0;
const char **glfw_ext_names
= glfwGetRequiredInstanceExtensions(&glfw_ext_count);
const VkInstanceCreateInfo inst_create_info = {
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pApplicationInfo = &app_info,
.enabledExtensionCount = glfw_ext_count,
.ppEnabledExtensionNames = glfw_ext_names,
.enabledLayerCount = sizeof(layers) / sizeof(layers[0]),
.ppEnabledLayerNames = layers,
};
VkInstance inst;
VkResult result = vkCreateInstance(&inst_create_info, NULL, &inst);
assert(result == VK_SUCCESS);
// Create surface
VkSurfaceKHR surface;
result = glfwCreateWindowSurface(inst, window, NULL, &surface);
assert(result == VK_SUCCESS);
// Select a physical device
VkPhysicalDevice physical_dev;
uint32_t dev_count = 1;
result = vkEnumeratePhysicalDevices(inst, &dev_count, &physical_dev);
assert(result == VK_SUCCESS || result == VK_INCOMPLETE);
// Select queue family
uint32_t queue_family_count = QUEUE_FAMILY_PROP_BUFFER_SIZE;
VkQueueFamilyProperties
queue_family_props[QUEUE_FAMILY_PROP_BUFFER_SIZE];
vkGetPhysicalDeviceQueueFamilyProperties(
physical_dev, &queue_family_count, queue_family_props);
uint32_t queue_family;
bool found_queue_family = false;
for (unsigned i = 0; i < queue_family_count; ++i) {
if (queue_family_props[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
VkBool32 surface_support = false;
vkGetPhysicalDeviceSurfaceSupportKHR(
physical_dev, i, surface, &surface_support);
if (surface_support) {
found_queue_family = true;
queue_family = i;
break;
}
}
}
assert(found_queue_family);
// Create the logical device and get the queue handle.
float queue_priority = 1.0;
const VkDeviceQueueCreateInfo queue_create_info = {
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = queue_family,
.queueCount = 1,
.pQueuePriorities = &queue_priority,
};
const VkDeviceCreateInfo dev_create_info = {
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.pQueueCreateInfos = &queue_create_info,
.queueCreateInfoCount = 1,
.enabledExtensionCount = 1,
.ppEnabledExtensionNames = &swapchain_ext_name,
};
VkDevice dev;
result = vkCreateDevice(physical_dev, &dev_create_info, NULL, &dev);
assert(result == VK_SUCCESS);
VkQueue queue;
vkGetDeviceQueue(dev, queue_family, 0, &queue);
// Check that the physical device has swap chain support
uint32_t ext_count = EXT_PROP_BUFFER_SIZE;
VkExtensionProperties ext_props[EXT_PROP_BUFFER_SIZE];
result = vkEnumerateDeviceExtensionProperties(
physical_dev, NULL, &ext_count, ext_props);
assert(result == VK_SUCCESS);
bool swapchain_support = false;
for (unsigned i = 0; i < ext_count; ++i) {
if (strcmp(swapchain_ext_name, ext_props[i].extensionName) == 0) {
swapchain_support = true;
break;
}
}
assert(swapchain_support);
// Select a surface format, preferring R8G8B8A8_SRGB with
// SRGB_NONLINEAR colour space.
uint32_t surface_fmt_count = SURFACE_FMT_BUFFER_SIZE;
VkSurfaceFormatKHR surface_fmts[SURFACE_FMT_BUFFER_SIZE];
result = vkGetPhysicalDeviceSurfaceFormatsKHR(
physical_dev, surface, &surface_fmt_count, surface_fmts);
assert(result == VK_SUCCESS);
assert(surface_fmt_count > 0);
VkSurfaceFormatKHR surface_fmt = surface_fmts[0];
for (unsigned i = 0; i < surface_fmt_count; ++i) {
if (surface_fmts[i].format == VK_FORMAT_R8G8B8A8_SRGB
&& surface_fmts[i].colorSpace
== VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
surface_fmt = surface_fmts[i];
}
}
// Query surface capabilities
VkSurfaceCapabilitiesKHR surface_caps;
result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(
physical_dev, surface, &surface_caps);
assert(result == VK_SUCCESS);
// Select swap chain extent
VkExtent2D swapchain_extent;
if (surface_caps.currentExtent.width == UINT32_MAX) {
int width, height;
glfwGetFramebufferSize(window, &width, &height);
swapchain_extent.width = CLAMP(
(uint32_t)width, surface_caps.minImageExtent.width,
surface_caps.maxImageExtent.width);
swapchain_extent.height = CLAMP(
(uint32_t)height, surface_caps.minImageExtent.height,
surface_caps.maxImageExtent.height);
} else {
swapchain_extent = surface_caps.currentExtent;
}
// Select image count
uint32_t swapchain_img_count = surface_caps.minImageCount + 1;
assert(
surface_caps.maxImageCount == 0
|| swapchain_img_count <= surface_caps.maxImageCount);
// Create the swap chain
const VkSwapchainCreateInfoKHR swapchain_create_info = {
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
.surface = surface,
.minImageCount = swapchain_img_count,
.imageFormat = surface_fmt.format,
.imageColorSpace = surface_fmt.colorSpace,
.imageExtent = swapchain_extent,
.imageArrayLayers = 1,
.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
.preTransform = surface_caps.currentTransform,
.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
.presentMode = VK_PRESENT_MODE_FIFO_KHR,
.clipped = VK_TRUE,
.oldSwapchain = VK_NULL_HANDLE,
};
VkSwapchainKHR swapchain;
result = vkCreateSwapchainKHR(
dev, &swapchain_create_info, NULL, &swapchain);
assert(result == VK_SUCCESS);
// Retreive swapchain images
VkImage swapchain_imgs[SWAPCHAIN_IMG_BUFFER_SIZE];
swapchain_img_count = SWAPCHAIN_IMG_BUFFER_SIZE;
result = vkGetSwapchainImagesKHR(
dev, swapchain, &swapchain_img_count, swapchain_imgs);
assert(result == VK_SUCCESS);
// Create swapchain image views
VkImageView swapchain_img_views[SWAPCHAIN_IMG_BUFFER_SIZE];
VkImageViewCreateInfo img_view_create_info = {
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.viewType = VK_IMAGE_VIEW_TYPE_2D,
.format = surface_fmt.format,
.components = {
.r = VK_COMPONENT_SWIZZLE_IDENTITY,
.g = VK_COMPONENT_SWIZZLE_IDENTITY,
.b = VK_COMPONENT_SWIZZLE_IDENTITY,
.a = VK_COMPONENT_SWIZZLE_IDENTITY,
},
.subresourceRange = {
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
for (unsigned i = 0; i < swapchain_img_count; ++i) {
img_view_create_info.image = swapchain_imgs[i];
result = vkCreateImageView(
dev, &img_view_create_info, NULL, swapchain_img_views + i);
}
// Load vertex and fragment shader modules
VkShaderModule vert_shader = load_shader_module(dev, "vert_shader.spv");
VkShaderModule frag_shader = load_shader_module(dev, "frag_shader.spv");
const VkPipelineShaderStageCreateInfo shader_stages[] = {
[0] = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_VERTEX_BIT,
.module = vert_shader,
.pName = "main",
},
[1] = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_FRAGMENT_BIT,
.module = frag_shader,
.pName = "main",
},
};
// Set up input and viewport configuration
const VkPipelineVertexInputStateCreateInfo vertex_input_config = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
};
const VkPipelineInputAssemblyStateCreateInfo input_assembly_config = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP,
};
const VkViewport viewport = {
.width = (float)swapchain_extent.width,
.height = (float)swapchain_extent.height,
};
const VkRect2D scissor = { .extent = swapchain_extent };
VkPipelineViewportStateCreateInfo viewport_config = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
.viewportCount = 1,
.pViewports = &viewport,
.scissorCount = 1,
.pScissors = &scissor,
};
// Populate rasterizer configuration
const VkPipelineRasterizationStateCreateInfo rasterizer_config = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
.depthClampEnable = VK_FALSE,
.rasterizerDiscardEnable = VK_FALSE,
.polygonMode = VK_POLYGON_MODE_FILL,
.lineWidth = 1.0f,
.cullMode = VK_CULL_MODE_NONE,
};
// Populate multisampling configuration
const VkPipelineMultisampleStateCreateInfo multisampling_config = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
.sampleShadingEnable = VK_FALSE,
.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT,
};
// Set up colour blending configuration
const VkPipelineColorBlendAttachmentState colour_blend_attatchment = {
.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT
| VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT,
.blendEnable = VK_FALSE,
};
const VkPipelineColorBlendStateCreateInfo colour_blend_config = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
.logicOpEnable = VK_FALSE,
.attachmentCount = 1,
.pAttachments = &colour_blend_attatchment,
};
// Create pipeline layout
const VkPushConstantRange push_constant_range = {
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
.offset = 0,
.size = sizeof(params_t),
};
const VkPipelineLayoutCreateInfo pipeline_layout_config = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.pushConstantRangeCount = 1,
.pPushConstantRanges = &push_constant_range,
};
VkPipelineLayout pipeline_layout;
result = vkCreatePipelineLayout(
dev, &pipeline_layout_config, NULL, &pipeline_layout);
assert(result == VK_SUCCESS);
// Set up colour attachment
const VkAttachmentDescription colour_attachment = {
.format = surface_fmt.format,
.samples = VK_SAMPLE_COUNT_1_BIT,
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
.storeOp = VK_ATTACHMENT_STORE_OP_STORE,
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
};
const VkAttachmentReference colour_attachment_ref = {
.attachment = 0,
.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
};
const VkSubpassDescription subpass = {
.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
.colorAttachmentCount = 1,
.pColorAttachments = &colour_attachment_ref,
};
// Create render pass
const VkSubpassDependency dependency = {
.srcSubpass = VK_SUBPASS_EXTERNAL,
.dstSubpass = 0,
.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
.srcAccessMask = 0,
.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
};
const VkRenderPassCreateInfo render_pass_config = {
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
.attachmentCount = 1,
.pAttachments = &colour_attachment,
.subpassCount = 1,
.pSubpasses = &subpass,
.dependencyCount = 1,
.pDependencies = &dependency,
};
VkRenderPass render_pass;
result
= vkCreateRenderPass(dev, &render_pass_config, NULL, &render_pass);
assert(result == VK_SUCCESS);
// Create pipeline
const VkGraphicsPipelineCreateInfo pipeline_config = {
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
.stageCount = 2,
.pStages = shader_stages,
.pVertexInputState = &vertex_input_config,
.pInputAssemblyState = &input_assembly_config,
.pViewportState = &viewport_config,
.pRasterizationState = &rasterizer_config,
.pMultisampleState = &multisampling_config,
.pColorBlendState = &colour_blend_config,
.layout = pipeline_layout,
.renderPass = render_pass,
.subpass = 0,
};
VkPipeline pipeline;
result = vkCreateGraphicsPipelines(
dev, VK_NULL_HANDLE, 1, &pipeline_config, NULL, &pipeline);
assert(result == VK_SUCCESS);
// Create framebuffers
VkFramebuffer framebuffers[SWAPCHAIN_IMG_BUFFER_SIZE];
for (unsigned i = 0; i < swapchain_img_count; ++i) {
const VkFramebufferCreateInfo framebuffer_config = {
.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
.renderPass = render_pass,
.attachmentCount = 1,
.pAttachments = swapchain_img_views + i,
.width = swapchain_extent.width,
.height = swapchain_extent.height,
.layers = 1,
};
result = vkCreateFramebuffer(
dev, &framebuffer_config, NULL, framebuffers + i);
assert(result == VK_SUCCESS);
}
// Create command pool
const VkCommandPoolCreateInfo cmd_pool_config = {
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
.queueFamilyIndex = queue_family,
};
VkCommandPool cmd_pool;
result = vkCreateCommandPool(dev, &cmd_pool_config, NULL, &cmd_pool);
assert(result == VK_SUCCESS);
// Allocate command buffer
const VkCommandBufferAllocateInfo cmd_buf_config = {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.commandPool = cmd_pool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1,
};
VkCommandBuffer cmd_buf;
result = vkAllocateCommandBuffers(dev, &cmd_buf_config, &cmd_buf);
assert(result == VK_SUCCESS);
// Create syncronisation primitives
VkSemaphore img_avail, render_fin;
VkSemaphoreCreateInfo semaphore_config
= { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
result = vkCreateSemaphore(dev, &semaphore_config, NULL, &img_avail);
assert(result == VK_SUCCESS);
result = vkCreateSemaphore(dev, &semaphore_config, NULL, &render_fin);
assert(result == VK_SUCCESS);
VkFence in_flight;
VkFenceCreateInfo fence_config = {
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
.flags = VK_FENCE_CREATE_SIGNALED_BIT,
};
result = vkCreateFence(dev, &fence_config, NULL, &in_flight);
assert(result == VK_SUCCESS);
params_t params = {
.width = (float)swapchain_extent.width,
.height = (float)swapchain_extent.height,
.shift = {
.x = -0.743643887037158704752191506114774,
.y = 0.131825904205311970493132056385139,
},
.zoom = 1.0,
};
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
// Wait for previous frame to finish
vkWaitForFences(dev, 1, &in_flight, VK_TRUE, UINT64_MAX);
vkResetFences(dev, 1, &in_flight);
// Aquire image from swapchain
uint32_t img_index;
result = vkAcquireNextImageKHR(
dev, swapchain, UINT64_MAX, img_avail, VK_NULL_HANDLE,
&img_index);
assert(result == VK_SUCCESS);
// Reset command buffer, then begin writing
vkResetCommandBuffer(cmd_buf, 0);
const VkCommandBufferBeginInfo cmd_begin = {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
};
result = vkBeginCommandBuffer(cmd_buf, &cmd_begin);
assert(result == VK_SUCCESS);
// Start render pass
VkClearValue clear_value = { { { 0.0, 0.0, 0.0, 1.0 } } };
VkRenderPassBeginInfo render_pass_begin = {
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
.renderPass = render_pass,
.framebuffer = framebuffers[img_index],
.renderArea.extent = swapchain_extent,
.clearValueCount = 1,
.pClearValues = &clear_value,
};
vkCmdBeginRenderPass(
cmd_buf, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE);
vkCmdPushConstants(
cmd_buf, pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, 0,
sizeof(params), &params);
// Bind graphics pipeline and draw
vkCmdBindPipeline(
cmd_buf, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
vkCmdDraw(cmd_buf, 4, 1, 0, 0);
// Finish render pass and end writing to command buffer
vkCmdEndRenderPass(cmd_buf);
result = vkEndCommandBuffer(cmd_buf);
assert(result == VK_SUCCESS);
// Submit command buffer
VkPipelineStageFlags wait_stage
= VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
const VkSubmitInfo submit = {
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.waitSemaphoreCount = 1,
.pWaitSemaphores = &img_avail,
.pWaitDstStageMask = &wait_stage,
.commandBufferCount = 1,
.pCommandBuffers = &cmd_buf,
.signalSemaphoreCount = 1,
.pSignalSemaphores = &render_fin,
};
result = vkQueueSubmit(queue, 1, &submit, in_flight);
assert(result == VK_SUCCESS);
// Present frame
const VkPresentInfoKHR present_info = {
.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
.waitSemaphoreCount = 1,
.pWaitSemaphores = &render_fin,
.swapchainCount = 1,
.pSwapchains = &swapchain,
.pImageIndices = &img_index,
};
result = vkQueuePresentKHR(queue, &present_info);
assert(result == VK_SUCCESS);
// Increment zoom
params.zoom *= 0.99;
}
vkDeviceWaitIdle(dev);
/*
* Cleanup
*/
vkDestroySemaphore(dev, img_avail, NULL);
vkDestroySemaphore(dev, render_fin, NULL);
vkDestroyFence(dev, in_flight, NULL);
vkDestroyCommandPool(dev, cmd_pool, NULL);
vkDestroyPipeline(dev, pipeline, NULL);
vkDestroyRenderPass(dev, render_pass, NULL);
vkDestroyPipelineLayout(dev, pipeline_layout, NULL);
vkDestroyShaderModule(dev, vert_shader, NULL);
vkDestroyShaderModule(dev, frag_shader, NULL);
for (unsigned i = 0; i < swapchain_img_count; ++i) {
vkDestroyFramebuffer(dev, framebuffers[i], NULL);
vkDestroyImageView(dev, swapchain_img_views[i], NULL);
}
vkDestroySwapchainKHR(dev, swapchain, NULL);
vkDestroyDevice(dev, NULL);
vkDestroySurfaceKHR(inst, surface, NULL);
vkDestroyInstance(inst, NULL);
glfwDestroyWindow(window);
glfwTerminate();
}