#include "core/VulkanWrapper.hpp" #include #include #include "GLFW.hpp" #include "core/VulkanBase.hpp" namespace Vulkan = Core::Vulkan; using Vulkan::CommandBuffer; using Vulkan::Queue; using Vulkan::SwapchainImages; #define WRAPPER_DESTRUCT(Type, ...) \ using Core::Vulkan::Type; \ Type::~Type() { \ if(device != VK_NULL_HANDLE) { \ vkDestroy##Type##__VA_ARGS__(device, handle, nullptr); \ } \ } WRAPPER_DESTRUCT(ImageView) WRAPPER_DESTRUCT(Framebuffer) WRAPPER_DESTRUCT(Semaphore) WRAPPER_DESTRUCT(Fence) WRAPPER_DESTRUCT(Swapchain, KHR) WRAPPER_DESTRUCT(ShaderModule) WRAPPER_DESTRUCT(PipelineLayout) WRAPPER_DESTRUCT(Pipeline) WRAPPER_DESTRUCT(CommandPool) WRAPPER_DESTRUCT(RenderPass) bool ImageView::init(VkDevice d, VkImage image, VkFormat format) { VkImageViewCreateInfo info = { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = image, .viewType = VK_IMAGE_VIEW_TYPE_2D, .format = format, .subresourceRange = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1}}; VK_CHECK_TRUE(vkCreateImageView(d, &info, nullptr, &handle)); device = d; return false; } bool Framebuffer::init( const ImageView& iv, VkRenderPass rp, u32 width, u32 height) { VkFramebufferCreateInfo info = { .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, .renderPass = rp, .attachmentCount = 1, .pAttachments = iv, .width = width, .height = height, .layers = 1}; VK_CHECK_TRUE(vkCreateFramebuffer(iv.device, &info, nullptr, &handle)); device = iv.device; return false; } bool Semaphore::init(VkDevice d) { VkSemaphoreCreateInfo info = { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; VK_CHECK_TRUE(vkCreateSemaphore(d, &info, nullptr, &handle)); device = d; return false; } bool Fence::init(VkDevice d) { VkFenceCreateInfo info = { .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .flags = VK_FENCE_CREATE_SIGNALED_BIT}; VK_CHECK_TRUE(vkCreateFence(d, &info, nullptr, &handle)); device = d; return false; } void Fence::reset() { VK_CHECK_VOID(vkResetFences(device, 1, &handle)); } void Fence::waitFor(u64 timeout) { VK_CHECK_VOID(vkWaitForFences(device, 1, &handle, true, timeout)); } static u32 getSwapImageCount(const VkSurfaceCapabilitiesKHR* caps) { u32 c = caps->minImageCount + 1; // according to VkSurfaceCapabilitiesKHR doc: // maxImageCount is 0 when there is no strict limit if(caps->maxImageCount != 0) { return Core::min(c, caps->maxImageCount); } return c; } bool Swapchain::init(Data& d) { VkSurfaceCapabilitiesKHR caps = {0}; if(d.base.getSurfaceCapabilities(caps)) { return true; } VkSwapchainCreateInfoKHR ci = { .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, .surface = d.base, .minImageCount = getSwapImageCount(&caps), .imageFormat = d.surfaceFormat.format, .imageColorSpace = d.surfaceFormat.colorSpace, .imageExtent = d.size, .imageArrayLayers = 1, .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, .imageSharingMode = d.sharingMode, .queueFamilyIndexCount = static_cast(d.queueFamilies.getLength()), .pQueueFamilyIndices = &d.queueFamilies[0], .preTransform = caps.currentTransform, .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, .presentMode = d.presentMode, .clipped = VK_TRUE, .oldSwapchain = VK_NULL_HANDLE}; VK_CHECK_TRUE(vkCreateSwapchainKHR(d.base, &ci, nullptr, &handle)); device = d.base; return false; } bool Swapchain::nextImage(u32& imageIndex, Semaphore& s, u64 timeout) { VK_CHECK_TRUE(vkAcquireNextImageKHR( device, handle, timeout, s, VK_NULL_HANDLE, &imageIndex)); return false; } bool ShaderModule::init(VkDevice d, const char* path) { List f; if(Core::readFile(f, path)) { return true; } size_t l = f.getLength() - 1; if((l % 4) != 0) { VK_REPORT_ERROR("Shader size is not a multiple of 4"); return true; } VkShaderModuleCreateInfo info = { .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = l, .pCode = reinterpret_cast(static_cast(&f[0]))}; VK_CHECK_TRUE(vkCreateShaderModule(d, &info, nullptr, &handle)); device = d; return false; } bool PipelineLayout::init(VkDevice d) { VkPipelineLayoutCreateInfo info = { .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; VK_CHECK_TRUE(vkCreatePipelineLayout(d, &info, nullptr, &handle)); device = d; return false; } bool SwapchainImages::init(const Swapchain& s, VkFormat format) { u32 c = 0; VK_CHECK_TRUE(vkGetSwapchainImagesKHR(s.device, s.handle, &c, nullptr)); images.resize(c); imageViews.resize(c); VK_CHECK_TRUE(vkGetSwapchainImagesKHR(s.device, s.handle, &c, &images[0])); for(u32 x = 0; x < c; x++) { if(imageViews[x].init(s.device, images[x], format)) { return true; } } return false; } bool RenderPass::init(VkDevice d, VkFormat format) { VkAttachmentDescription c = { .format = format, .samples = VK_SAMPLE_COUNT_1_BIT, .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, .finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR}; VkAttachmentReference ca = { .attachment = 0, .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; VkSubpassDescription subpass = { .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, .colorAttachmentCount = 1, .pColorAttachments = &ca}; VkSubpassDependency dependency = { .srcSubpass = VK_SUBPASS_EXTERNAL, .dstSubpass = 0, .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, .srcAccessMask = 0, .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, }; VkRenderPassCreateInfo info = { .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, .attachmentCount = 1, .pAttachments = &c, .subpassCount = 1, .pSubpasses = &subpass, .dependencyCount = 1, .pDependencies = &dependency}; VK_CHECK_TRUE(vkCreateRenderPass(d, &info, nullptr, &handle)); device = d; return false; } Pipeline::Pipeline() : viewport{.maxDepth = 1.0f}, scissor{} { } void Pipeline::updateSize(u32 width, u32 height) { viewport.width = static_cast(width); viewport.height = static_cast(height); scissor.extent.width = width; scissor.extent.height = height; } bool Pipeline::init(PipelineLayout& pl, RenderPass& rp) { ShaderModule vertexShaderModule; ShaderModule fragmentShaderModule; if(vertexShaderModule.init(pl.device, "shaders/vertex.spv") || fragmentShaderModule.init(pl.device, "shaders/fragment.spv")) { return true; } VkPipelineShaderStageCreateInfo stages[2] = { {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_VERTEX_BIT, .module = vertexShaderModule, .pName = "main"}, {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_FRAGMENT_BIT, .module = fragmentShaderModule, .pName = "main"}}; VkPipelineVertexInputStateCreateInfo vertexInputState = { .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; VkPipelineInputAssemblyStateCreateInfo inputAssemblyState = { .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, .primitiveRestartEnable = false}; VkPipelineViewportStateCreateInfo viewportState = { .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, .viewportCount = 1, .pViewports = &viewport, .scissorCount = 1, .pScissors = &scissor}; VkPipelineRasterizationStateCreateInfo rasterizationState = { .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, .depthClampEnable = false, .rasterizerDiscardEnable = false, .polygonMode = VK_POLYGON_MODE_FILL, .cullMode = VK_CULL_MODE_BACK_BIT, .frontFace = VK_FRONT_FACE_CLOCKWISE, .depthBiasEnable = false, .depthBiasConstantFactor = 0.0f, .depthBiasClamp = 0.0f, .depthBiasSlopeFactor = 0.0f, .lineWidth = 1.0f}; VkPipelineMultisampleStateCreateInfo multisampleState = { .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT, .sampleShadingEnable = false, .minSampleShading = 1.0f, .pSampleMask = nullptr, .alphaToCoverageEnable = false, .alphaToOneEnable = false}; VkPipelineColorBlendAttachmentState colorBlendAttachmentState = { .blendEnable = false, .srcColorBlendFactor = VK_BLEND_FACTOR_ONE, .dstColorBlendFactor = VK_BLEND_FACTOR_ZERO, .colorBlendOp = VK_BLEND_OP_ADD, .srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE, .dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO, .alphaBlendOp = VK_BLEND_OP_ADD, .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT}; VkPipelineColorBlendStateCreateInfo colorBlendState = { .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, .logicOpEnable = false, .logicOp = VK_LOGIC_OP_COPY, .attachmentCount = 1, .pAttachments = &colorBlendAttachmentState, .blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}}; Core::Array dynamicStates = { {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}}; VkPipelineDynamicStateCreateInfo dynamicState = { .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, .dynamicStateCount = dynamicStates.getLength(), .pDynamicStates = dynamicStates.begin()}; VkGraphicsPipelineCreateInfo info = { .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .stageCount = 2, .pStages = stages, .pVertexInputState = &vertexInputState, .pInputAssemblyState = &inputAssemblyState, .pTessellationState = nullptr, .pViewportState = &viewportState, .pRasterizationState = &rasterizationState, .pMultisampleState = &multisampleState, .pDepthStencilState = nullptr, .pColorBlendState = &colorBlendState, .pDynamicState = &dynamicState, .layout = pl, .renderPass = rp, .subpass = 0, .basePipelineHandle = VK_NULL_HANDLE, .basePipelineIndex = -1}; VK_CHECK_TRUE(vkCreateGraphicsPipelines( pl.device, VK_NULL_HANDLE, 1, &info, nullptr, &handle)); device = pl.device; return false; } bool CommandPool::init(VkDevice d, u32 queueFamily) { VkCommandPoolCreateInfo info = { .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, .queueFamilyIndex = queueFamily}; VK_CHECK_TRUE(vkCreateCommandPool(d, &info, nullptr, &handle)); device = d; return false; } bool CommandBuffer::init(CommandPool& cp) { VkCommandBufferAllocateInfo info = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .commandPool = cp, .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = 1}; VK_CHECK_TRUE(vkAllocateCommandBuffers(cp.device, &info, &handle)); device = cp.device; return false; } void CommandBuffer::reset() { VK_CHECK_VOID(vkResetCommandBuffer(handle, 0)); } void CommandBuffer::begin() { VkCommandBufferBeginInfo info = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; VK_CHECK_VOID(vkBeginCommandBuffer(handle, &info)); } void CommandBuffer::end() { VK_CHECK_VOID(vkEndCommandBuffer(handle)); } void CommandBuffer::beginRenderPass( RenderPass& rp, Framebuffer& f, const VkExtent2D& size) { VkClearValue v = {.color = {.float32 = {0.0f, 0.0f, 0.0f, 1.0f}}}; VkRenderPassBeginInfo info = { .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .renderPass = rp, .framebuffer = f, .renderArea = {.offset = {0, 0}, .extent = size}, .clearValueCount = 1, .pClearValues = &v}; vkCmdBeginRenderPass(handle, &info, VK_SUBPASS_CONTENTS_INLINE); } void CommandBuffer::endRenderPass() { vkCmdEndRenderPass(handle); } void CommandBuffer::bindPipeline(Pipeline& p) { vkCmdBindPipeline(handle, VK_PIPELINE_BIND_POINT_GRAPHICS, p); vkCmdSetViewport(handle, 0, 1, &p.viewport); vkCmdSetScissor(handle, 0, 1, &p.scissor); } void CommandBuffer::draw(u32 vertices) { vkCmdDraw(handle, vertices, 1, 0, 0); } bool Queue::init(VkDevice d, u32 queueFamily) { vkGetDeviceQueue(d, queueFamily, 0, &handle); return handle == VK_NULL_HANDLE; } void Queue::submit( CommandBuffer& cb, Fence& f, Semaphore& wait, Semaphore& signal, VkPipelineStageFlags flags) { VkSubmitInfo info = { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .waitSemaphoreCount = 1, .pWaitSemaphores = wait, .pWaitDstStageMask = &flags, .commandBufferCount = 1, .pCommandBuffers = cb, .signalSemaphoreCount = 1, .pSignalSemaphores = signal}; VK_CHECK_VOID(vkQueueSubmit(handle, 1, &info, f)); } void Queue::present(Semaphore& signal, Swapchain& s, u32 index) { VkPresentInfoKHR info = { .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, .waitSemaphoreCount = 1, .pWaitSemaphores = signal, .swapchainCount = 1, .pSwapchains = s, .pImageIndices = &index}; VK_CHECK_VOID(vkQueuePresentKHR(handle, &info)); } struct VulkanDummy { Core::Vulkan::Base base{}; struct BaseData { u32 graphicsFamily = 0; u32 presentFamily = 0; VkSurfaceFormatKHR surfaceFormat{}; VkPresentModeKHR presentMode{}; }; BaseData baseData{}; Queue graphicsQueue{}; Queue presentQueue{}; VkExtent2D swapchainSize{}; Swapchain swapchain{}; SwapchainImages images{}; PipelineLayout pipelineLayout{}; RenderPass renderPass{}; Pipeline pipeline{}; Core::List framebuffers{}; CommandPool commandPool{}; size_t currentFrame = 0; bool shouldWait = false; struct Frame { CommandBuffer commandBuffer{}; Semaphore imageAvailableSemaphore{}; Semaphore renderFinishedSemaphore{}; Fence inFlightFence{}; }; Core::Array frames{}; static int getSurfaceFormatPoints(const VkSurfaceFormatKHR& sf) { if(sf.format == VK_FORMAT_B8G8R8A8_UNORM && sf.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { return 10; } return 1; } static int getSurfacePresentModePoints(VkPresentModeKHR m) { if(m == VK_PRESENT_MODE_MAILBOX_KHR) { return 5; } else if(m == VK_PRESENT_MODE_FIFO_KHR) { return 2; } return 0; } bool getSwapchainSize(VkExtent2D* size) { VkSurfaceCapabilitiesKHR c = {0}; if(base.getSurfaceCapabilities(c)) { return true; } if(c.currentExtent.width != 0xFFFF'FFFFu && c.currentExtent.height != 0xFFFF'FFFFu) { *size = c.currentExtent; LOG_INFO("Swapchain size: #x#", size->width, size->height); return false; } int w = 0; int h = 0; glfwGetFramebufferSize(Core::Window::get(), &w, &h); if(w <= 0 || h <= 0) { LOG_ERROR("Could not get framebuffer size"); return true; } LOG_INFO("Framebuffer size: #x#", w, h); size->width = Core::clamp( static_cast(w), c.minImageExtent.width, c.maxImageExtent.width); size->height = Core::clamp( static_cast(h), c.minImageExtent.height, c.maxImageExtent.height); LOG_INFO("Swapchain size: #x#", size->width, size->height); return false; } bool initSwapchain() { Vulkan::Swapchain::Data d = { .base = base, .surfaceFormat = baseData.surfaceFormat, .presentMode = baseData.presentMode}; if(getSwapchainSize(&d.size)) { LOG_ERROR("Could not retrieve any swapchain size"); return true; } swapchainSize = d.size; d.queueFamilies.add(baseData.graphicsFamily); if(baseData.graphicsFamily != baseData.presentFamily) { d.queueFamilies.add(baseData.presentFamily); d.sharingMode = VK_SHARING_MODE_CONCURRENT; } else { d.sharingMode = VK_SHARING_MODE_EXCLUSIVE; } return swapchain.init(d); } bool initDevice() { bool same = baseData.graphicsFamily == baseData.presentFamily; Core::List data; data.add(baseData.graphicsFamily, 1.0f); if(!same) { data.add(baseData.presentFamily, 1.0f); }; Core::List extensions; extensions.add(VK_KHR_SWAPCHAIN_EXTENSION_NAME); if(base.initDevice(data, extensions)) { return true; } else if(graphicsQueue.init(base, baseData.graphicsFamily)) { LOG_ERROR("Cannot get device graphics queue"); return true; } else if(presentQueue.init(base, baseData.presentFamily)) { LOG_ERROR("Cannot get device present queue"); return true; } return false; } static int getDevicePoints(Vulkan::Base& b, BaseData& d) { int points = 0; VkPhysicalDeviceProperties p; b.getPhysicalDeviceProperties(p); LOG_INFO("Checking '#'", p.deviceName); switch(p.deviceType) { case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: points += 100; break; case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: points += 50; break; case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: points += 20; break; default: break; } d.graphicsFamily = b.findQueueFamily(VK_QUEUE_GRAPHICS_BIT); if(d.graphicsFamily == Vulkan::INVALID_QUEUE_FAMILY) { LOG_INFO("> ... has no graphics family"); points = -1; } d.presentFamily = b.findSurfaceQueueFamily(); if(d.presentFamily == Vulkan::INVALID_QUEUE_FAMILY) { LOG_INFO("> ... has no present family"); points = -1; } if(!b.hasExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME)) { LOG_INFO("> ... has no swapchain support"); points = -1; } if(b.findSurfaceFormat(d.surfaceFormat, getSurfaceFormatPoints)) { LOG_INFO("> ... has no matching surface format"); points = -1; } else { points += getSurfaceFormatPoints(d.surfaceFormat); } if(b.findSurfacePresentMode( d.presentMode, getSurfacePresentModePoints)) { LOG_INFO("> ... has no matching present mode"); points = -1; } else { points += getSurfacePresentModePoints(d.presentMode); } LOG_INFO("> Final points: #", points); return points; } bool initPhysicalDevice() { LOG_INFO("Searching for physical devices ..."); if(base.findPhysicalDevice(baseData, getDevicePoints)) { LOG_ERROR("No matching physical device was found"); return true; } VkPhysicalDeviceProperties p; base.getPhysicalDeviceProperties(p); LOG_INFO("Best Device: #", p.deviceName); return false; } bool initSwapchainImages() { if(images.init(swapchain, baseData.surfaceFormat.format)) { LOG_ERROR("Could not get swapchain images"); return true; } LOG_INFO("Found # images", images.images.getLength()); return false; } bool initPipeline() { pipeline.updateSize(swapchainSize.width, swapchainSize.height); return pipeline.init(pipelineLayout, renderPass); } bool initFramebuffers() { framebuffers.resize(images.imageViews.getLength()); size_t i = 0; for(Framebuffer& f : framebuffers) { if(f.init( images.imageViews[i++], renderPass, swapchainSize.width, swapchainSize.height)) { return true; } } return false; } void fillCommandBuffer(CommandBuffer& cb, u32 index) { cb.begin(); cb.beginRenderPass(renderPass, framebuffers[index], swapchainSize); cb.bindPipeline(pipeline); cb.draw(6); cb.endRenderPass(); cb.end(); } bool initFrames() { for(Frame& f : frames) { if(f.commandBuffer.init(commandPool) || f.imageAvailableSemaphore.init(base) || f.renderFinishedSemaphore.init(base) || f.inFlightFence.init(base)) { return true; } } return false; } bool init() { return base.init() || initPhysicalDevice() || initDevice() || initSwapchain() || initSwapchainImages() || pipelineLayout.init(base) || renderPass.init(base, baseData.surfaceFormat.format) || initPipeline() || initFramebuffers() || commandPool.init(base, baseData.graphicsFamily) || initFrames(); } bool render() { if(shouldWait) { return false; } Frame& f = frames[currentFrame]; f.inFlightFence.waitFor(); f.inFlightFence.reset(); u32 imageIndex = 0; if(swapchain.nextImage(imageIndex, f.imageAvailableSemaphore)) { return true; } f.commandBuffer.reset(); fillCommandBuffer(f.commandBuffer, imageIndex); graphicsQueue.submit( f.commandBuffer, f.inFlightFence, f.imageAvailableSemaphore, f.renderFinishedSemaphore, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT); presentQueue.present(f.renderFinishedSemaphore, swapchain, imageIndex); currentFrame = (currentFrame + 1) % frames.getLength(); return false; } }; static Core::UniquePointer dummy; bool Vulkan::init() { dummy = new VulkanDummy(); return dummy->init(); } void Vulkan::render() { if(dummy->render()) { dummy->shouldWait = true; } } void Vulkan::destroy() { dummy->base.waitForIdle(); dummy = nullptr; }