#include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define GLFW_INCLUDE_VULKAN #include #include #include #ifdef HAVE_NCURSES #include #endif #include "fb/chfb.hpp" #include "fb/pixfb.hpp" #include "o3d/scene.hpp" #include "o3d/mesh.hpp" #include "o3d/obj3d.hpp" #include "o3d/vertex_data.hpp" #include "o3d/tri.hpp" #include "o3d/camera.hpp" #include "math/vector.hpp" #include "math/mat4.hpp" #include "math/quat.hpp" #include "math/tform.hpp" #include "ctrl/keyboard.hpp" #include "ctrl/mouse.hpp" #include "renderer.hpp" #include "obj_parser.hpp" #include "vulkan_utils.hpp" using engine::Renderer, engine::fb::CharacterFrameBuffer, engine::fb::PixelFrameBuffer, engine::o3d::Scene, engine::o3d::Mesh, engine::o3d::Triangle, engine::o3d::Camera, engine::o3d::VertexData, engine::math::Vector2, engine::math::Vector3, engine::math::Vector4, engine::math::Matrix4, engine::math::Quaternion, engine::controllers::Keyboard, engine::controllers::KeyboardKey, engine::controllers::Mouse; #define FPS 60 #define PI 3.1415926535f #define MODE_HELP 0 #define MODE_TERM 1 #define MODE_GRAPHICAL 2 #define GAME_PLANE 0 #define GAME_SUZANNE 1 #define GAME_PHYSICS 2 #define GAME GAME_PHYSICS static void print_usage(std::ostream& output_stream) { output_stream << "Usage: ./engine [-htg] [--help] [--term] [--graphical]\n" << " -h, --help show usage (this)\n" << " -t, --term terminal mode\n" << " -g, --graphical graphical mode (default)\n" << std::flush; } [[noreturn]] static void usage_error_exit() { print_usage(std::cerr); std::exit(EXIT_FAILURE); } extern Camera* camera; Camera* camera; template static void scene_main(Renderer& renderer, const Matrix4& final_transform_mat, UpdateFrameFn update_frame) { bool cont = true; Scene scene{ {90.f * PI / 180.f, {{0.f, 1.8f, 7.f}, Quaternion::one(), {1.f, 1.f, 1.f}}}, { #if GAME == GAME_PLANE { Mesh::plane(2.f, 2.f), { Vector3(0.f, 0.f, 0.f), Quaternion::one(), Vector3(1.f, 1.f, 1.f), } }, #elif GAME == GAME_SUZANNE { engine::parse_object(DATADIR "/assets/suzanne.obj"), { Vector3(0.f, 0.f, 0.f), Quaternion::one(), Vector3(1.f, 1.f, 1.f), } }, #elif GAME == GAME_PHYSICS { Mesh::plane(10.f, 10.f), { Vector3(0.f, 0.f, 0.f), Quaternion::one(), Vector3(1.f, 1.f, 1.f), } }, { engine::parse_object(DATADIR "/assets/suzanne.obj"), { Vector3(0.f, 1.f, 0.f), Quaternion::one(), Vector3(1.f, 1.f, 1.f), } }, #endif } }; float rx = 0.f, ry = 0.f; Keyboard kb{[&](KeyboardKey key) { (void) key; }, [&](KeyboardKey key) { (void) key; }}; Mouse mouse{[&](Vector2 rel) { rx += -rel.y; ry += -rel.x; if (rx < -PI / 2.f) rx = -PI / 2.f; if (rx > PI / 2.f) rx = PI / 2.f; scene.camera.transform.rot = Quaternion::euler_zxy(rx, ry, 0.f); }}; camera = &scene.camera; while (cont) { renderer.clear(); auto pre_final_mat = final_transform_mat * scene.camera.to_mat4(static_cast(renderer.width()) / static_cast(renderer.height()), .5f, 12.f); for (const auto& obj : scene.objs) { auto obj_mat = obj.transform.to_mat4(); auto final_mat = pre_final_mat * obj_mat; const auto& mesh = obj.mesh; std::vector vertices; std::vector vertices_data; for (const auto& vertex : mesh.vertices) { vertices.push_back(final_mat * vertex); vertices_data.push_back(VertexData((obj_mat * vertex).xyz())); } for (const auto& triangle_indices : mesh.indices) { [&](std::integer_sequence) { renderer.draw_triangle({{vertices[triangle_indices[j][0]], mesh.normals[triangle_indices[j][1]], vertices_data[triangle_indices[j][0]]}...}); }(std::make_integer_sequence()); } } cont = update_frame(scene, kb, mouse); Vector3 movement(0.f, 0.f, 0.f); if (kb.is_down(KeyboardKey::fw)) movement.z += -1.f; if (kb.is_down(KeyboardKey::key_left)) movement.x += -1.f; if (kb.is_down(KeyboardKey::bw)) movement.z += +1.f; if (kb.is_down(KeyboardKey::key_right)) movement.x += +1.f; if (kb.is_down(KeyboardKey::fw) || kb.is_down(KeyboardKey::key_left) || kb.is_down(KeyboardKey::bw) || kb.is_down(KeyboardKey::key_right)) movement.normalize(); scene.camera.transform.loc += movement.rot(Quaternion::rot_y(ry)) * .05f; scene.camera.fov = (kb.is_down(KeyboardKey::zoom) ? 40.f : 80.f) * PI / 180.f; } } #ifdef HAVE_NCURSES #define MKEY_ESC 27 static int main_term() { // init std::setlocale(LC_ALL, ""); initscr(); cbreak(); noecho(); intrflush(stdscr, FALSE); keypad(stdscr, TRUE); set_escdelay(0); curs_set(0); int w, h; getmaxyx(stdscr, h, w); Renderer renderer{CharacterFrameBuffer{static_cast(w), static_cast(h)}}; scene_main(renderer, Matrix4::scale(Vector3(2.f, 1.f, 1.f)), [&](Scene& scene, auto& kb, auto& mouse) { (void) scene; mvaddnstr(0, 0, renderer.fb.chars(), renderer.width() * renderer.height()); bool cont = true; std::optional key; std::optional rel; //timeout(1000 / FPS); timeout(10); int c = getch(); switch (c) { case 'z': key = KeyboardKey::fw; break; case 'q': key = KeyboardKey::key_left; break; case 's': key = KeyboardKey::bw; break; case 'd': key = KeyboardKey::key_right; break; case 'p': key = KeyboardKey::zoom; break; case KEY_UP: rel = Vector2(0.f, -.1f); break; case KEY_LEFT: rel = Vector2(-.1f, 0.f); break; case KEY_DOWN: rel = Vector2(0.f, +.1f); break; case KEY_RIGHT: rel = Vector2(+.1f, 0.f); break; case MKEY_ESC: return false; } if (key && *key == KeyboardKey::fw) { if (!kb.is_down(KeyboardKey::fw)) kb.key_down_event(KeyboardKey::fw); } else { if (kb.is_down(KeyboardKey::fw)) kb.key_up_event(KeyboardKey::fw); } if (key && *key == KeyboardKey::key_left) { if (!kb.is_down(KeyboardKey::key_left)) kb.key_down_event(KeyboardKey::key_left); } else { if (kb.is_down(KeyboardKey::key_left)) kb.key_up_event(KeyboardKey::key_left); } if (key && *key == KeyboardKey::bw) { if (!kb.is_down(KeyboardKey::bw)) kb.key_down_event(KeyboardKey::bw); } else { if (kb.is_down(KeyboardKey::bw)) kb.key_up_event(KeyboardKey::bw); } if (key && *key == KeyboardKey::key_right) { if (!kb.is_down(KeyboardKey::key_right)) kb.key_down_event(KeyboardKey::key_right); } else { if (kb.is_down(KeyboardKey::key_right)) kb.key_up_event(KeyboardKey::key_right); } if (key && *key == KeyboardKey::zoom) { if (kb.is_down(KeyboardKey::zoom)) kb.key_up_event(KeyboardKey::zoom); else kb.key_down_event(KeyboardKey::zoom); } if (rel) mouse.mouse_motion_event(*rel); getmaxyx(stdscr, h, w); renderer.resize(static_cast(w), static_cast(h)); return cont; } ); // terminate endwin(); return EXIT_SUCCESS; } #endif #define SCREEN_WIDTH 800 #define SCREEN_HEIGHT 600 static const std::vector validation_layers = { "VK_LAYER_KHRONOS_validation", }; static const std::vector physical_device_ext_names = { VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_KHR_SPIRV_1_4_EXTENSION_NAME, VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME, }; #ifdef NDEBUG constexpr bool enable_validation_layers = false; #else constexpr bool enable_validation_layers = true; #endif constexpr size_t max_frames_in_flight = 2; constexpr bool show_fps = false; struct PhysicalDeviceEntry { uint32_t idx; VkPhysicalDevice physical_device; uint32_t graphics_queue_family_index, present_queue_family_index; VkPhysicalDeviceProperties2 props; VkPhysicalDeviceFeatures2 features; VkFormat depth_format; }; static bool check_validation_layer_support() { auto avail_layers = [&]() { uint32_t avail_layers_count; if (VkResult res = vkEnumerateInstanceLayerProperties(&avail_layers_count, nullptr); res != VK_SUCCESS) { std::cerr << "failed to enumerate instance layer properties, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } std::vector avail_layers(avail_layers_count); if (VkResult res = vkEnumerateInstanceLayerProperties(&avail_layers_count, avail_layers.data()); res != VK_SUCCESS) { std::cerr << "failed to enumerate instance layer properties, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return avail_layers; }(); for (const char* layer_name : validation_layers) { if (std::ranges::find_if(avail_layers, [&](const auto& props) { return strcmp(layer_name, props.layerName) == 0; }) == avail_layers.end()) return false; } return true; } static std::string severity_to_str(VkDebugUtilsMessageSeverityFlagBitsEXT severity) { switch (severity) { case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT: return "VERBOSE"; case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: return "INFO"; case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: return "WARNING"; case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: return "ERROR"; default: std::unreachable(); } } static VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback( VkDebugUtilsMessageSeverityFlagBitsEXT message_severity, VkDebugUtilsMessageTypeFlagsEXT /* message_type */, const VkDebugUtilsMessengerCallbackDataEXT* callback_data, void* /* user_data */) { std::cerr << "validation layer: [" << severity_to_str(message_severity) << "] " << callback_data->pMessage << std::endl; return VK_FALSE; } static void populate_msger_ci(VkDebugUtilsMessengerCreateInfoEXT& msger_ci) { msger_ci.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; msger_ci.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; msger_ci.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; msger_ci.pfnUserCallback = debug_callback; msger_ci.pUserData = nullptr; } static std::tuple, std::optional> find_queue_family_indices( VkPhysicalDevice physical_device, const std::vector& queue_family_properties, VkSurfaceKHR surface) { std::optional graphics_queue_family_index, present_queue_family_index; for (uint32_t i = 0; i < queue_family_properties.size(); i++) { const auto& prop = queue_family_properties[i]; bool is_graphics_queue = (prop.queueFamilyProperties.queueFlags & VK_QUEUE_GRAPHICS_BIT) != static_cast(0); VkBool32 is_presentation_queue; if (VkResult res = vkGetPhysicalDeviceSurfaceSupportKHR(physical_device, i, surface, &is_presentation_queue); res != VK_SUCCESS) { std::cerr << "failed to check if queue family supports surface, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } if (is_graphics_queue && is_presentation_queue) { graphics_queue_family_index = i; present_queue_family_index = i; break; } else if (is_graphics_queue && !graphics_queue_family_index) { graphics_queue_family_index = i; } else if (is_presentation_queue && !present_queue_family_index) { present_queue_family_index = i; } } return { graphics_queue_family_index, present_queue_family_index }; } static std::optional find_format(VkPhysicalDevice physical_device, const std::vector& formats, VkImageTiling tiling, VkFormatFeatureFlags flags) { for (const auto& format : formats) { VkFormatProperties2 format_props {}; format_props.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2; format_props.pNext = nullptr; vkGetPhysicalDeviceFormatProperties2(physical_device, format, &format_props); switch (tiling) { case VK_IMAGE_TILING_OPTIMAL: if ((format_props.formatProperties.optimalTilingFeatures & flags) == flags) return format; break; case VK_IMAGE_TILING_LINEAR: if ((format_props.formatProperties.linearTilingFeatures & flags) == flags) return format; break; default: std::cerr << "tiling not supported: " << string_VkImageTiling(tiling) << std::endl; std::exit(EXIT_FAILURE); } } return {}; } static bool has_stencil(VkFormat format) { switch (format) { case VK_FORMAT_D16_UNORM_S8_UINT: case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_D32_SFLOAT_S8_UINT: return true; default: return false; } } static uint32_t find_mem_type(VkPhysicalDevice physical_device, uint32_t type_filter, VkMemoryPropertyFlags flags) { VkPhysicalDeviceMemoryProperties2 props {}; props.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2; vkGetPhysicalDeviceMemoryProperties2(physical_device, &props); for (uint32_t i = 0; i < props.memoryProperties.memoryTypeCount; i++) if (type_filter & (1 << i) && (props.memoryProperties.memoryTypes[i].propertyFlags & flags) == flags) return i; // not found // TODO: improve by being explicit about what are requirements std::cerr << "cannot find memory type matching requirements" << std::endl; std::exit(EXIT_FAILURE); } static std::tuple create_buf(VkPhysicalDevice physical_device, VkDevice device, VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags mem_flags) { auto buf = [&]() { VkBufferCreateInfo buf_ci { .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .pNext = nullptr, .flags = {}, .size = size, .usage = usage, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, .queueFamilyIndexCount = {}, .pQueueFamilyIndices = {}, }; VkBuffer buf; if (VkResult res = vkCreateBuffer(device, &buf_ci, nullptr, &buf); res != VK_SUCCESS) { std::cerr << "failed to create buffer, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return buf; }(); VkMemoryRequirements buf_mem_requirements; vkGetBufferMemoryRequirements(device, buf, &buf_mem_requirements); auto buf_device_mem = [&]() { VkMemoryAllocateInfo buf_mem_ai { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .pNext = nullptr, .allocationSize = buf_mem_requirements.size, .memoryTypeIndex = find_mem_type(physical_device, buf_mem_requirements.memoryTypeBits, mem_flags), }; VkDeviceMemory buf_device_mem; if (VkResult res = vkAllocateMemory(device, &buf_mem_ai, nullptr, &buf_device_mem); res != VK_SUCCESS) { std::cerr << "failed to allocate buffer memory, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return buf_device_mem; }(); if (VkResult res = vkBindBufferMemory(device, buf, buf_device_mem, 0); res != VK_SUCCESS) { std::cerr << "failed to bind buffer memory, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return { buf, buf_device_mem }; } static VkCommandBuffer begin_single_time_cmds(VkDevice device, VkCommandPool cmd_pool) { auto cmd_buf = [&]() { VkCommandBufferAllocateInfo cmd_buf_ai { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .pNext = nullptr, .commandPool = cmd_pool, .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = 1, }; VkCommandBuffer cmd_buf; if (VkResult res = vkAllocateCommandBuffers(device, &cmd_buf_ai, &cmd_buf); res != VK_SUCCESS) { std::cerr << "failed to allocate command buffer, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return cmd_buf; }(); { VkCommandBufferBeginInfo cmd_buf_bi { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, .pNext = nullptr, .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, .pInheritanceInfo = {}, }; if (VkResult res = vkBeginCommandBuffer(cmd_buf, &cmd_buf_bi); res != VK_SUCCESS) { std::cerr << "failed to begin command buffer, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } } return cmd_buf; } static void end_single_time_cmds(VkQueue queue, VkDevice device, VkCommandPool cmd_pool, VkCommandBuffer cmd_buf) { if (VkResult res = vkEndCommandBuffer(cmd_buf); res != VK_SUCCESS) { std::cerr << "failed to end command buffer, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } { VkSubmitInfo submit_info { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .pNext = nullptr, .waitSemaphoreCount = {}, .pWaitSemaphores = {}, .pWaitDstStageMask = {}, .commandBufferCount = 1, .pCommandBuffers = &cmd_buf, .signalSemaphoreCount = {}, .pSignalSemaphores = {}, }; // TODO: should probably submit every command buffers in a single function call, instead of // submitting and waiting for them to finish separatly if (VkResult res = vkQueueSubmit(queue, 1, &submit_info, nullptr); res != VK_SUCCESS) { std::cerr << "failed to submit command buffer to queue, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } } if (VkResult res = vkQueueWaitIdle(queue); res != VK_SUCCESS) { std::cerr << "failed to wait idle for graphics queue, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } vkFreeCommandBuffers(device, cmd_pool, 1, &cmd_buf); } static void copy_buf_to_buf(VkCommandBuffer cmd_buf, VkBuffer src, VkBuffer dst, VkDeviceSize size) { VkBufferCopy buf_copy { .srcOffset = 0, .dstOffset = 0, .size = size, }; vkCmdCopyBuffer(cmd_buf, src, dst, 1, &buf_copy); } static void copy_buf_to_img(VkCommandBuffer cmd_buf, VkBuffer src, VkImage dst, uint32_t width, uint32_t height) { VkBufferImageCopy buf_img_copy { .bufferOffset = 0, .bufferRowLength = 0, .bufferImageHeight = 0, .imageSubresource = { // VkImageSubresourceLayers .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .mipLevel = 0, .baseArrayLayer = 0, .layerCount = 1, }, .imageOffset = { .x = 0, .y = 0, .z = 0 }, .imageExtent = { .width = width, .height = height, .depth = 1 }, }; vkCmdCopyBufferToImage(cmd_buf, src, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &buf_img_copy); } static std::tuple create_img(VkPhysicalDevice physical_device, VkDevice device, uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags mem_flags) { auto texture_img = [&]() { VkImageCreateInfo img_ci { .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .pNext = nullptr, .flags = {}, .imageType = VK_IMAGE_TYPE_2D, // TODO: check if this format is supported .format = format, .extent = { .width = width, .height = height, .depth = 1 }, .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = tiling, .usage = usage, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, .queueFamilyIndexCount = {}, .pQueueFamilyIndices = {}, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, }; VkImage texture_img; if (VkResult res = vkCreateImage(device, &img_ci, nullptr, &texture_img); res != VK_SUCCESS) { std::cerr << "failed to create image, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return texture_img; }(); auto texture_img_device_mem = [&]() { VkMemoryRequirements mem_requirements; vkGetImageMemoryRequirements(device, texture_img, &mem_requirements); VkMemoryAllocateInfo mem_ai { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .pNext = nullptr, .allocationSize = mem_requirements.size, .memoryTypeIndex = find_mem_type(physical_device, mem_requirements.memoryTypeBits, mem_flags), }; VkDeviceMemory texture_img_device_mem; if (VkResult res = vkAllocateMemory(device, &mem_ai, nullptr, &texture_img_device_mem); res != VK_SUCCESS) { std::cerr << "failed to allocate texture image memory, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return texture_img_device_mem; }(); if (VkResult res = vkBindImageMemory(device, texture_img, texture_img_device_mem, 0); res != VK_SUCCESS) { std::cerr << "failed to bind texture image to memory, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return std::tuple { texture_img, texture_img_device_mem }; } static std::tuple create_depth_resources(VkPhysicalDevice physical_device, VkDevice device, VkExtent2D swapchain_extent, VkFormat depth_format) { auto [depth_img, depth_img_device_mem] = create_img(physical_device, device, swapchain_extent.width, swapchain_extent.height, depth_format, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); auto depth_img_view = [&]() { VkImageViewCreateInfo img_view_ci { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .pNext = {}, .flags = {}, .image = depth_img, .viewType = VK_IMAGE_VIEW_TYPE_2D, .format = depth_format, .components = {}, .subresourceRange = { // VkImageSubresourceRange .aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1, }, }; VkImageView depth_img_view; if (VkResult res = vkCreateImageView(device, &img_view_ci, nullptr, &depth_img_view); res != VK_SUCCESS) { std::cerr << "failed to create depth image view, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return depth_img_view; }(); return std::tuple { depth_img, depth_img_device_mem, depth_img_view }; } static void destroy_depth_resources(VkDevice device, VkImage depth_img, VkDeviceMemory depth_img_device_mem, VkImageView depth_img_view) { vkDestroyImageView(device, depth_img_view, nullptr); vkFreeMemory(device, depth_img_device_mem, nullptr); vkDestroyImage(device, depth_img, nullptr); } static void transition_image_layout(VkCommandBuffer cmd_buf, VkImage img, VkImageLayout old_layout, VkImageLayout new_layout, VkAccessFlags2 src_access_mask, VkAccessFlags2 dst_access_mask, VkPipelineStageFlags2 src_stage_mask, VkPipelineStageFlags2 dst_stage_mask) { VkImageMemoryBarrier2 img_mem_barrier { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .pNext = nullptr, .srcStageMask = src_stage_mask, .srcAccessMask = src_access_mask, .dstStageMask = dst_stage_mask, .dstAccessMask = dst_access_mask, .oldLayout = old_layout, .newLayout = new_layout, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = img, .subresourceRange = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1, }, }; VkDependencyInfo dependency_info { .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .pNext = nullptr, .dependencyFlags = {}, .memoryBarrierCount = 0, .pMemoryBarriers = {}, .bufferMemoryBarrierCount = 0, .pBufferMemoryBarriers = {}, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &img_mem_barrier, }; vkCmdPipelineBarrier2(cmd_buf, &dependency_info); } static int main_graphical() { // init window glfwInit(); glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); bool fb_resized = false; GLFWwindow* window = glfwCreateWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "Engine", nullptr, nullptr); // TODO: might have to improve, because if in the future we need more values in a glfw callback, // this approch wouldn't work glfwSetWindowUserPointer(window, &fb_resized); glfwSetFramebufferSizeCallback(window, [](GLFWwindow* window, int /* width */, int /* height */) { *reinterpret_cast(glfwGetWindowUserPointer(window)) = true; }); // TODO: verify alignment requirements // TODO: maybe move these definitions? const std::array vertices { engine::vk::Vertex { { -.5f, -.5f, 0.f }, { 1.f, 0.f, 0.f }, { 1.f, 0.f } }, engine::vk::Vertex { { .5f, -.5f, 0.f }, { 0.f, 1.f, 0.f }, { 0.f, 0.f } }, engine::vk::Vertex { { .5f, .5f, 0.f }, { 0.f, 0.f, 1.f }, { 0.f, 1.f } }, engine::vk::Vertex { { -.5f, .5f, 0.f }, { 1.f, 1.f, 1.f }, { 1.f, 1.f } }, engine::vk::Vertex { { -.5f, -.5f, -.5f }, { 1.f, 0.f, 0.f }, { 1.f, 0.f } }, engine::vk::Vertex { { .5f, -.5f, -.5f }, { 0.f, 1.f, 0.f }, { 0.f, 0.f } }, engine::vk::Vertex { { .5f, .5f, -.5f }, { 0.f, 0.f, 1.f }, { 0.f, 1.f } }, engine::vk::Vertex { { -.5f, .5f, -.5f }, { 1.f, 1.f, 1.f }, { 1.f, 1.f } }, }; const std::array indices { 0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4, }; // init Vulkan std::cout << "Vulkan loader version: " << engine::vk::api { VK_HEADER_VERSION_COMPLETE } << std::endl; // init Vulkan - create instance if (enable_validation_layers && !check_validation_layer_support()) { std::cerr << "validation layers requested, but not available!" << std::endl; std::exit(EXIT_FAILURE); } auto instance = [&]() { VkApplicationInfo app_info { .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pNext = nullptr, .pApplicationName = "engine - test", .applicationVersion = VK_MAKE_VERSION(1, 0, 0), .pEngineName = "engine", .engineVersion = VK_MAKE_VERSION(1, 0, 0), .apiVersion = VK_API_VERSION_1_4, }; auto instance_exts = [&]() { std::vector instance_exts; { uint32_t glfw_extension_count; const char** glfw_extensions = glfwGetRequiredInstanceExtensions(&glfw_extension_count); instance_exts.insert(instance_exts.end(), glfw_extensions, glfw_extensions + glfw_extension_count); } if (enable_validation_layers) instance_exts.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); return instance_exts; }(); auto avail_instance_exts = [&]() { uint32_t avail_instance_exts_count; if (VkResult res = vkEnumerateInstanceExtensionProperties(nullptr, &avail_instance_exts_count, nullptr); res != VK_SUCCESS) { std::cerr << "failed to enumerate instance extension properties, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } std::vector avail_instance_exts(avail_instance_exts_count); if (VkResult res = vkEnumerateInstanceExtensionProperties(nullptr, &avail_instance_exts_count, avail_instance_exts.data()); res != VK_SUCCESS) { std::cerr << "failed to enumerate instance extension properties, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return avail_instance_exts; }(); std::cout << "required instance extensions:\n"; for (const auto& extension_name : instance_exts) { std::cout << (std::ranges::find_if(avail_instance_exts, [&](const auto& avail_ext) { return strcmp(avail_ext.extensionName, extension_name) == 0; }) == avail_instance_exts.end() ? "!" : " "); std::cout << " " << extension_name << "\n"; } VkInstanceCreateInfo instance_ci { .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pNext = nullptr, .flags = {}, .pApplicationInfo = &app_info, .enabledLayerCount = {}, .ppEnabledLayerNames = {}, .enabledExtensionCount = static_cast(instance_exts.size()), .ppEnabledExtensionNames = instance_exts.data(), }; VkDebugUtilsMessengerCreateInfoEXT inst_msger_ci{}; if (enable_validation_layers) { populate_msger_ci(inst_msger_ci); instance_ci.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &inst_msger_ci; instance_ci.enabledLayerCount = static_cast(validation_layers.size()); instance_ci.ppEnabledLayerNames = validation_layers.data(); } VkInstance instance; if (VkResult res = vkCreateInstance(&instance_ci, nullptr, &instance); res != VK_SUCCESS) { std::cerr << "failed to create instance, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return instance; }(); VkDebugUtilsMessengerEXT debug_messenger; if (enable_validation_layers) { VkDebugUtilsMessengerCreateInfoEXT msger_ci{}; populate_msger_ci(msger_ci); auto create_debug_messenger = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"); if (!create_debug_messenger) { std::cerr << "failed to set up debug messenger!" << std::endl; std::exit(EXIT_FAILURE); } create_debug_messenger(instance, &msger_ci, nullptr, &debug_messenger); } // create window surface auto surface = [&]() { VkSurfaceKHR surface; if (VkResult res = glfwCreateWindowSurface(instance, window, nullptr, &surface); res != VK_SUCCESS) { std::cerr << "failed to create window surface, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return surface; }(); // select physical device and queues auto [physical_device, graphics_queue_family_index, present_queue_family_index, physical_device_props, physical_device_features, depth_format] = [&]() { std::multimap physical_devices; auto avail_physical_devices = [&]() { uint32_t avail_physical_devices_count; if (VkResult res = vkEnumeratePhysicalDevices(instance, &avail_physical_devices_count, nullptr); res != VK_SUCCESS) { std::cerr << "failed to enumerate physical devices, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } std::vector avail_physical_devices(avail_physical_devices_count); if (VkResult res = vkEnumeratePhysicalDevices(instance, &avail_physical_devices_count, avail_physical_devices.data()); res != VK_SUCCESS) { std::cerr << "failed to enumerate physical devices, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return avail_physical_devices; }(); if (avail_physical_devices.empty()) { std::cerr << "failed to find physical devices with Vulkan support" << std::endl; std::exit(EXIT_FAILURE); } std::cout << "devices:" << std::endl; for (uint32_t i = 0; i < avail_physical_devices.size(); i++) { const auto& avail_physical_device = avail_physical_devices[i]; auto physical_device_props = [&]() { VkPhysicalDeviceProperties2 physical_device_props{}; physical_device_props.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; vkGetPhysicalDeviceProperties2(avail_physical_device, &physical_device_props); return physical_device_props; }(); std::cout << " " << (i + 1) << ". " << physical_device_props.properties.deviceName << ":" << std::endl; std::cout << " apiVersion: " << engine::vk::api { physical_device_props.properties.apiVersion } << std::endl; auto physical_device_features = [&]() { VkPhysicalDeviceFeatures2 physical_device_features{}; physical_device_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; vkGetPhysicalDeviceFeatures2(avail_physical_device, &physical_device_features); return physical_device_features; }(); auto physical_device_ext_props = [&]() { uint32_t physical_device_ext_props_count; if (VkResult res = vkEnumerateDeviceExtensionProperties(avail_physical_device, nullptr, &physical_device_ext_props_count, nullptr); res != VK_SUCCESS) { std::cerr << "failed to enumerate physical device extension properties, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } std::vector physical_device_ext_props(physical_device_ext_props_count); if (VkResult res = vkEnumerateDeviceExtensionProperties(avail_physical_device, nullptr, &physical_device_ext_props_count, physical_device_ext_props.data()); res != VK_SUCCESS) { std::cerr << "failed to enumerate physical device extension properties, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return physical_device_ext_props; }(); std::cout << " required physical device extensions:" << std::endl; for (const auto& ext : physical_device_ext_names) { std::cout << " " << (std::ranges::find_if(physical_device_ext_props, [&](const auto& avail_ext) { return strcmp(avail_ext.extensionName, ext) == 0; }) == physical_device_ext_props.end() ? "!" : " "); std::cout << " " << ext << std::endl; } auto queue_family_properties = [&]() { uint32_t queue_family_properties_count; vkGetPhysicalDeviceQueueFamilyProperties2(avail_physical_device, &queue_family_properties_count, nullptr); std::vector queue_family_properties(queue_family_properties_count); for (auto& elt : queue_family_properties) elt.sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2; vkGetPhysicalDeviceQueueFamilyProperties2(avail_physical_device, &queue_family_properties_count, queue_family_properties.data()); return queue_family_properties; }(); auto [graphics_queue_family_index, present_queue_family_index] = find_queue_family_indices(avail_physical_device, queue_family_properties, surface); std::cout << " graphics queue family index: "; if (graphics_queue_family_index) std::cout << *graphics_queue_family_index; else std::cout << "none"; std::cout << std::endl; std::cout << " presentation queue family index: "; if (present_queue_family_index) std::cout << *present_queue_family_index; else std::cout << "none"; std::cout << std::endl; auto depth_format = find_format(avail_physical_device, { VK_FORMAT_D32_SFLOAT, VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D16_UNORM_S8_UINT }, VK_IMAGE_TILING_OPTIMAL, VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT); std::cout << " depth format: "; if (depth_format) std::cout << string_VkFormat(*depth_format); else std::cout << "none"; std::cout << std::endl; auto score = [&]() { if (VK_API_VERSION_VARIANT(physical_device_props.properties.apiVersion) != 0 || physical_device_props.properties.apiVersion < VK_API_VERSION_1_4) return std::optional {}; if (!graphics_queue_family_index || !present_queue_family_index) return std::optional {}; if (std::ranges::find_if_not(physical_device_ext_names, [&](const auto& device_ext) { return std::ranges::find_if(physical_device_ext_props, [&](const auto& ext_prop) { return strcmp(ext_prop.extensionName, device_ext) == 0; }) != physical_device_ext_props.end(); }) != physical_device_ext_names.end()) return std::optional {}; if (!physical_device_features.features.samplerAnisotropy) return std::optional {}; if (!depth_format) return std::optional {}; // TODO: lots of checks are probably missing, like checking if the swapchain is // supported (I'm not sure if it's needed) unsigned score = 0; if (*graphics_queue_family_index == *present_queue_family_index) score += 5; score += [&](VkPhysicalDeviceType device_type) { switch (device_type) { case VK_PHYSICAL_DEVICE_TYPE_OTHER: return 0; case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 10; case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 5; case VK_PHYSICAL_DEVICE_TYPE_CPU: return 1; default: { std::cerr << "device type not supported: " << string_VkPhysicalDeviceType(device_type) << std::endl; std::exit(EXIT_FAILURE); } } }(physical_device_props.properties.deviceType); return std::optional { score }; }(); std::cout << " score: "; if (score) std::cout << *score; else std::cout << "not suitable"; std::cout << std::endl; if (score) physical_devices.insert({ *score, { .idx = i, .physical_device = avail_physical_device, .graphics_queue_family_index = *graphics_queue_family_index, .present_queue_family_index = *present_queue_family_index, .props = physical_device_props, .features = physical_device_features, .depth_format = *depth_format, } }); } std::cout << std::endl; if (physical_devices.empty()) { std::cerr << "no suitable physical device found" << std::endl; std::exit(EXIT_FAILURE); } auto best = physical_devices.crbegin(); std::cout << "picking: " << (best->second.idx + 1) << ". " << best->second.props.properties.deviceName << " (score: " << best->first << ")" << std::endl; return std::tuple { best->second.physical_device, best->second.graphics_queue_family_index, best->second.present_queue_family_index, best->second.props, best->second.features, best->second.depth_format }; }(); auto device = [&]() { // TODO: really weird way of making a single structure if // graphics_queue_family_index == present_queue_family_index // here, in this cas, we create both *CreateInfo, and then tell VkDeviceCreateInfo that // there is only a single element std::array queue_priorities { .5f, .5f }; std::array device_queue_cis { VkDeviceQueueCreateInfo { .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, .pNext = nullptr, .flags = {}, .queueFamilyIndex = graphics_queue_family_index, .queueCount = 1, .pQueuePriorities = &queue_priorities[0], }, VkDeviceQueueCreateInfo { .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, .pNext = nullptr, .flags = {}, .queueFamilyIndex = present_queue_family_index, .queueCount = 1, .pQueuePriorities = &queue_priorities[1], }, }; VkPhysicalDeviceExtendedDynamicStateFeaturesEXT device_eds_features { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT, .pNext = {}, .extendedDynamicState = VK_TRUE, }; VkPhysicalDeviceVulkan13Features device_vk13_features { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, .pNext = &device_eds_features, .robustImageAccess = {}, .inlineUniformBlock = {}, .descriptorBindingInlineUniformBlockUpdateAfterBind = {}, .pipelineCreationCacheControl = {}, .privateData = {}, .shaderDemoteToHelperInvocation = {}, .shaderTerminateInvocation = {}, .subgroupSizeControl = {}, .computeFullSubgroups = {}, .synchronization2 = VK_TRUE, .textureCompressionASTC_HDR = {}, .shaderZeroInitializeWorkgroupMemory = {}, .dynamicRendering = VK_TRUE, .shaderIntegerDotProduct = {}, .maintenance4 = {}, }; // TODO: name is confusing. We can't call it physical_device_features because the name is // already taken by features queried while selecting the physical device. But calling it // device_features makes a bit of sense, because theses are features requested for (logical) // device creation VkPhysicalDeviceFeatures2 device_features { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &device_vk13_features, .features = {}, // default to VK_FALSE for everything }; device_features.features.samplerAnisotropy = VK_TRUE; VkDeviceCreateInfo device_ci { .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, .pNext = &device_features, .flags = {}, .queueCreateInfoCount = (graphics_queue_family_index == present_queue_family_index ? static_cast(1) : static_cast(2)), .pQueueCreateInfos = device_queue_cis.data(), .enabledLayerCount = {}, .ppEnabledLayerNames = {}, .enabledExtensionCount = static_cast(physical_device_ext_names.size()), .ppEnabledExtensionNames = physical_device_ext_names.data(), .pEnabledFeatures = {}, }; VkDevice device; if (VkResult res = vkCreateDevice(physical_device, &device_ci, nullptr, &device); res != VK_SUCCESS) { std::cerr << "failed to create device: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return device; }(); auto [graphics_queue, present_queue] = [&]() { const auto map_family_to_queue = [&](uint32_t queue_family_index) { VkQueue queue; vkGetDeviceQueue(device, queue_family_index, 0, &queue); return queue; }; return std::tuple { map_family_to_queue(graphics_queue_family_index), map_family_to_queue(present_queue_family_index), }; }(); // create swap chain // TODO: when we recreate the swapchain, I don't know if the number of images in it can change // between the old and new swapchain. Right now we assume that it doesn't, which might cause // problem (but I'm not sure) // TODO: pass the old swapchain to VkSwapchainCreateInfoKHR so the new swapchain can be created // while the old one is in-flight // TODO: with the same intention of recreating the swapchain in-flight, we should also do // something about the depth image. If we recreate the swapchain in-flight without worrying // about the depth image, we might destroy it while it's being used. But I'm not sure, we have // to test it VkExtent2D swapchain_extent; VkSurfaceFormatKHR surface_format; VkSwapchainKHR swapchain = nullptr; std::vector swapchain_imgs; std::vector swapchain_img_views; VkImage depth_img; VkDeviceMemory depth_img_device_mem; VkImageView depth_img_view; const auto destroy_swapchain = [&]() { for (auto it_img_view = swapchain_img_views.rbegin(); it_img_view != swapchain_img_views.rend(); ++it_img_view) vkDestroyImageView(device, *it_img_view, nullptr); vkDestroySwapchainKHR(device, swapchain, nullptr); }; const auto recreate_swapchain = [&]() { bool is_first_swapchain = (swapchain == nullptr); if (!is_first_swapchain) { if (VkResult res = vkDeviceWaitIdle(device); res != VK_SUCCESS) { std::cerr << "failed to wait idle for device, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } destroy_depth_resources(device, depth_img, depth_img_device_mem, depth_img_view); destroy_swapchain(); } // TODO: should probably use version 2 of theses functions, but glfwCreateWindowSurface // return version 1, so for now we will use version 1 auto surface_capabilities = [&]() { VkSurfaceCapabilitiesKHR surface_capabilities; if (VkResult res = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, surface, &surface_capabilities); res != VK_SUCCESS) { std::cerr << "failed to get physical device surface capabilities, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return surface_capabilities; }(); swapchain_extent = [&]() { if (surface_capabilities.currentExtent.width != std::numeric_limits::max()) return surface_capabilities.currentExtent; int width, height; glfwGetFramebufferSize(window, &width, &height); // TODO: improve minimization handling. I'm on wayland so it's impossible to know if a // window is minimized, therefore I can't really test it properly right now while (width == 0 || height == 0) { glfwGetFramebufferSize(window, &width, &height); glfwWaitEvents(); } return VkExtent2D{ .width = std::clamp(static_cast(width), surface_capabilities.minImageExtent.width, surface_capabilities.maxImageExtent.width), .height = std::clamp(static_cast(height), surface_capabilities.minImageExtent.height, surface_capabilities.maxImageExtent.height), }; }(); surface_format = [&]() { auto surface_formats = [&]() { uint32_t surface_formats_count; if (VkResult res = vkGetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface, &surface_formats_count, nullptr); res != VK_SUCCESS) { std::cerr << "failed to get physical device surface formats, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } std::vector surface_formats(surface_formats_count); if (VkResult res = vkGetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface, &surface_formats_count, surface_formats.data()); res != VK_SUCCESS) { std::cerr << "failed to get physical device surface formats, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return surface_formats; }(); for (const auto& surface_format : surface_formats) { if (surface_format.format == VK_FORMAT_B8G8R8A8_SRGB && surface_format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) return surface_format; } // not found std::cerr << "surface format not found (VK_FORMAT_B8G8R8_SRGB and VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)" << std::endl; std::exit(EXIT_FAILURE); }(); [&]() { auto present_mode = [&]() { auto present_modes = [&]() { uint32_t present_modes_count; if (VkResult res = vkGetPhysicalDeviceSurfacePresentModesKHR(physical_device, surface, &present_modes_count, nullptr); res != VK_SUCCESS) { std::cerr << "failed to get physical device present modes, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } std::vector present_modes(present_modes_count); if (VkResult res = vkGetPhysicalDeviceSurfacePresentModesKHR(physical_device, surface, &present_modes_count, present_modes.data()); res != VK_SUCCESS) { std::cerr << "failed to get physical device present modes, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return present_modes; }(); for (const auto& present_mode : present_modes) if (present_mode == VK_PRESENT_MODE_MAILBOX_KHR) return present_mode; return VK_PRESENT_MODE_FIFO_KHR; }(); // TODO: remove unnecessary static_cast, but at this moment I'm not sure where they // are necessary uint32_t min_image_count = std::max(static_cast(3), surface_capabilities.minImageCount + static_cast(1)); if (surface_capabilities.maxImageCount > 0 && surface_capabilities.maxImageCount < min_image_count) min_image_count = surface_capabilities.maxImageCount; // might not be used, but if we do, we have to keep it in memory until the call to vkCreateSwapchainKHR() std::array queue_family_indices; VkSwapchainCreateInfoKHR swapchain_ci { .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, .pNext = nullptr, .flags = {}, .surface = surface, .minImageCount = min_image_count, .imageFormat = surface_format.format, .imageColorSpace = surface_format.colorSpace, .imageExtent = swapchain_extent, .imageArrayLayers = 1, .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, .imageSharingMode = {}, .queueFamilyIndexCount = {}, .pQueueFamilyIndices = {}, .preTransform = surface_capabilities.currentTransform, .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, .presentMode = present_mode, .clipped = VK_TRUE, .oldSwapchain = {}, }; if (graphics_queue_family_index == present_queue_family_index) { swapchain_ci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; } else { // TODO: should use VK_SHARING_MODE_EXCLUSIVE too, and handle queue family ownership // while transitionning swapchain image layout swapchain_ci.imageSharingMode = VK_SHARING_MODE_CONCURRENT; queue_family_indices[0] = graphics_queue_family_index; queue_family_indices[1] = present_queue_family_index; swapchain_ci.queueFamilyIndexCount = 2; swapchain_ci.pQueueFamilyIndices = queue_family_indices.data(); } if (VkResult res = vkCreateSwapchainKHR(device, &swapchain_ci, nullptr, &swapchain); res != VK_SUCCESS) { std::cerr << "failed create swapchain, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } }(); [&]() { uint32_t swapchain_imgs_count; if (VkResult res = vkGetSwapchainImagesKHR(device, swapchain, &swapchain_imgs_count, nullptr); res != VK_SUCCESS) { std::cerr << "failed to get swapchain images, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } swapchain_imgs.resize(swapchain_imgs_count); if (VkResult res = vkGetSwapchainImagesKHR(device, swapchain, &swapchain_imgs_count, swapchain_imgs.data()); res != VK_SUCCESS) { std::cerr << "failed to get swapchain images, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } }(); [&]() { swapchain_img_views.resize(swapchain_imgs.size()); for (uint32_t i = 0; i < swapchain_imgs.size(); i++) { VkImageViewCreateInfo img_view_ci { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .pNext = {}, .flags = {}, .image = swapchain_imgs[i], .viewType = VK_IMAGE_VIEW_TYPE_2D, .format = surface_format.format, .components = {}, .subresourceRange = { // VkImageSubresourceRange .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1, }, }; if (VkResult res = vkCreateImageView(device, &img_view_ci, nullptr, &swapchain_img_views[i]); res != VK_SUCCESS) { std::cerr << "failed to create image view, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } } }(); if (!is_first_swapchain) std::tie(depth_img, depth_img_device_mem, depth_img_view) = create_depth_resources(physical_device, device, swapchain_extent, depth_format); }; recreate_swapchain(); // create descriptor set layout auto descriptor_set_layout = [&]() { std::array descriptor_set_layout_bindings { VkDescriptorSetLayoutBinding { .binding = 0, .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_VERTEX_BIT, .pImmutableSamplers = {}, }, VkDescriptorSetLayoutBinding { .binding = 1, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, .pImmutableSamplers = {}, }, }; VkDescriptorSetLayoutCreateInfo descriptor_set_layout_ci { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, .pNext = nullptr, .flags = {}, .bindingCount = descriptor_set_layout_bindings.size(), .pBindings = descriptor_set_layout_bindings.data(), }; VkDescriptorSetLayout descriptor_set_layout; if (VkResult res = vkCreateDescriptorSetLayout(device, &descriptor_set_layout_ci, nullptr, &descriptor_set_layout); res != VK_SUCCESS) { std::cerr << "failed to create descriptor set layout, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return descriptor_set_layout; }(); // create pipeline auto [pl_layout, graphics_pl] = [&]() { // reading shader file auto shader_module = [&]() { auto shader_code = [&]() { const char* shader_file_name = SHADERSDIR "/shader.spv"; std::ifstream shader_file(shader_file_name, std::ios::ate | std::ios::binary); if (!shader_file.is_open()) { std::cerr << "file `" << shader_file_name << "' not found" << std::endl; // TODO: improve std::exit(EXIT_SUCCESS); } // shader code has to be 32-bits aligned, which is the case with the default allocator std::vector shader_code(shader_file.tellg()); shader_file.seekg(0, std::ios::beg); shader_file.read(shader_code.data(), static_cast(shader_code.size())); return shader_code; }(); VkShaderModuleCreateInfo shader_module_ci { .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .pNext = nullptr, .flags = {}, .codeSize = shader_code.size(), .pCode = reinterpret_cast(shader_code.data()), }; VkShaderModule shader_module; if (VkResult res = vkCreateShaderModule(device, &shader_module_ci, nullptr, &shader_module); res != VK_SUCCESS) { std::cerr << "failed to create shader module, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return shader_module; }(); auto [pl_layout, graphics_pl] = [&]() { std::array pl_shader_stage_cis { VkPipelineShaderStageCreateInfo { .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .pNext = nullptr, .flags = {}, .stage = VK_SHADER_STAGE_VERTEX_BIT, .module = shader_module, .pName = "vert_main", .pSpecializationInfo = nullptr, }, VkPipelineShaderStageCreateInfo { .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .pNext = nullptr, .flags = {}, .stage = VK_SHADER_STAGE_FRAGMENT_BIT, .module = shader_module, .pName = "frag_main", .pSpecializationInfo = nullptr, }, }; std::array dynamic_states { VkDynamicState { VK_DYNAMIC_STATE_VIEWPORT }, VkDynamicState { VK_DYNAMIC_STATE_SCISSOR }, }; VkPipelineDynamicStateCreateInfo pl_dyn_state_ci { .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, .pNext = nullptr, .flags = {}, .dynamicStateCount = static_cast(dynamic_states.size()), .pDynamicStates = dynamic_states.data(), }; const auto vertex_binding_desc = engine::vk::Vertex::get_binding_desc(); const auto vertex_attr_descs = engine::vk::Vertex::get_attr_descs(); VkPipelineVertexInputStateCreateInfo pl_vert_in_state_ci { .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, .pNext = nullptr, .flags = {}, .vertexBindingDescriptionCount = 1, .pVertexBindingDescriptions = &vertex_binding_desc, .vertexAttributeDescriptionCount = static_cast(vertex_attr_descs.size()), .pVertexAttributeDescriptions = vertex_attr_descs.data(), }; VkPipelineInputAssemblyStateCreateInfo pl_in_asm_state_ci { .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, .pNext = nullptr, .flags = {}, .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, .primitiveRestartEnable = VK_FALSE, }; VkPipelineViewportStateCreateInfo pl_viewport_state_ci { .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, .pNext = nullptr, .flags = {}, .viewportCount = 1, .pViewports = {}, .scissorCount = 1, .pScissors = {}, }; VkPipelineRasterizationStateCreateInfo pl_raster_state_ci { .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, .pNext = nullptr, .flags = {}, .depthClampEnable = VK_FALSE, .rasterizerDiscardEnable = VK_FALSE, .polygonMode = VK_POLYGON_MODE_FILL, .cullMode = VK_CULL_MODE_BACK_BIT, .frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, .depthBiasEnable = VK_FALSE, .depthBiasConstantFactor = {}, .depthBiasClamp = {}, .depthBiasSlopeFactor = {}, .lineWidth = 1.f, }; VkPipelineMultisampleStateCreateInfo pl_ms_state_ci { .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, .pNext = nullptr, .flags = {}, .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT, .sampleShadingEnable = VK_FALSE, .minSampleShading = {}, .pSampleMask = {}, .alphaToCoverageEnable = VK_FALSE, .alphaToOneEnable = VK_FALSE, }; VkPipelineDepthStencilStateCreateInfo pl_depth_stencil_state_ci { .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, .pNext = nullptr, .flags = {}, .depthTestEnable = VK_TRUE, .depthWriteEnable = VK_TRUE, .depthCompareOp = VK_COMPARE_OP_LESS, .depthBoundsTestEnable = VK_FALSE, .stencilTestEnable = VK_FALSE, .front = {}, .back = {}, .minDepthBounds = {}, .maxDepthBounds = {}, }; VkPipelineColorBlendAttachmentState pl_col_blend_attachment_state { .blendEnable = VK_FALSE, .srcColorBlendFactor = {}, .dstColorBlendFactor = {}, .colorBlendOp = {}, .srcAlphaBlendFactor = {}, .dstAlphaBlendFactor = {}, .alphaBlendOp = {}, .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT, }; VkPipelineColorBlendStateCreateInfo pl_col_blend_state_ci { .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, .pNext = nullptr, .flags = {}, .logicOpEnable = VK_FALSE, .logicOp = {}, .attachmentCount = 1, .pAttachments = &pl_col_blend_attachment_state, .blendConstants = { 0.f, 0.f, 0.f, 0.f }, }; auto pl_layout = [&]() { VkPipelineLayoutCreateInfo pl_layout_ci { .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .pNext = nullptr, .flags = {}, .setLayoutCount = 1, .pSetLayouts = &descriptor_set_layout, .pushConstantRangeCount = 0, .pPushConstantRanges = {}, }; VkPipelineLayout pl_layout; if (VkResult res = vkCreatePipelineLayout(device, &pl_layout_ci, nullptr, &pl_layout); res != VK_SUCCESS) { std::cerr << "failed to create pipeline layout, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return pl_layout; }(); VkPipelineRenderingCreateInfo pl_render_ci { .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, .pNext = nullptr, .viewMask = {}, .colorAttachmentCount = 1, .pColorAttachmentFormats = &surface_format.format, .depthAttachmentFormat = depth_format, .stencilAttachmentFormat = {}, }; VkGraphicsPipelineCreateInfo graphics_pl_ci { .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .pNext = &pl_render_ci, .flags = {}, .stageCount = static_cast(pl_shader_stage_cis.size()), .pStages = pl_shader_stage_cis.data(), .pVertexInputState = &pl_vert_in_state_ci, .pInputAssemblyState = &pl_in_asm_state_ci, .pTessellationState = {}, .pViewportState = &pl_viewport_state_ci, .pRasterizationState = &pl_raster_state_ci, .pMultisampleState = &pl_ms_state_ci, .pDepthStencilState = &pl_depth_stencil_state_ci, .pColorBlendState = &pl_col_blend_state_ci, .pDynamicState = &pl_dyn_state_ci, .layout = pl_layout, .renderPass = nullptr, .subpass = {}, .basePipelineHandle = {}, .basePipelineIndex = {}, }; VkPipeline graphics_pl; if (VkResult res = vkCreateGraphicsPipelines(device, nullptr, 1, &graphics_pl_ci, nullptr, &graphics_pl); res != VK_SUCCESS) { std::cerr << "failed to pipeline, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return std::tuple { pl_layout, graphics_pl }; }(); vkDestroyShaderModule(device, shader_module, nullptr); return std::tuple { pl_layout, graphics_pl }; }(); // create command pool auto cmd_pool = [&]() { VkCommandPoolCreateInfo cmd_pool_ci { .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .pNext = nullptr, .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, .queueFamilyIndex = graphics_queue_family_index, }; VkCommandPool cmd_pool; if (VkResult res = vkCreateCommandPool(device, &cmd_pool_ci, nullptr, &cmd_pool); res != VK_SUCCESS) { std::cerr << "failed to create command pool, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return cmd_pool; }(); // create depth resources // TODO: check why we only need one image. From the looks of it, we need max_frames_in_flight // depth images, because there is at most max_frames_in_flight rendering at the same time std::tie(depth_img, depth_img_device_mem, depth_img_view) = create_depth_resources(physical_device, device, swapchain_extent, depth_format); // create texture image auto [texture_img, texture_img_device_mem] = [&]() { int w, h, channels; stbi_uc* pixels = stbi_load(DATADIR "/assets/textures/texture.jpg", &w, &h, &channels, STBI_rgb_alpha); // TODO: we're also using this size as the host pixels buffer size, I don't know if mixing // the two will cause problems later. Right now they are the same, so it doesn't VkDeviceSize img_size = w * h * 4; if (!pixels) { std::cerr << "failed to load texture image, reason: " << stbi_failure_reason() << std::endl; std::exit(EXIT_FAILURE); } auto [staging_buf, staging_buf_device_mem] = create_buf(physical_device, device, img_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); { void* staging_buf_mem; if (VkResult res = vkMapMemory(device, staging_buf_device_mem, 0, img_size, 0, &staging_buf_mem); res != VK_SUCCESS) { std::cerr << "failed to map staging buffer memory, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } memcpy(staging_buf_mem, pixels, img_size); vkUnmapMemory(device, staging_buf_device_mem); } stbi_image_free(pixels); auto [texture_img, texture_img_device_mem] = create_img(physical_device, device, static_cast(w), static_cast(h), VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); { auto cmd_buf = begin_single_time_cmds(device, cmd_pool); transition_image_layout(cmd_buf, texture_img, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, {}, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT); copy_buf_to_img(cmd_buf, staging_buf, texture_img, static_cast(w), static_cast(h)); transition_image_layout(cmd_buf, texture_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_ACCESS_2_SHADER_READ_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT); end_single_time_cmds(graphics_queue, device, cmd_pool, cmd_buf); } vkFreeMemory(device, staging_buf_device_mem, nullptr); vkDestroyBuffer(device, staging_buf, nullptr); return std::tuple { texture_img, texture_img_device_mem }; }(); // create texture image view // TODO: this is the same code, with minor differences, than the one that create swapchain image // views, so maybe we should create a function. The thing is, this code only call a single // function with a single create-info, so I'm not sure if it would make sense auto texture_img_view = [&]() { VkImageViewCreateInfo img_view_ci { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .pNext = {}, .flags = {}, .image = texture_img, .viewType = VK_IMAGE_VIEW_TYPE_2D, .format = VK_FORMAT_R8G8B8A8_SRGB, .components = {}, .subresourceRange = { // VkImageSubresourceRange .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1, }, }; VkImageView texture_img_view; if (VkResult res = vkCreateImageView(device, &img_view_ci, nullptr, &texture_img_view); res != VK_SUCCESS) { std::cerr << "failed to create texture image view, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return texture_img_view; }(); // create texture sampler auto sampler = [&]() { VkSamplerCreateInfo sampler_ci { .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .pNext = nullptr, .flags = {}, .magFilter = VK_FILTER_LINEAR, .minFilter = VK_FILTER_LINEAR, .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, .mipLodBias = 0.f, .anisotropyEnable = VK_TRUE, .maxAnisotropy = physical_device_props.properties.limits.maxSamplerAnisotropy, .compareEnable = VK_FALSE, // TODO: check if it has to be VK_COMPARE_OP_ALWAYS instead of VK_COMPARE_OP_NEVER .compareOp = VK_COMPARE_OP_NEVER, .minLod = 0.f, .maxLod = 0.f, .borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK, .unnormalizedCoordinates = VK_FALSE, }; VkSampler sampler; if (VkResult res = vkCreateSampler(device, &sampler_ci, nullptr, &sampler); res != VK_SUCCESS) { std::cerr << "failed to create sampler, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return sampler; }(); // create vertex buffer auto [vertex_buf, vertex_buf_device_mem] = [&]() { VkDeviceSize vertex_buf_size = sizeof(engine::vk::Vertex) * vertices.size(); auto [staging_buf, staging_buf_device_mem] = create_buf(physical_device, device, vertex_buf_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); { void* staging_buf_mem; if (VkResult res = vkMapMemory(device, staging_buf_device_mem, 0, vertex_buf_size, 0, &staging_buf_mem); res != VK_SUCCESS) { std::cerr << "failed to map staging buffer memory, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } memcpy(staging_buf_mem, vertices.data(), static_cast(vertex_buf_size)); vkUnmapMemory(device, staging_buf_device_mem); } auto [vertex_buf, vertex_buf_device_mem] = create_buf(physical_device, device, vertex_buf_size, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); // TODO: consider using different command pool for short-lived command buffers, with the flag // VK_COMMAND_POOL_CREATE_TRANSIENT_BIT, for optimization. For now we will stick to the one // which does the rendering // TODO: don't wait individually for every copy operations, use a fence or something similar { auto cmd_buf = begin_single_time_cmds(device, cmd_pool); copy_buf_to_buf(cmd_buf, staging_buf, vertex_buf, vertex_buf_size); end_single_time_cmds(graphics_queue, device, cmd_pool, cmd_buf); } vkFreeMemory(device, staging_buf_device_mem, nullptr); vkDestroyBuffer(device, staging_buf, nullptr); return std::tuple { vertex_buf, vertex_buf_device_mem }; }(); // create index buffer // TODO: this code is pretty much a duplicate with minor differences of the vertex_buf one. We // should probably factor it out in a function. Also, every TODOs were remove for this one, but // they still hold auto [index_buf, index_buf_device_mem] = [&]() { VkDeviceSize index_buf_size = sizeof(uint16_t) * indices.size(); auto [staging_buf, staging_buf_device_mem] = create_buf(physical_device, device, index_buf_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); { void* staging_buf_mem; if (VkResult res = vkMapMemory(device, staging_buf_device_mem, 0, index_buf_size, 0, &staging_buf_mem); res != VK_SUCCESS) { std::cerr << "failed to map staging buffer memory, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } memcpy(staging_buf_mem, indices.data(), static_cast(index_buf_size)); vkUnmapMemory(device, staging_buf_device_mem); } auto [index_buf, index_buf_device_mem] = create_buf(physical_device, device, index_buf_size, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); { auto cmd_buf = begin_single_time_cmds(device, cmd_pool); copy_buf_to_buf(cmd_buf, staging_buf, index_buf, index_buf_size); end_single_time_cmds(graphics_queue, device, cmd_pool, cmd_buf); } vkFreeMemory(device, staging_buf_device_mem, nullptr); vkDestroyBuffer(device, staging_buf, nullptr); return std::tuple { index_buf, index_buf_device_mem }; }(); // create uniform buffers auto [uniform_bufs, uniform_buf_device_mems, uniform_buf_mems] = [&]() { constexpr VkDeviceSize uniform_buf_size = sizeof(engine::vk::UniformBufferObject); std::array uniform_bufs; std::array uniform_buf_device_mems; std::array uniform_buf_mems; for (size_t i = 0; i < max_frames_in_flight; i++) { std::tie(uniform_bufs[i], uniform_buf_device_mems[i]) = create_buf(physical_device, device, uniform_buf_size, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); if (VkResult res = vkMapMemory(device, uniform_buf_device_mems[i], 0, uniform_buf_size, 0, &uniform_buf_mems[i]); res != VK_SUCCESS) { std::cerr << "failed to map uniform buffer #" << i << " memory, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } } return std::tuple { uniform_bufs, uniform_buf_device_mems, uniform_buf_mems }; }(); // create descriptor pool auto descriptor_pool = [&]() { std::array descriptor_pool_sizes { VkDescriptorPoolSize { .type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .descriptorCount = max_frames_in_flight, }, VkDescriptorPoolSize { .type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = max_frames_in_flight, }, }; VkDescriptorPoolCreateInfo descriptor_pool_ci { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, .pNext = nullptr, // TODO: right now we do not touch descriptor sets after they've been created, so // setting VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT is useless .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, .maxSets = max_frames_in_flight, .poolSizeCount = descriptor_pool_sizes.size(), .pPoolSizes = descriptor_pool_sizes.data(), }; VkDescriptorPool descriptor_pool; if (VkResult res = vkCreateDescriptorPool(device, &descriptor_pool_ci, nullptr, &descriptor_pool); res != VK_SUCCESS) { std::cerr << "failed to create descriptor pool, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return descriptor_pool; }(); // create descriptor sets auto descriptor_sets = [&]() { std::array layouts; layouts.fill(descriptor_set_layout); VkDescriptorSetAllocateInfo descriptor_set_ai { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, .pNext = nullptr, .descriptorPool = descriptor_pool, .descriptorSetCount = static_cast(layouts.size()), .pSetLayouts = layouts.data(), }; std::array descriptor_sets; if (VkResult res = vkAllocateDescriptorSets(device, &descriptor_set_ai, descriptor_sets.data()); res != VK_SUCCESS) { std::cerr << "failed to allocate descriptor sets, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } std::array descriptor_buffer_infos; std::array descriptor_image_infos; // the 2 comes from uniform buffer object + combined image sampler std::array write_descriptor_sets; for (size_t i = 0; i < max_frames_in_flight; i++) { // uniform buffer object descriptor_buffer_infos[i] = VkDescriptorBufferInfo { .buffer = uniform_bufs[i], .offset = 0, .range = sizeof(engine::vk::UniformBufferObject), }; write_descriptor_sets[2 * i + 0] = VkWriteDescriptorSet { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .pNext = nullptr, .dstSet = descriptor_sets[i], .dstBinding = 0, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .pImageInfo = {}, .pBufferInfo = &descriptor_buffer_infos[i], .pTexelBufferView = {}, }; // combined image sampler descriptor_image_infos[i] = VkDescriptorImageInfo { .sampler = sampler, .imageView = texture_img_view, .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, }; write_descriptor_sets[2 * i + 1] = VkWriteDescriptorSet { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .pNext = nullptr, .dstSet = descriptor_sets[i], .dstBinding = 1, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = &descriptor_image_infos[i], .pBufferInfo = {}, .pTexelBufferView = {}, }; } // TODO: maybe we should call this function for each descriptor sets, which I would find // weird by the fact that it takes an array as an argument, but IDK vkUpdateDescriptorSets(device, static_cast(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, nullptr); return descriptor_sets; }(); // create command buffers auto cmd_bufs = [&]() { VkCommandBufferAllocateInfo cmd_buf_ai { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .pNext = nullptr, .commandPool = cmd_pool, .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = max_frames_in_flight, }; std::array cmd_bufs; if (VkResult res = vkAllocateCommandBuffers(device, &cmd_buf_ai, cmd_bufs.data()); res != VK_SUCCESS) { std::cerr << "failed to allocate command buffers, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } return cmd_bufs; }(); // create sync objects auto [sems_present_complete, sems_render_finished] = [&]() { // TODO: remove duplicate code auto sems_present_complete = [&]() { std::array sems_present_complete; size_t num = 0; for (auto& sem : sems_present_complete) { VkSemaphoreCreateInfo sem_ci { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, .pNext = nullptr, .flags = {}, }; if (VkResult res = vkCreateSemaphore(device, &sem_ci, nullptr, &sem); res != VK_SUCCESS) { std::cerr << "failed to create present complete semaphore #" << (++num) << ", error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } } return sems_present_complete; }(); auto sems_render_finished = [&]() { std::vector sems_render_finished(swapchain_imgs.size()); size_t num = 0; for (auto& sem : sems_render_finished) { VkSemaphoreCreateInfo sem_ci { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, .pNext = nullptr, .flags = {}, }; if (VkResult res = vkCreateSemaphore(device, &sem_ci, nullptr, &sem); res != VK_SUCCESS) { std::cerr << "failed to create render finished semaphore #" << (++num) << ", error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } } return sems_render_finished; }(); return std::tuple { sems_present_complete, sems_render_finished, }; }(); std::cout << "sems_present_complete: [ "; for (auto it_sem = sems_present_complete.begin(); it_sem != sems_present_complete.end(); ++it_sem) { if (it_sem != sems_present_complete.begin()) std::cout << ", "; std::cout << *it_sem; } std::cout << " ]" << std::endl; std::cout << "sems_render_finished: [ "; for (auto it_sem = sems_render_finished.begin(); it_sem != sems_render_finished.end(); ++it_sem) { if (it_sem != sems_render_finished.begin()) std::cout << ", "; std::cout << *it_sem; } std::cout << " ]" << std::endl; auto fences_in_flight = [&]() { std::array fences_in_flight; size_t num = 0; for (auto& fence_in_flight : fences_in_flight) { VkFenceCreateInfo fence_in_flight_ci { .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .pNext = nullptr, .flags = VK_FENCE_CREATE_SIGNALED_BIT, }; if (VkResult res = vkCreateFence(device, &fence_in_flight_ci, nullptr, &fence_in_flight); res != VK_SUCCESS) { std::cerr << "failed to create draw fence #" << (++num) << ", error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } } return fences_in_flight; }(); std::cout << "fences_in_flight: [ "; for (auto it_fence = fences_in_flight.begin(); it_fence != fences_in_flight.end(); ++it_fence) { if (it_fence != fences_in_flight.begin()) std::cout << ", "; std::cout << *it_fence; } std::cout << " ]" << std::endl; auto start_time = std::chrono::high_resolution_clock::now(); auto last_time = start_time; uint32_t frame_idx = 0; // main loop while (!glfwWindowShouldClose(window)) { glfwPollEvents(); auto cur_time = std::chrono::high_resolution_clock::now(); float ellapsed_time = std::chrono::duration(cur_time - last_time).count(); last_time = cur_time; float time = std::chrono::duration(cur_time - start_time).count(); if (show_fps) std::cout << "\rfps: " << (1.f / ellapsed_time); if (VkResult res = vkWaitForFences(device, 1, &fences_in_flight[frame_idx], VK_TRUE, std::numeric_limits::max()); res != VK_SUCCESS) { std::cerr << "failed to wait for draw fence, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } uint32_t img_idx; { VkResult res = vkAcquireNextImageKHR(device, swapchain, std::numeric_limits::max(), sems_present_complete[frame_idx], nullptr, &img_idx); switch (res) { case VK_SUBOPTIMAL_KHR: case VK_SUCCESS: break; case VK_ERROR_OUT_OF_DATE_KHR: recreate_swapchain(); continue; default: std::cerr << "failed to acquire next image, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } } // update uniform buffer { // TODO: should use push constants engine::vk::UniformBufferObject ubo { .model = Matrix4::rot_z(time * PI / 2.f), .view = Matrix4::look_at(Vector3(2.f, 2.f, 2.f), Vector3(0.f, 0.f, 0.f), Vector3(0.f, 0.f, 1.f)), .proj = Matrix4::perspective(PI / 4.f, static_cast(swapchain_extent.width) / static_cast(swapchain_extent.height), .1f, 10.f), }; memcpy(uniform_buf_mems[frame_idx], &ubo, sizeof(engine::vk::UniformBufferObject)); } // implicit command buffer reset // record command buffer { VkCommandBufferBeginInfo cmd_buf_bi { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, .pNext = nullptr, // TODO: I don't understand why here we shouldn't also set // VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, because we always submit this // command buffer once before resetting it, like when we copy the staging buffer to // the vertex buffer (where we set this flag). The only difference is that we wait // for the queue to idle for the latter, where here we only fence for the end of // vkQueueSubmit .flags = {}, .pInheritanceInfo = {}, }; if (VkResult res = vkBeginCommandBuffer(cmd_bufs[frame_idx], &cmd_buf_bi); res != VK_SUCCESS) { std::cerr << "failed to begin command buffer, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } } // transition layouts [&]() { std::array img_mem_barriers { VkImageMemoryBarrier2 { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .pNext = nullptr, .srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, .srcAccessMask = {}, .dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, .dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = swapchain_imgs[img_idx], .subresourceRange = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1, }, }, VkImageMemoryBarrier2 { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .pNext = nullptr, .srcStageMask = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT, .srcAccessMask = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT, .dstAccessMask = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = (has_stencil(depth_format) ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL), .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = depth_img, .subresourceRange = { .aspectMask = static_cast(VK_IMAGE_ASPECT_DEPTH_BIT | (has_stencil(depth_format) ? VK_IMAGE_ASPECT_STENCIL_BIT : 0)), .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1, }, }, }; VkDependencyInfo dependency_info { .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .pNext = nullptr, .dependencyFlags = {}, .memoryBarrierCount = 0, .pMemoryBarriers = {}, .bufferMemoryBarrierCount = 0, .pBufferMemoryBarriers = {}, .imageMemoryBarrierCount = img_mem_barriers.size(), .pImageMemoryBarriers = img_mem_barriers.data(), }; vkCmdPipelineBarrier2(cmd_bufs[frame_idx], &dependency_info); }(); { VkClearValue clear_color_val { .color { .float32 = { 0.f, 0.f, 0.f, 1.f } } }; VkClearValue clear_depth_val { .depthStencil { .depth = 1.f, .stencil = 0 } }; VkRenderingAttachmentInfo rendering_color_info { .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, .pNext = nullptr, .imageView = swapchain_img_views[img_idx], .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, .resolveMode = {}, .resolveImageView = {}, .resolveImageLayout = {}, .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, .clearValue = clear_color_val, }; VkRenderingAttachmentInfo rendering_depth_info { .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, .pNext = nullptr, .imageView = depth_img_view, .imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, .resolveMode = {}, .resolveImageView = {}, .resolveImageLayout = {}, .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, .storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, .clearValue = clear_depth_val, }; VkRenderingInfo render_info { .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, .pNext = nullptr, .flags = {}, .renderArea = { .offset { 0, 0 }, .extent = swapchain_extent }, .layerCount = 1, .viewMask = {}, .colorAttachmentCount = 1, .pColorAttachments = &rendering_color_info, .pDepthAttachment = &rendering_depth_info, .pStencilAttachment = {}, }; vkCmdBeginRendering(cmd_bufs[frame_idx], &render_info); } vkCmdBindPipeline(cmd_bufs[frame_idx], VK_PIPELINE_BIND_POINT_GRAPHICS, graphics_pl); { VkViewport viewport { .x = 0.f, .y = 0.f, .width = static_cast(swapchain_extent.width), .height = static_cast(swapchain_extent.height), .minDepth = 0.f, .maxDepth = 1.f, }; vkCmdSetViewport(cmd_bufs[frame_idx], 0, 1, &viewport); } { VkRect2D scissor { .offset = { 0, 0 }, .extent = swapchain_extent, }; vkCmdSetScissor(cmd_bufs[frame_idx], 0, 1, &scissor); } VkDeviceSize vertex_buf_offset = 0; vkCmdBindVertexBuffers(cmd_bufs[frame_idx], 0, 1, &vertex_buf, &vertex_buf_offset); vkCmdBindIndexBuffer(cmd_bufs[frame_idx], index_buf, 0, VK_INDEX_TYPE_UINT16); vkCmdBindDescriptorSets(cmd_bufs[frame_idx], VK_PIPELINE_BIND_POINT_GRAPHICS, pl_layout, 0, 1, &descriptor_sets[frame_idx], 0, nullptr); vkCmdDrawIndexed(cmd_bufs[frame_idx], static_cast(indices.size()), 1, 0, 0, 0); vkCmdEndRendering(cmd_bufs[frame_idx]); transition_image_layout(cmd_bufs[frame_idx], swapchain_imgs[img_idx], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, {}, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT); if (VkResult res = vkEndCommandBuffer(cmd_bufs[frame_idx]); res != VK_SUCCESS) { std::cerr << "failed to end command buffer, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } if (VkResult res = vkResetFences(device, 1, &fences_in_flight[frame_idx]); res != VK_SUCCESS) { std::cerr << "failed to reset draw fence, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } { VkPipelineStageFlags pl_stage_flags = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; VkSubmitInfo submit_info { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .pNext = nullptr, .waitSemaphoreCount = 1, .pWaitSemaphores = &sems_present_complete[frame_idx], .pWaitDstStageMask = &pl_stage_flags, .commandBufferCount = 1, .pCommandBuffers = &cmd_bufs[frame_idx], .signalSemaphoreCount = 1, .pSignalSemaphores = &sems_render_finished[img_idx], }; if (VkResult res = vkQueueSubmit(graphics_queue, 1, &submit_info, fences_in_flight[frame_idx]); res != VK_SUCCESS) { std::cerr << "failed to submit command buffer queue, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } } { VkPresentInfoKHR present_info { .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, .pNext = nullptr, .waitSemaphoreCount = 1, .pWaitSemaphores = &sems_render_finished[img_idx], .swapchainCount = 1, .pSwapchains = &swapchain, .pImageIndices = &img_idx, .pResults = nullptr, }; VkResult res = vkQueuePresentKHR(present_queue, &present_info); if (res == VK_SUBOPTIMAL_KHR || res == VK_ERROR_OUT_OF_DATE_KHR || fb_resized) { fb_resized = false; recreate_swapchain(); } else if (res != VK_SUCCESS) { std::cerr << "failed to present, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } } frame_idx = (frame_idx + 1) % max_frames_in_flight; } if (show_fps) std::cout << std::endl; if (VkResult res = vkDeviceWaitIdle(device); res != VK_SUCCESS) { std::cerr << "failed to wait idle for device, error code: " << string_VkResult(res) << std::endl; std::exit(EXIT_FAILURE); } // cleanup for (auto it_fence = fences_in_flight.rbegin(); it_fence != fences_in_flight.rend(); ++it_fence) vkDestroyFence(device, *it_fence, nullptr); for (auto it_sem = sems_render_finished.rbegin(); it_sem != sems_render_finished.rend(); ++it_sem) vkDestroySemaphore(device, *it_sem, nullptr); for (auto it_sem = sems_present_complete.rbegin(); it_sem != sems_present_complete.rend(); ++it_sem) vkDestroySemaphore(device, *it_sem, nullptr); vkDestroyDescriptorPool(device, descriptor_pool, nullptr); for (size_t i = max_frames_in_flight; i > 0; i--) { vkUnmapMemory(device, uniform_buf_device_mems[i - 1]); vkFreeMemory(device, uniform_buf_device_mems[i - 1], nullptr); vkDestroyBuffer(device, uniform_bufs[i - 1], nullptr); } vkFreeMemory(device, index_buf_device_mem, nullptr); vkDestroyBuffer(device, index_buf, nullptr); vkFreeMemory(device, vertex_buf_device_mem, nullptr); vkDestroyBuffer(device, vertex_buf, nullptr); vkDestroySampler(device, sampler, nullptr); vkDestroyImageView(device, texture_img_view, nullptr); vkFreeMemory(device, texture_img_device_mem, nullptr); vkDestroyImage(device, texture_img, nullptr); destroy_depth_resources(device, depth_img, depth_img_device_mem, depth_img_view); // no need to free cmd_buf because destroying the command pool will vkDestroyCommandPool(device, cmd_pool, nullptr); // no need to free descriptor_sets because destroying the descriptor pool will vkDestroyDescriptorSetLayout(device, descriptor_set_layout, nullptr); vkDestroyPipelineLayout(device, pl_layout, nullptr); vkDestroyPipeline(device, graphics_pl, nullptr); destroy_swapchain(); vkDestroySurfaceKHR(instance, surface, nullptr); vkDestroyDevice(device, nullptr); if (enable_validation_layers) { auto destroy_debug_messenger = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"); if (!destroy_debug_messenger) { std::cerr << "failed to destroy debug messenger!" << std::endl; std::exit(EXIT_FAILURE); } destroy_debug_messenger(instance, debug_messenger, nullptr); } vkDestroyInstance(instance, nullptr); glfwDestroyWindow(window); glfwTerminate(); return EXIT_SUCCESS; } static std::vector convert_args(int argc, char *argv[]) { std::vector args(argc); for (int i = 0; i < argc; i++) args[i] = argv[i]; return args; } static void parse_args(const std::vector& args, int& mode) { for (auto args_iter = std::next(args.begin()); args_iter != args.end(); args_iter++) { const auto& arg = *args_iter; if (arg.size() >= 1 && arg[0] == '-') { if (arg.size() >= 2 && arg[1] == '-') { auto long_opt = arg.substr(2); if (long_opt == "help") { mode = MODE_HELP; } else if (long_opt == "term") { mode = MODE_TERM; } else if (long_opt == "graphical") { mode = MODE_GRAPHICAL; } else { std::cerr << "Error: Unexpected option `--" << long_opt << "'." << std::endl; usage_error_exit(); } } else { std::size_t arg_len = arg.size(); if (arg_len == 1) { std::cerr << "Error: Unexpected argument `-'." << std::endl; usage_error_exit(); } for (auto arg_iter = std::next(arg.begin()); arg_iter != arg.end(); arg_iter++) { const auto& opt = *arg_iter; switch (opt) { case 'h': mode = MODE_HELP; break; case 't': mode = MODE_TERM; break; case 'g': mode = MODE_GRAPHICAL; break; default: std::cerr << "Error: Unexpected option `-" << opt << "'." << std::endl; usage_error_exit(); } } } } else { std::cerr << "Error: Unexpected argument `" << arg << "'." << std::endl; usage_error_exit(); } } } int main(int argc, char *argv[]) { int mode = MODE_GRAPHICAL; parse_args(convert_args(argc, argv), mode); switch (mode) { case MODE_HELP: print_usage(std::cout); return EXIT_SUCCESS; case MODE_TERM: #ifdef HAVE_NCURSES return main_term(); #else std::cerr << "Error: ncurses was not enabled during compilation." << std::endl; return EXIT_FAILURE; #endif case MODE_GRAPHICAL: return main_graphical(); default: std::unreachable(); } }