#include "sdl-game.hpp" #include #include #include #include "IMGUI/imgui_impl_sdl.h" #include "logger.hpp" #include "utils.hpp" #include "gui/imgui/button-imgui.hpp" using namespace std; #define IMGUI_UNLIMITED_FRAME_RATE static void check_imgui_vk_result(VkResult res) { if (res == VK_SUCCESS) { return; } ostringstream oss; oss << "[imgui] Vulkan error! VkResult is \"" << VulkanUtils::resultString(res) << "\"" << __LINE__; if (res < 0) { throw runtime_error("Fatal: " + oss.str()); } else { cerr << oss.str(); } } VKAPI_ATTR VkBool32 VKAPI_CALL VulkanGame::debugCallback( VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { cerr << "validation layer: " << pCallbackData->pMessage << endl; // TODO: Figure out what the return value means and if it should always be VK_FALSE return VK_FALSE; } VulkanGame::VulkanGame() : swapChainImageCount(0) , swapChainMinImageCount(0) , swapChainSurfaceFormat({}) , swapChainPresentMode(VK_PRESENT_MODE_MAX_ENUM_KHR) , swapChainExtent{ 0, 0 } , swapChain(VK_NULL_HANDLE) , vulkanSurface(VK_NULL_HANDLE) , sdlVersion({ 0, 0, 0 }) , instance(VK_NULL_HANDLE) , physicalDevice(VK_NULL_HANDLE) , device(VK_NULL_HANDLE) , debugMessenger(VK_NULL_HANDLE) , resourceCommandPool(VK_NULL_HANDLE) , renderPass(VK_NULL_HANDLE) , graphicsQueue(VK_NULL_HANDLE) , presentQueue(VK_NULL_HANDLE) , depthImage({}) , shouldRecreateSwapChain(false) , frameCount(0) , currentFrame(0) , imageIndex(0) , fpsStartTime(0.0f) , curTime(0.0f) , done(false) , currentRenderScreenFn(nullptr) , imguiDescriptorPool(VK_NULL_HANDLE) , gui(nullptr) , window(nullptr) , uniforms_modelPipeline() , objects_modelPipeline() , score(0) , fps(0.0f) { } VulkanGame::~VulkanGame() { } void VulkanGame::run(int width, int height, unsigned char guiFlags) { cout << "Vulkan Game" << endl; cout << "DEBUGGING IS " << (ENABLE_VALIDATION_LAYERS ? "ON" : "OFF") << endl; if (initUI(width, height, guiFlags) == RTWO_ERROR) { return; } initVulkan(); VkPhysicalDeviceProperties deviceProperties; vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties); uniforms_modelPipeline = VulkanBuffer(1, deviceProperties.limits.maxUniformBufferRange, deviceProperties.limits.minUniformBufferOffsetAlignment); objects_modelPipeline = VulkanBuffer(10, deviceProperties.limits.maxUniformBufferRange, deviceProperties.limits.minUniformBufferOffsetAlignment); initImGuiOverlay(); // TODO: Figure out how much of ubo creation and associated variables should be in the pipeline class // Maybe combine the ubo-related objects into a new class initGraphicsPipelines(); initMatrices(); modelPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::pos)); modelPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::color)); modelPipeline.addAttribute(VK_FORMAT_R32G32_SFLOAT, offset_of(&ModelVertex::texCoord)); modelPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::normal)); modelPipeline.addAttribute(VK_FORMAT_R32_UINT, offset_of(&ModelVertex::objIndex)); createBufferSet(uniforms_modelPipeline.memorySize(), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, uniformBuffers_modelPipeline); uniforms_modelPipeline.map(uniformBuffers_modelPipeline.memory, device); createBufferSet(objects_modelPipeline.memorySize(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, objectBuffers_modelPipeline); objects_modelPipeline.map(objectBuffers_modelPipeline.memory, device); modelPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, &uniformBuffers_modelPipeline.infoSet); modelPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, VK_SHADER_STAGE_VERTEX_BIT, &objectBuffers_modelPipeline.infoSet); modelPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, &floorTextureImageDescriptor); SceneObject* texturedSquare = nullptr; texturedSquare = &addObject(modelObjects, modelPipeline, addObjectIndex(modelObjects.size(), addVertexNormals({ {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}}, {{ 0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}}, {{ 0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}}, {{ 0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}}, {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f}}, {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}} })), { 0, 1, 2, 3, 4, 5 }, objects_modelPipeline, { mat4(1.0f) }); texturedSquare->model_base = translate(mat4(1.0f), vec3(0.0f, 0.0f, -2.0f)); texturedSquare = &addObject(modelObjects, modelPipeline, addObjectIndex(modelObjects.size(), addVertexNormals({ {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}}, {{ 0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}}, {{ 0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}}, {{ 0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}}, {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f}}, {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}} })), { 0, 1, 2, 3, 4, 5 }, objects_modelPipeline, { mat4(1.0f) }); texturedSquare->model_base = translate(mat4(1.0f), vec3(0.0f, 0.0f, -1.5f)); modelPipeline.createDescriptorSetLayout(); modelPipeline.createPipeline("shaders/model-vert.spv", "shaders/model-frag.spv"); modelPipeline.createDescriptorPool(swapChainImages.size()); modelPipeline.createDescriptorSets(swapChainImages.size()); currentRenderScreenFn = &VulkanGame::renderMainScreen; ImGuiIO& io = ImGui::GetIO(); initGuiValueLists(valueLists); valueLists["stats value list"].push_back(UIValue(UIVALUE_INT, "Score", &score)); valueLists["stats value list"].push_back(UIValue(UIVALUE_DOUBLE, "FPS", &fps)); valueLists["stats value list"].push_back(UIValue(UIVALUE_DOUBLE, "IMGUI FPS", &io.Framerate)); renderLoop(); cleanup(); close_log(); } bool VulkanGame::initUI(int width, int height, unsigned char guiFlags) { // TODO: Create a game-gui function to get the gui version and retrieve it that way SDL_VERSION(&sdlVersion); // This gets the compile-time version SDL_GetVersion(&sdlVersion); // This gets the runtime version cout << "SDL " << to_string(sdlVersion.major) << "." << to_string(sdlVersion.minor) << "." << to_string(sdlVersion.patch) << endl; // TODO: Refactor the logger api to be more flexible, // esp. since gl_log() and gl_log_err() have issues printing anything besides strings restart_gl_log(); gl_log("starting SDL\n%s.%s.%s", to_string(sdlVersion.major).c_str(), to_string(sdlVersion.minor).c_str(), to_string(sdlVersion.patch).c_str()); // TODO: Use open_Log() and related functions instead of gl_log ones // TODO: In addition, delete the gl_log functions open_log(); get_log() << "starting SDL" << endl; get_log() << (int)sdlVersion.major << "." << (int)sdlVersion.minor << "." << (int)sdlVersion.patch << endl; // TODO: Put all fonts, textures, and images in the assets folder gui = new GameGui_SDL(); if (gui->init() == RTWO_ERROR) { // TODO: Also print these sorts of errors to the log cout << "UI library could not be initialized!" << endl; cout << gui->getError() << endl; // TODO: Rename RTWO_ERROR to something else return RTWO_ERROR; } window = (SDL_Window*)gui->createWindow("Vulkan Game", width, height, guiFlags & GUI_FLAGS_WINDOW_FULLSCREEN); if (window == nullptr) { cout << "Window could not be created!" << endl; cout << gui->getError() << endl; return RTWO_ERROR; } cout << "Target window size: (" << width << ", " << height << ")" << endl; cout << "Actual window size: (" << gui->getWindowWidth() << ", " << gui->getWindowHeight() << ")" << endl; return RTWO_SUCCESS; } void VulkanGame::initVulkan() { const vector validationLayers = { "VK_LAYER_KHRONOS_validation" }; const vector deviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; createVulkanInstance(validationLayers); setupDebugMessenger(); createVulkanSurface(); pickPhysicalDevice(deviceExtensions); createLogicalDevice(validationLayers, deviceExtensions); chooseSwapChainProperties(); createSwapChain(); createImageViews(); createResourceCommandPool(); createImageResources(); createRenderPass(); createCommandPools(); createFramebuffers(); createCommandBuffers(); createSyncObjects(); } void VulkanGame::initGraphicsPipelines() { modelPipeline = GraphicsPipeline_Vulkan( VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, physicalDevice, device, renderPass, { 0, 0, (int)swapChainExtent.width, (int)swapChainExtent.height }, 16, 24); } // TODO: Maybe change the name to initScene() or something similar void VulkanGame::initMatrices() { cam_pos = vec3(0.0f, 0.0f, 2.0f); float cam_yaw = 0.0f; float cam_pitch = -50.0f; mat4 yaw_mat = rotate(mat4(1.0f), radians(-cam_yaw), vec3(0.0f, 1.0f, 0.0f)); mat4 pitch_mat = rotate(mat4(1.0f), radians(-cam_pitch), vec3(1.0f, 0.0f, 0.0f)); mat4 R_view = pitch_mat * yaw_mat; mat4 T_view = translate(mat4(1.0f), vec3(-cam_pos.x, -cam_pos.y, -cam_pos.z)); viewMat = R_view * T_view; projMat = perspective(radians(FOV_ANGLE), (float)swapChainExtent.width / (float)swapChainExtent.height, NEAR_CLIP, FAR_CLIP); projMat[1][1] *= -1; // flip the y-axis so that +y is up } void VulkanGame::renderLoop() { startTime = steady_clock::now(); curTime = duration(steady_clock::now() - startTime).count(); fpsStartTime = curTime; frameCount = 0; ImGuiIO& io = ImGui::GetIO(); done = false; while (!done) { prevTime = curTime; curTime = duration(steady_clock::now() - startTime).count(); elapsedTime = curTime - prevTime; if (curTime - fpsStartTime >= 1.0f) { fps = (float)frameCount / (curTime - fpsStartTime); frameCount = 0; fpsStartTime = curTime; } frameCount++; gui->processEvents(); UIEvent uiEvent; while (gui->pollEvent(&uiEvent)) { GameEvent& e = uiEvent.event; SDL_Event sdlEvent = uiEvent.rawEvent.sdl; ImGui_ImplSDL2_ProcessEvent(&sdlEvent); if ((e.type == UI_EVENT_MOUSEBUTTONDOWN || e.type == UI_EVENT_MOUSEBUTTONUP || e.type == UI_EVENT_UNKNOWN) && io.WantCaptureMouse) { if (sdlEvent.type == SDL_MOUSEWHEEL || sdlEvent.type == SDL_MOUSEBUTTONDOWN || sdlEvent.type == SDL_MOUSEBUTTONUP) { continue; } } if ((e.type == UI_EVENT_KEYDOWN || e.type == UI_EVENT_KEYUP) && io.WantCaptureKeyboard) { if (sdlEvent.type == SDL_KEYDOWN || sdlEvent.type == SDL_KEYUP) { continue; } } if (io.WantTextInput) { // show onscreen keyboard if on mobile } switch (e.type) { case UI_EVENT_QUIT: cout << "Quit event detected" << endl; done = true; break; case UI_EVENT_WINDOWRESIZE: cout << "Window resize event detected" << endl; shouldRecreateSwapChain = true; break; case UI_EVENT_KEYDOWN: if (e.key.repeat) { break; } if (e.key.keycode == SDL_SCANCODE_ESCAPE) { done = true; } else if (e.key.keycode == SDL_SCANCODE_SPACE) { cout << "Adding a plane" << endl; float zOffset = -2.0f + (0.5f * modelObjects.size()); SceneObject& texturedSquare = addObject(modelObjects, modelPipeline, addObjectIndex(modelObjects.size(), addVertexNormals({ {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}}, {{ 0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}}, {{ 0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}}, {{ 0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}}, {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f}}, {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}} })), { 0, 1, 2, 3, 4, 5 }, objects_modelPipeline, { mat4(1.0f) }); texturedSquare.model_base = translate(mat4(1.0f), vec3(0.0f, 0.0f, zOffset)); // START UNREVIEWED SECTION // END UNREVIEWED SECTION } else { cout << "Key event detected" << endl; } break; case UI_EVENT_KEYUP: // START UNREVIEWED SECTION // END UNREVIEWED SECTION break; case UI_EVENT_WINDOW: case UI_EVENT_MOUSEBUTTONDOWN: case UI_EVENT_MOUSEBUTTONUP: case UI_EVENT_MOUSEMOTION: break; case UI_EVENT_UNHANDLED: cout << "Unhandled event type: 0x" << hex << sdlEvent.type << dec << endl; break; case UI_EVENT_UNKNOWN: default: cout << "Unknown event type: 0x" << hex << sdlEvent.type << dec << endl; break; } } if (shouldRecreateSwapChain) { gui->refreshWindowSize(); const bool isMinimized = gui->getWindowWidth() == 0 || gui->getWindowHeight() == 0; if (!isMinimized) { // TODO: This should be used if the min image count changes, presumably because a new surface was created // with a different image count or something like that. Maybe I want to add code to query for a new min image count // during swapchain recreation to take advantage of this ImGui_ImplVulkan_SetMinImageCount(swapChainMinImageCount); recreateSwapChain(); shouldRecreateSwapChain = false; } } updateScene(); // TODO: Move this into a renderImGuiOverlay() function ImGui_ImplVulkan_NewFrame(); ImGui_ImplSDL2_NewFrame(window); ImGui::NewFrame(); (this->*currentRenderScreenFn)(gui->getWindowWidth(), gui->getWindowHeight()); ImGui::Render(); gui->refreshWindowSize(); const bool isMinimized = gui->getWindowWidth() == 0 || gui->getWindowHeight() == 0; if (!isMinimized) { renderFrame(ImGui::GetDrawData()); presentFrame(); } } } void VulkanGame::updateScene() { uniforms_modelPipeline.data()->view = viewMat; uniforms_modelPipeline.data()->proj = projMat; // TODO: Probably move the resizing to the VulkanBuffer class's add() method // Remember that we want to avoid resizing the actuall ssbo or ubo more than once per frame // TODO: Figure out a way to make updateDescriptorInfo easier to use, maybe store the binding index in the buffer set if (objects_modelPipeline.resized) { objects_modelPipeline.unmap(objectBuffers_modelPipeline.memory, device); resizeBufferSet(objectBuffers_modelPipeline, objects_modelPipeline.memorySize(), resourceCommandPool, graphicsQueue, true); objects_modelPipeline.map(objectBuffers_modelPipeline.memory, device); objects_modelPipeline.resize(); modelPipeline.updateDescriptorInfo(1, &objectBuffers_modelPipeline.infoSet, swapChainImages.size()); } for (size_t i = 0; i < modelObjects.size(); i++) { SceneObject& obj = modelObjects[i]; SSBO_ModelObject& objData = objects_modelPipeline.get(i); // Rotate the textured squares obj.model_transform = translate(mat4(1.0f), vec3(0.0f, -2.0f, -0.0f)) * rotate(mat4(1.0f), curTime * radians(90.0f), vec3(0.0f, 0.0f, 1.0f)); objData.model = obj.model_transform * obj.model_base; obj.center = vec3(objData.model * vec4(0.0f, 0.0f, 0.0f, 1.0f)); } VulkanUtils::copyDataToMappedMemory(device, uniforms_modelPipeline.data(), uniforms_modelPipeline.mapped(imageIndex), uniformBuffers_modelPipeline.memory[imageIndex], uniforms_modelPipeline.memorySize(), true); VulkanUtils::copyDataToMappedMemory(device, objects_modelPipeline.data(), objects_modelPipeline.mapped(imageIndex), objectBuffers_modelPipeline.memory[imageIndex], objects_modelPipeline.memorySize(), true); } void VulkanGame::cleanup() { // FIXME: We could wait on the Queue if we had the queue in wd-> (otherwise VulkanH functions can't use globals) //vkQueueWaitIdle(g_Queue); VKUTIL_CHECK_RESULT(vkDeviceWaitIdle(device), "failed to wait for device!"); cleanupImGuiOverlay(); cleanupSwapChain(); VulkanUtils::destroyVulkanImage(device, floorTextureImage); // START UNREVIEWED SECTION vkDestroySampler(device, textureSampler, nullptr); modelPipeline.cleanupBuffers(); // END UNREVIEWED SECTION vkDestroyCommandPool(device, resourceCommandPool, nullptr); vkDestroyDevice(device, nullptr); vkDestroySurfaceKHR(instance, vulkanSurface, nullptr); if (ENABLE_VALIDATION_LAYERS) { VulkanUtils::destroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); } vkDestroyInstance(instance, nullptr); gui->destroyWindow(); gui->shutdown(); delete gui; } void VulkanGame::createVulkanInstance(const vector& validationLayers) { if (ENABLE_VALIDATION_LAYERS && !VulkanUtils::checkValidationLayerSupport(validationLayers)) { throw runtime_error("validation layers requested, but not available!"); } VkApplicationInfo appInfo = {}; appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; appInfo.pApplicationName = "Vulkan Game"; appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); appInfo.pEngineName = "No Engine"; appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); appInfo.apiVersion = VK_API_VERSION_1_0; VkInstanceCreateInfo createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; createInfo.pApplicationInfo = &appInfo; vector extensions = gui->getRequiredExtensions(); if (ENABLE_VALIDATION_LAYERS) { extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); } createInfo.enabledExtensionCount = static_cast(extensions.size()); createInfo.ppEnabledExtensionNames = extensions.data(); cout << endl << "Extensions:" << endl; for (const char* extensionName : extensions) { cout << extensionName << endl; } cout << endl; VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo; if (ENABLE_VALIDATION_LAYERS) { createInfo.enabledLayerCount = static_cast(validationLayers.size()); createInfo.ppEnabledLayerNames = validationLayers.data(); populateDebugMessengerCreateInfo(debugCreateInfo); createInfo.pNext = &debugCreateInfo; } else { createInfo.enabledLayerCount = 0; createInfo.pNext = nullptr; } if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { throw runtime_error("failed to create instance!"); } } void VulkanGame::setupDebugMessenger() { if (!ENABLE_VALIDATION_LAYERS) { return; } VkDebugUtilsMessengerCreateInfoEXT createInfo; populateDebugMessengerCreateInfo(createInfo); if (VulkanUtils::createDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { throw runtime_error("failed to set up debug messenger!"); } } void VulkanGame::populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) { createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; createInfo.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; createInfo.pfnUserCallback = debugCallback; } void VulkanGame::createVulkanSurface() { if (gui->createVulkanSurface(instance, &vulkanSurface) == RTWO_ERROR) { throw runtime_error("failed to create window surface!"); } } void VulkanGame::pickPhysicalDevice(const vector& deviceExtensions) { uint32_t deviceCount = 0; // TODO: Check VkResult vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); if (deviceCount == 0) { throw runtime_error("failed to find GPUs with Vulkan support!"); } vector devices(deviceCount); // TODO: Check VkResult vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()); cout << endl << "Graphics cards:" << endl; for (const VkPhysicalDevice& device : devices) { if (isDeviceSuitable(device, deviceExtensions)) { physicalDevice = device; break; } } cout << endl; if (physicalDevice == VK_NULL_HANDLE) { throw runtime_error("failed to find a suitable GPU!"); } } bool VulkanGame::isDeviceSuitable(VkPhysicalDevice physicalDevice, const vector& deviceExtensions) { VkPhysicalDeviceProperties deviceProperties; vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties); cout << "Device: " << deviceProperties.deviceName << endl; // TODO: Eventually, maybe let the user pick out of a set of GPUs in case the user does want to use // an integrated GPU. On my laptop, this function returns TRUE for the integrated GPU, but crashes // when trying to use it to render. Maybe I just need to figure out which other extensions and features // to check. if (deviceProperties.deviceType != VkPhysicalDeviceType::VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { return false; } QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, vulkanSurface); bool extensionsSupported = VulkanUtils::checkDeviceExtensionSupport(physicalDevice, deviceExtensions); bool swapChainAdequate = false; if (extensionsSupported) { vector formats = VulkanUtils::querySwapChainFormats(physicalDevice, vulkanSurface); vector presentModes = VulkanUtils::querySwapChainPresentModes(physicalDevice, vulkanSurface); swapChainAdequate = !formats.empty() && !presentModes.empty(); } VkPhysicalDeviceFeatures supportedFeatures; vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures); return indices.isComplete() && extensionsSupported && swapChainAdequate && supportedFeatures.samplerAnisotropy; } void VulkanGame::createLogicalDevice(const vector& validationLayers, const vector& deviceExtensions) { QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, vulkanSurface); if (!indices.isComplete()) { throw runtime_error("failed to find required queue families!"); } // TODO: Using separate graphics and present queues currently works, but I should verify that I'm // using them correctly to get the most benefit out of separate queues vector queueCreateInfoList; set uniqueQueueFamilies = { indices.graphicsFamily.value(), indices.presentFamily.value() }; float queuePriority = 1.0f; for (uint32_t queueFamily : uniqueQueueFamilies) { VkDeviceQueueCreateInfo queueCreateInfo = {}; queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; queueCreateInfo.queueCount = 1; queueCreateInfo.queueFamilyIndex = queueFamily; queueCreateInfo.pQueuePriorities = &queuePriority; queueCreateInfoList.push_back(queueCreateInfo); } VkPhysicalDeviceFeatures deviceFeatures = {}; deviceFeatures.samplerAnisotropy = VK_TRUE; VkDeviceCreateInfo createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; createInfo.queueCreateInfoCount = static_cast(queueCreateInfoList.size()); createInfo.pQueueCreateInfos = queueCreateInfoList.data(); createInfo.pEnabledFeatures = &deviceFeatures; createInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); createInfo.ppEnabledExtensionNames = deviceExtensions.data(); // These fields are ignored by up-to-date Vulkan implementations, // but it's a good idea to set them for backwards compatibility if (ENABLE_VALIDATION_LAYERS) { createInfo.enabledLayerCount = static_cast(validationLayers.size()); createInfo.ppEnabledLayerNames = validationLayers.data(); } else { createInfo.enabledLayerCount = 0; } if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) { throw runtime_error("failed to create logical device!"); } vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue); vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue); } void VulkanGame::chooseSwapChainProperties() { vector availableFormats = VulkanUtils::querySwapChainFormats(physicalDevice, vulkanSurface); vector availablePresentModes = VulkanUtils::querySwapChainPresentModes(physicalDevice, vulkanSurface); // Per Spec Format and View Format are expected to be the same unless VK_IMAGE_CREATE_MUTABLE_BIT was set at image creation // Assuming that the default behavior is without setting this bit, there is no need for separate Swapchain image and image view format // Additionally several new color spaces were introduced with Vulkan Spec v1.0.40, // hence we must make sure that a format with the mostly available color space, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, is found and used. swapChainSurfaceFormat = VulkanUtils::chooseSwapSurfaceFormat(availableFormats, { VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8_UNORM, VK_FORMAT_R8G8B8_UNORM }, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); #ifdef IMGUI_UNLIMITED_FRAME_RATE vector presentModes{ VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_FIFO_KHR }; #else vector presentModes{ VK_PRESENT_MODE_FIFO_KHR }; #endif swapChainPresentMode = VulkanUtils::chooseSwapPresentMode(availablePresentModes, presentModes); cout << "[vulkan] Selected PresentMode = " << swapChainPresentMode << endl; VkSurfaceCapabilitiesKHR capabilities = VulkanUtils::querySwapChainCapabilities(physicalDevice, vulkanSurface); // If min image count was not specified, request different count of images dependent on selected present mode if (swapChainMinImageCount == 0) { if (swapChainPresentMode == VK_PRESENT_MODE_MAILBOX_KHR) { swapChainMinImageCount = 3; } else if (swapChainPresentMode == VK_PRESENT_MODE_FIFO_KHR || swapChainPresentMode == VK_PRESENT_MODE_FIFO_RELAXED_KHR) { swapChainMinImageCount = 2; } else if (swapChainPresentMode == VK_PRESENT_MODE_IMMEDIATE_KHR) { swapChainMinImageCount = 1; } else { throw runtime_error("unexpected present mode!"); } } if (swapChainMinImageCount < capabilities.minImageCount) { swapChainMinImageCount = capabilities.minImageCount; } else if (capabilities.maxImageCount != 0 && swapChainMinImageCount > capabilities.maxImageCount) { swapChainMinImageCount = capabilities.maxImageCount; } } void VulkanGame::createSwapChain() { VkSurfaceCapabilitiesKHR capabilities = VulkanUtils::querySwapChainCapabilities(physicalDevice, vulkanSurface); swapChainExtent = VulkanUtils::chooseSwapExtent(capabilities, gui->getWindowWidth(), gui->getWindowHeight()); VkSwapchainCreateInfoKHR createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; createInfo.surface = vulkanSurface; createInfo.minImageCount = swapChainMinImageCount; createInfo.imageFormat = swapChainSurfaceFormat.format; createInfo.imageColorSpace = swapChainSurfaceFormat.colorSpace; createInfo.imageExtent = swapChainExtent; createInfo.imageArrayLayers = 1; createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; // TODO: Maybe save this result so I don't have to recalculate it every time QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, vulkanSurface); uint32_t queueFamilyIndices[] = { indices.graphicsFamily.value(), indices.presentFamily.value() }; if (indices.graphicsFamily != indices.presentFamily) { createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT; createInfo.queueFamilyIndexCount = 2; createInfo.pQueueFamilyIndices = queueFamilyIndices; } else { createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; createInfo.queueFamilyIndexCount = 0; createInfo.pQueueFamilyIndices = nullptr; } createInfo.preTransform = capabilities.currentTransform; createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; createInfo.presentMode = swapChainPresentMode; createInfo.clipped = VK_TRUE; createInfo.oldSwapchain = VK_NULL_HANDLE; if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) { throw runtime_error("failed to create swap chain!"); } if (vkGetSwapchainImagesKHR(device, swapChain, &swapChainImageCount, nullptr) != VK_SUCCESS) { throw runtime_error("failed to get swap chain image count!"); } swapChainImages.resize(swapChainImageCount); if (vkGetSwapchainImagesKHR(device, swapChain, &swapChainImageCount, swapChainImages.data()) != VK_SUCCESS) { throw runtime_error("failed to get swap chain images!"); } } void VulkanGame::createImageViews() { swapChainImageViews.resize(swapChainImageCount); for (uint32_t i = 0; i < swapChainImageViews.size(); i++) { swapChainImageViews[i] = VulkanUtils::createImageView(device, swapChainImages[i], swapChainSurfaceFormat.format, VK_IMAGE_ASPECT_COLOR_BIT); } } void VulkanGame::createResourceCommandPool() { QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, vulkanSurface); VkCommandPoolCreateInfo poolInfo = {}; poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; poolInfo.queueFamilyIndex = indices.graphicsFamily.value(); poolInfo.flags = 0; if (vkCreateCommandPool(device, &poolInfo, nullptr, &resourceCommandPool) != VK_SUCCESS) { throw runtime_error("failed to create resource command pool!"); } } void VulkanGame::createImageResources() { VulkanUtils::createDepthImage(device, physicalDevice, resourceCommandPool, findDepthFormat(), swapChainExtent, depthImage, graphicsQueue); createTextureSampler(); // TODO: Move all images/textures somewhere into the assets folder VulkanUtils::createVulkanImageFromFile(device, physicalDevice, resourceCommandPool, "textures/texture.jpg", floorTextureImage, graphicsQueue); floorTextureImageDescriptor = {}; floorTextureImageDescriptor.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; floorTextureImageDescriptor.imageView = floorTextureImage.imageView; floorTextureImageDescriptor.sampler = textureSampler; } VkFormat VulkanGame::findDepthFormat() { return VulkanUtils::findSupportedFormat( physicalDevice, { VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D32_SFLOAT, VK_FORMAT_D24_UNORM_S8_UINT }, VK_IMAGE_TILING_OPTIMAL, VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT ); } void VulkanGame::createRenderPass() { VkAttachmentDescription colorAttachment = {}; colorAttachment.format = swapChainSurfaceFormat.format; colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; // Set to VK_ATTACHMENT_LOAD_OP_DONT_CARE to disable clearing colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; VkAttachmentReference colorAttachmentRef = {}; colorAttachmentRef.attachment = 0; colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; VkAttachmentDescription depthAttachment = {}; depthAttachment.format = findDepthFormat(); depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT; depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; depthAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; VkAttachmentReference depthAttachmentRef = {}; depthAttachmentRef.attachment = 1; depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; VkSubpassDescription subpass = {}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &colorAttachmentRef; //subpass.pDepthStencilAttachment = &depthAttachmentRef; VkSubpassDependency dependency = {}; dependency.srcSubpass = VK_SUBPASS_EXTERNAL; dependency.dstSubpass = 0; dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dependency.srcAccessMask = 0; dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; array attachments = { colorAttachment, depthAttachment }; VkRenderPassCreateInfo renderPassInfo = {}; renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; renderPassInfo.attachmentCount = static_cast(attachments.size()); renderPassInfo.pAttachments = attachments.data(); renderPassInfo.subpassCount = 1; renderPassInfo.pSubpasses = &subpass; renderPassInfo.dependencyCount = 1; renderPassInfo.pDependencies = &dependency; if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) { throw runtime_error("failed to create render pass!"); } // We do not create a pipeline by default as this is also used by examples' main.cpp, // but secondary viewport in multi-viewport mode may want to create one with: //ImGui_ImplVulkan_CreatePipeline(device, g_Allocator, VK_NULL_HANDLE, g_MainWindowData.RenderPass, VK_SAMPLE_COUNT_1_BIT, &g_MainWindowData.Pipeline); } void VulkanGame::createCommandPools() { commandPools.resize(swapChainImageCount); QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, vulkanSurface); for (size_t i = 0; i < swapChainImageCount; i++) { VkCommandPoolCreateInfo poolInfo = {}; poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; poolInfo.queueFamilyIndex = indices.graphicsFamily.value(); poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPools[i]) != VK_SUCCESS) { throw runtime_error("failed to create graphics command pool!"); } } } void VulkanGame::createTextureSampler() { VkSamplerCreateInfo samplerInfo = {}; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; samplerInfo.magFilter = VK_FILTER_LINEAR; samplerInfo.minFilter = VK_FILTER_LINEAR; samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; samplerInfo.anisotropyEnable = VK_TRUE; samplerInfo.maxAnisotropy = 16; samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; samplerInfo.unnormalizedCoordinates = VK_FALSE; samplerInfo.compareEnable = VK_FALSE; samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS; samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; samplerInfo.mipLodBias = 0.0f; samplerInfo.minLod = 0.0f; samplerInfo.maxLod = 0.0f; VKUTIL_CHECK_RESULT(vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler), "failed to create texture sampler!"); } void VulkanGame::createFramebuffers() { swapChainFramebuffers.resize(swapChainImageCount); VkFramebufferCreateInfo framebufferInfo = {}; framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; framebufferInfo.renderPass = renderPass; framebufferInfo.width = swapChainExtent.width; framebufferInfo.height = swapChainExtent.height; framebufferInfo.layers = 1; for (size_t i = 0; i < swapChainImageCount; i++) { array attachments = { swapChainImageViews[i], depthImage.imageView }; framebufferInfo.attachmentCount = static_cast(attachments.size()); framebufferInfo.pAttachments = attachments.data(); if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) { throw runtime_error("failed to create framebuffer!"); } } } void VulkanGame::createCommandBuffers() { commandBuffers.resize(swapChainImageCount); for (size_t i = 0; i < swapChainImageCount; i++) { VkCommandBufferAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocInfo.commandPool = commandPools[i]; allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocInfo.commandBufferCount = 1; if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffers[i]) != VK_SUCCESS) { throw runtime_error("failed to allocate command buffer!"); } } } void VulkanGame::createSyncObjects() { imageAcquiredSemaphores.resize(swapChainImageCount); renderCompleteSemaphores.resize(swapChainImageCount); inFlightFences.resize(swapChainImageCount); VkSemaphoreCreateInfo semaphoreInfo = {}; semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; VkFenceCreateInfo fenceInfo = {}; fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; for (size_t i = 0; i < swapChainImageCount; i++) { VKUTIL_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAcquiredSemaphores[i]), "failed to create image acquired sempahore for a frame!"); VKUTIL_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderCompleteSemaphores[i]), "failed to create render complete sempahore for a frame!"); VKUTIL_CHECK_RESULT(vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]), "failed to create fence for a frame!"); } } void VulkanGame::initImGuiOverlay() { vector pool_sizes { { VK_DESCRIPTOR_TYPE_SAMPLER, 1000 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 }, { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000 }, { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000 }, { VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000 }, { VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000 }, { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000 }, { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000 }, { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000 }, { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000 }, { VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000 } }; VkDescriptorPoolCreateInfo pool_info = {}; pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; pool_info.maxSets = 1000 * pool_sizes.size(); pool_info.poolSizeCount = static_cast(pool_sizes.size()); pool_info.pPoolSizes = pool_sizes.data(); VKUTIL_CHECK_RESULT(vkCreateDescriptorPool(device, &pool_info, nullptr, &imguiDescriptorPool), "failed to create IMGUI descriptor pool!"); // TODO: Do this in one place and save it instead of redoing it every time I need a queue family index QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, vulkanSurface); // Setup Dear ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); //io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls //io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls // Setup Dear ImGui style ImGui::StyleColorsDark(); //ImGui::StyleColorsClassic(); // Setup Platform/Renderer bindings ImGui_ImplSDL2_InitForVulkan(window); ImGui_ImplVulkan_InitInfo init_info = {}; init_info.Instance = instance; init_info.PhysicalDevice = physicalDevice; init_info.Device = device; init_info.QueueFamily = indices.graphicsFamily.value(); init_info.Queue = graphicsQueue; init_info.DescriptorPool = imguiDescriptorPool; init_info.Allocator = nullptr; init_info.MinImageCount = swapChainMinImageCount; init_info.ImageCount = swapChainImageCount; init_info.CheckVkResultFn = check_imgui_vk_result; ImGui_ImplVulkan_Init(&init_info, renderPass); // Load Fonts // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them. // - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple. // - If the file cannot be loaded, the function will return NULL. Please handle those errors in your application (e.g. use an assertion, or display an error and quit). // - The fonts will be rasterized at a given size (w/ oversampling) and stored into a texture when calling ImFontAtlas::Build()/GetTexDataAsXXXX(), which ImGui_ImplXXXX_NewFrame below will call. // - Read 'docs/FONTS.md' for more instructions and details. // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ ! //io.Fonts->AddFontDefault(); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf", 16.0f); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf", 15.0f); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf", 16.0f); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/ProggyTiny.ttf", 10.0f); //ImFont* font = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf", 18.0f, NULL, io.Fonts->GetGlyphRangesJapanese()); //assert(font != NULL); // Upload Fonts VkCommandBuffer commandBuffer = VulkanUtils::beginSingleTimeCommands(device, resourceCommandPool); ImGui_ImplVulkan_CreateFontsTexture(commandBuffer); VulkanUtils::endSingleTimeCommands(device, resourceCommandPool, commandBuffer, graphicsQueue); ImGui_ImplVulkan_DestroyFontUploadObjects(); } void VulkanGame::cleanupImGuiOverlay() { ImGui_ImplVulkan_Shutdown(); ImGui_ImplSDL2_Shutdown(); ImGui::DestroyContext(); vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr); } void VulkanGame::createBufferSet(VkDeviceSize bufferSize, VkBufferUsageFlags usages, VkMemoryPropertyFlags properties, BufferSet& set) { set.usages = usages; set.properties = properties; set.buffers.resize(swapChainImageCount); set.memory.resize(swapChainImageCount); set.infoSet.resize(swapChainImageCount); for (size_t i = 0; i < swapChainImageCount; i++) { VulkanUtils::createBuffer(device, physicalDevice, bufferSize, usages, properties, set.buffers[i], set.memory[i]); set.infoSet[i].buffer = set.buffers[i]; set.infoSet[i].offset = 0; // This is the offset from the start of the buffer, so always 0 for now set.infoSet[i].range = bufferSize; // Size of the update starting from offset, or VK_WHOLE_SIZE } } void VulkanGame::resizeBufferSet(BufferSet& set, VkDeviceSize newSize, VkCommandPool commandPool, VkQueue graphicsQueue, bool copyData) { for (size_t i = 0; i < set.buffers.size(); i++) { VkBuffer newBuffer; VkDeviceMemory newMemory; VulkanUtils::createBuffer(device, physicalDevice, newSize, set.usages, set.properties, newBuffer, newMemory); if (copyData) { VulkanUtils::copyBuffer(device, commandPool, set.buffers[i], newBuffer, 0, 0, set.infoSet[i].range, graphicsQueue); } vkDestroyBuffer(device, set.buffers[i], nullptr); vkFreeMemory(device, set.memory[i], nullptr); set.buffers[i] = newBuffer; set.memory[i] = newMemory; set.infoSet[i].buffer = set.buffers[i]; set.infoSet[i].offset = 0; // This is the offset from the start of the buffer, so always 0 for now set.infoSet[i].range = newSize; // Size of the update starting from offset, or VK_WHOLE_SIZE } } void VulkanGame::renderFrame(ImDrawData* draw_data) { VkResult result = vkAcquireNextImageKHR(device, swapChain, numeric_limits::max(), imageAcquiredSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); if (result == VK_SUBOPTIMAL_KHR) { shouldRecreateSwapChain = true; } else if (result == VK_ERROR_OUT_OF_DATE_KHR) { shouldRecreateSwapChain = true; return; } else { VKUTIL_CHECK_RESULT(result, "failed to acquire swap chain image!"); } VKUTIL_CHECK_RESULT( vkWaitForFences(device, 1, &inFlightFences[imageIndex], VK_TRUE, numeric_limits::max()), "failed waiting for fence!"); VKUTIL_CHECK_RESULT(vkResetFences(device, 1, &inFlightFences[imageIndex]), "failed to reset fence!"); VKUTIL_CHECK_RESULT(vkResetCommandPool(device, commandPools[imageIndex], 0), "failed to reset command pool!"); VkCommandBufferBeginInfo beginInfo = {}; beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; VKUTIL_CHECK_RESULT(vkBeginCommandBuffer(commandBuffers[imageIndex], &beginInfo), "failed to begin recording command buffer!"); VkRenderPassBeginInfo renderPassInfo = {}; renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; renderPassInfo.renderPass = renderPass; renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex]; renderPassInfo.renderArea.offset = { 0, 0 }; renderPassInfo.renderArea.extent = swapChainExtent; array clearValues = {}; clearValues[0].color = { { 0.0f, 0.0f, 0.0f, 1.0f } }; clearValues[1].depthStencil = { 1.0f, 0 }; renderPassInfo.clearValueCount = static_cast(clearValues.size()); renderPassInfo.pClearValues = clearValues.data(); vkCmdBeginRenderPass(commandBuffers[imageIndex], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); // TODO: Find a more elegant, per-screen solution for this if (currentRenderScreenFn == &VulkanGame::renderGameScreen) { modelPipeline.createRenderCommands(commandBuffers[imageIndex], imageIndex, { 0 }); } ImGui_ImplVulkan_RenderDrawData(draw_data, commandBuffers[imageIndex]); vkCmdEndRenderPass(commandBuffers[imageIndex]); VKUTIL_CHECK_RESULT(vkEndCommandBuffer(commandBuffers[imageIndex]), "failed to record command buffer!"); VkSemaphore waitSemaphores[] = { imageAcquiredSemaphores[currentFrame] }; VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT }; VkSemaphore signalSemaphores[] = { renderCompleteSemaphores[currentFrame] }; VkSubmitInfo submitInfo = {}; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.waitSemaphoreCount = 1; submitInfo.pWaitSemaphores = waitSemaphores; submitInfo.pWaitDstStageMask = waitStages; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &commandBuffers[imageIndex]; submitInfo.signalSemaphoreCount = 1; submitInfo.pSignalSemaphores = signalSemaphores; VKUTIL_CHECK_RESULT(vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[imageIndex]), "failed to submit draw command buffer!"); } void VulkanGame::presentFrame() { VkSemaphore signalSemaphores[] = { renderCompleteSemaphores[currentFrame] }; VkPresentInfoKHR presentInfo = {}; presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; presentInfo.waitSemaphoreCount = 1; presentInfo.pWaitSemaphores = signalSemaphores; presentInfo.swapchainCount = 1; presentInfo.pSwapchains = &swapChain; presentInfo.pImageIndices = &imageIndex; presentInfo.pResults = nullptr; VkResult result = vkQueuePresentKHR(presentQueue, &presentInfo); if (result == VK_SUBOPTIMAL_KHR) { shouldRecreateSwapChain = true; } else if (result == VK_ERROR_OUT_OF_DATE_KHR) { shouldRecreateSwapChain = true; return; } else { VKUTIL_CHECK_RESULT(result, "failed to present swap chain image!"); } currentFrame = (currentFrame + 1) % swapChainImageCount; } void VulkanGame::recreateSwapChain() { if (vkDeviceWaitIdle(device) != VK_SUCCESS) { throw runtime_error("failed to wait for device!"); } cleanupSwapChain(); createSwapChain(); createImageViews(); // The depth buffer does need to be recreated with the swap chain since its dimensions depend on the window size // and resizing the window is a common reason to recreate the swapchain VulkanUtils::createDepthImage(device, physicalDevice, resourceCommandPool, findDepthFormat(), swapChainExtent, depthImage, graphicsQueue); createRenderPass(); createCommandPools(); createFramebuffers(); createCommandBuffers(); createSyncObjects(); createBufferSet(uniforms_modelPipeline.memorySize(), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, uniformBuffers_modelPipeline); uniforms_modelPipeline.map(uniformBuffers_modelPipeline.memory, device); createBufferSet(objects_modelPipeline.memorySize(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, objectBuffers_modelPipeline); objects_modelPipeline.map(objectBuffers_modelPipeline.memory, device); modelPipeline.updateRenderPass(renderPass); modelPipeline.createPipeline("shaders/model-vert.spv", "shaders/model-frag.spv"); modelPipeline.createDescriptorPool(swapChainImages.size()); modelPipeline.createDescriptorSets(swapChainImages.size()); imageIndex = 0; } void VulkanGame::cleanupSwapChain() { VulkanUtils::destroyVulkanImage(device, depthImage); for (VkFramebuffer framebuffer : swapChainFramebuffers) { vkDestroyFramebuffer(device, framebuffer, nullptr); } for (uint32_t i = 0; i < swapChainImageCount; i++) { vkFreeCommandBuffers(device, commandPools[i], 1, &commandBuffers[i]); vkDestroyCommandPool(device, commandPools[i], nullptr); } modelPipeline.cleanup(); uniforms_modelPipeline.unmap(uniformBuffers_modelPipeline.memory, device); for (size_t i = 0; i < uniformBuffers_modelPipeline.buffers.size(); i++) { vkDestroyBuffer(device, uniformBuffers_modelPipeline.buffers[i], nullptr); vkFreeMemory(device, uniformBuffers_modelPipeline.memory[i], nullptr); } objects_modelPipeline.unmap(objectBuffers_modelPipeline.memory, device); for (size_t i = 0; i < objectBuffers_modelPipeline.buffers.size(); i++) { vkDestroyBuffer(device, objectBuffers_modelPipeline.buffers[i], nullptr); vkFreeMemory(device, objectBuffers_modelPipeline.memory[i], nullptr); } for (uint32_t i = 0; i < swapChainImageCount; i++) { vkDestroySemaphore(device, imageAcquiredSemaphores[i], nullptr); vkDestroySemaphore(device, renderCompleteSemaphores[i], nullptr); vkDestroyFence(device, inFlightFences[i], nullptr); } vkDestroyRenderPass(device, renderPass, nullptr); for (VkImageView imageView : swapChainImageViews) { vkDestroyImageView(device, imageView, nullptr); } vkDestroySwapchainKHR(device, swapChain, nullptr); } void VulkanGame::renderMainScreen(int width, int height) { { int padding = 4; ImGui::SetNextWindowPos(vec2(-padding, -padding), ImGuiCond_Once); ImGui::SetNextWindowSize(vec2(width + 2 * padding, height + 2 * padding), ImGuiCond_Always); ImGui::Begin("WndMain", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); ButtonImGui btn("New Game"); ImGui::InvisibleButton("", vec2(10, height / 6)); if (btn.draw((width - btn.getWidth()) / 2)) { goToScreen(&VulkanGame::renderGameScreen); } ButtonImGui btn2("Quit"); ImGui::InvisibleButton("", vec2(10, 15)); if (btn2.draw((width - btn2.getWidth()) / 2)) { quitGame(); } ImGui::End(); } } void VulkanGame::renderGameScreen(int width, int height) { { ImGui::SetNextWindowSize(vec2(130, 65), ImGuiCond_Once); ImGui::SetNextWindowPos(vec2(10, 50), ImGuiCond_Once); ImGui::Begin("WndStats", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); //ImGui::Text(ImGui::GetIO().Framerate); renderGuiValueList(valueLists["stats value list"]); ImGui::End(); } { ImGui::SetNextWindowSize(vec2(250, 35), ImGuiCond_Once); ImGui::SetNextWindowPos(vec2(width - 260, 10), ImGuiCond_Always); ImGui::Begin("WndMenubar", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); ImGui::InvisibleButton("", vec2(155, 18)); ImGui::SameLine(); if (ImGui::Button("Main Menu")) { goToScreen(&VulkanGame::renderMainScreen); } ImGui::End(); } { ImGui::SetNextWindowSize(vec2(200, 200), ImGuiCond_Once); ImGui::SetNextWindowPos(vec2(width - 210, 60), ImGuiCond_Always); ImGui::Begin("WndDebug", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); renderGuiValueList(valueLists["debug value list"]); ImGui::End(); } } void VulkanGame::initGuiValueLists(map>& valueLists) { valueLists["stats value list"] = vector(); valueLists["debug value list"] = vector(); } // TODO: Probably turn this into a UI widget class void VulkanGame::renderGuiValueList(vector& values) { float maxWidth = 0.0f; float cursorStartPos = ImGui::GetCursorPosX(); for (vector::iterator it = values.begin(); it != values.end(); it++) { float textWidth = ImGui::CalcTextSize(it->label.c_str()).x; if (maxWidth < textWidth) maxWidth = textWidth; } stringstream ss; // TODO: Possibly implement this based on gui/ui-value.hpp instead and use templates // to keep track of the type. This should make it a bit easier to use and maintain // Also, implement this in a way that's agnostic to the UI renderer. for (vector::iterator it = values.begin(); it != values.end(); it++) { ss.str(""); ss.clear(); switch (it->type) { case UIVALUE_INT: ss << it->label << ": " << *(unsigned int*)it->value; break; case UIVALUE_DOUBLE: ss << it->label << ": " << *(double*)it->value; break; } float textWidth = ImGui::CalcTextSize(it->label.c_str()).x; ImGui::SetCursorPosX(cursorStartPos + maxWidth - textWidth); //ImGui::Text("%s", ss.str().c_str()); ImGui::Text("%s: %.1f", it->label.c_str(), *(float*)it->value); } } void VulkanGame::goToScreen(void (VulkanGame::* renderScreenFn)(int width, int height)) { currentRenderScreenFn = renderScreenFn; // TODO: Maybe just set shouldRecreateSwapChain to true instead. Check this render loop logic // to make sure there'd be no issues //recreateSwapChain(); } void VulkanGame::quitGame() { done = true; }