#include #include #include #include #include #include #include #include #include #include #include #include #ifdef __INTELLISENSE__ #include #else import vulkan_hpp; #endif #include #if defined(__ANDROID__) #include #include #endif #include #if defined(__ANDROID__) #define PLATFORM_ANDROID 1 #else #define PLATFORM_DESKTOP 1 #endif // Include tinygltf instead of tinyobjloader // TINYGLTF_IMPLEMENTATION is already defined in the command line #define STB_IMAGE_WRITE_IMPLEMENTATION #include // Include KTX library for texture loading #include #if PLATFORM_ANDROID #include #include #include #include // Declare and implement app_dummy function from native_app_glue extern "C" void app_dummy() { // This is a dummy function that does nothing // It's used to prevent the linker from stripping out the native_app_glue code } // Define AAssetManager type for Android typedef AAssetManager AssetManagerType; #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "VulkanTutorial", __VA_ARGS__)) #define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "VulkanTutorial", __VA_ARGS__)) #define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "VulkanTutorial", __VA_ARGS__)) #else // Define AAssetManager type for non-Android platforms typedef void AssetManagerType; // Desktop-specific includes #define GLFW_INCLUDE_VULKAN #include // Define logging macros for Desktop #define LOGI(...) printf(__VA_ARGS__); printf("\n") #define LOGW(...) printf(__VA_ARGS__); printf("\n") #define LOGE(...) fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n") #endif #define GLM_FORCE_RADIANS #define GLM_FORCE_DEPTH_ZERO_TO_ONE #define GLM_ENABLE_EXPERIMENTAL #define GLM_FORCE_CXX11 #include #include #include constexpr uint32_t WIDTH = 800; constexpr uint32_t HEIGHT = 600; constexpr uint64_t FenceTimeout = 100000000; // Update paths to use glTF model and KTX2 texture const std::string MODEL_PATH = "models/viking_room.glb"; const std::string TEXTURE_PATH = "textures/viking_room.ktx2"; constexpr int MAX_FRAMES_IN_FLIGHT = 2; // Define VpProfileProperties structure for Android only #if PLATFORM_ANDROID #ifndef VP_PROFILE_PROPERTIES_DEFINED #define VP_PROFILE_PROPERTIES_DEFINED struct VpProfileProperties { char name[256]; uint32_t specVersion; }; #endif #endif // Define Vulkan Profile constants #ifndef VP_KHR_ROADMAP_2022_NAME #define VP_KHR_ROADMAP_2022_NAME "VP_KHR_roadmap_2022" #endif #ifndef VP_KHR_ROADMAP_2022_SPEC_VERSION #define VP_KHR_ROADMAP_2022_SPEC_VERSION 1 #endif struct AppInfo { bool profileSupported = false; VpProfileProperties profile; }; #if PLATFORM_ANDROID void android_main(android_app* app); struct AndroidAppState { ANativeWindow* nativeWindow = nullptr; bool initialized = false; android_app* app = nullptr; }; #endif #ifdef NDEBUG constexpr bool enableValidationLayers = false; #else constexpr bool enableValidationLayers = true; #endif struct Vertex { glm::vec3 pos; glm::vec3 color; glm::vec2 texCoord; static vk::VertexInputBindingDescription getBindingDescription() { return { 0, sizeof(Vertex), vk::VertexInputRate::eVertex }; } static std::array getAttributeDescriptions() { return { vk::VertexInputAttributeDescription( 0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos) ), vk::VertexInputAttributeDescription( 1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color) ), vk::VertexInputAttributeDescription( 2, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, texCoord) ) }; } bool operator==(const Vertex& other) const { return pos == other.pos && color == other.color && texCoord == other.texCoord; } }; template<> struct std::hash { size_t operator()(Vertex const& vertex) const noexcept { return ((hash()(vertex.pos) ^ (hash()(vertex.color) << 1)) >> 1) ^ (hash()(vertex.texCoord) << 1); } }; struct UniformBufferObject { alignas(16) glm::mat4 model; alignas(16) glm::mat4 view; alignas(16) glm::mat4 proj; }; class VulkanApplication { public: #if PLATFORM_ANDROID void run(android_app* app) { androidAppState.nativeWindow = app->window; androidAppState.app = app; app->userData = &androidAppState; app->onAppCmd = handleAppCommand; // Note: onInputEvent is no longer a member of android_app in the current NDK version // Input events are now handled differently int events; android_poll_source* source; while (app->destroyRequested == 0) { while (ALooper_pollOnce(androidAppState.initialized ? 0 : -1, nullptr, &events, (void**)&source) >= 0) { if (source != nullptr) { source->process(app, source); } } if (androidAppState.initialized && androidAppState.nativeWindow != nullptr) { drawFrame(); } } if (androidAppState.initialized) { device.waitIdle(); } } #else void run() { initWindow(); initVulkan(); mainLoop(); cleanup(); } #endif private: #if PLATFORM_ANDROID AndroidAppState androidAppState; static void handleAppCommand(android_app* app, int32_t cmd) { auto* appState = static_cast(app->userData); switch (cmd) { case APP_CMD_INIT_WINDOW: if (app->window != nullptr) { appState->nativeWindow = app->window; // We can't cast AndroidAppState to VulkanApplication directly // Instead, we need to access the VulkanApplication instance through a global variable // or another mechanism. For now, we'll just set the initialized flag. appState->initialized = true; } break; case APP_CMD_TERM_WINDOW: appState->nativeWindow = nullptr; break; default: break; } } static int32_t handleInputEvent(android_app* app, AInputEvent* event) { if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) { float x = AMotionEvent_getX(event, 0); float y = AMotionEvent_getY(event, 0); LOGI("Touch at: %f, %f", x, y); return 1; } return 0; } #else GLFWwindow* window = nullptr; #endif AppInfo appInfo; vk::raii::Context context; vk::raii::Instance instance = nullptr; vk::raii::DebugUtilsMessengerEXT debugMessenger = nullptr; vk::raii::SurfaceKHR surface = nullptr; vk::raii::PhysicalDevice physicalDevice = nullptr; vk::raii::Device device = nullptr; uint32_t queueIndex = ~0; vk::raii::Queue queue = nullptr; vk::raii::SwapchainKHR swapChain = nullptr; std::vector swapChainImages; vk::Format swapChainImageFormat = vk::Format::eUndefined; vk::Extent2D swapChainExtent; std::vector swapChainImageViews; vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; vk::raii::PipelineLayout pipelineLayout = nullptr; vk::raii::Pipeline graphicsPipeline = nullptr; vk::raii::Image depthImage = nullptr; vk::raii::DeviceMemory depthImageMemory = nullptr; vk::raii::ImageView depthImageView = nullptr; vk::raii::Image textureImage = nullptr; vk::raii::DeviceMemory textureImageMemory = nullptr; vk::raii::ImageView textureImageView = nullptr; vk::raii::Sampler textureSampler = nullptr; vk::Format textureImageFormat = vk::Format::eUndefined; std::vector vertices; std::vector indices; vk::raii::Buffer vertexBuffer = nullptr; vk::raii::DeviceMemory vertexBufferMemory = nullptr; vk::raii::Buffer indexBuffer = nullptr; vk::raii::DeviceMemory indexBufferMemory = nullptr; std::vector uniformBuffers; std::vector uniformBuffersMemory; std::vector uniformBuffersMapped; vk::raii::DescriptorPool descriptorPool = nullptr; std::vector descriptorSets; vk::raii::CommandPool commandPool = nullptr; std::vector commandBuffers; std::vector presentCompleteSemaphore; std::vector renderFinishedSemaphore; std::vector inFlightFences; uint32_t currentFrame = 0; bool framebufferResized = false; std::vector requiredDeviceExtension = { vk::KHRSwapchainExtensionName, vk::KHRCreateRenderpass2ExtensionName }; #if PLATFORM_DESKTOP void initWindow() { glfwInit(); glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); glfwSetWindowUserPointer(window, this); glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); } static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { auto app = static_cast(glfwGetWindowUserPointer(window)); app->framebufferResized = true; } #endif public: void initVulkan() { createInstance(); setupDebugMessenger(); createSurface(); pickPhysicalDevice(); createLogicalDevice(); createSwapChain(); createImageViews(); createDescriptorSetLayout(); createGraphicsPipeline(); createCommandPool(); createDepthResources(); createTextureImage(); createTextureImageView(); createTextureSampler(); loadModel(); createVertexBuffer(); createIndexBuffer(); createUniformBuffers(); createDescriptorPool(); createDescriptorSets(); createCommandBuffers(); createSyncObjects(); } private: #if PLATFORM_DESKTOP void mainLoop() { while (!glfwWindowShouldClose(window)) { glfwPollEvents(); drawFrame(); } device.waitIdle(); } #endif void cleanupSwapChain() { swapChainImageViews.clear(); swapChain = nullptr; } #if PLATFORM_DESKTOP void cleanup() const { glfwDestroyWindow(window); glfwTerminate(); } #endif void recreateSwapChain() { #if PLATFORM_DESKTOP int width = 0, height = 0; glfwGetFramebufferSize(window, &width, &height); while (width == 0 || height == 0) { glfwGetFramebufferSize(window, &width, &height); glfwWaitEvents(); } #endif device.waitIdle(); cleanupSwapChain(); createSwapChain(); createImageViews(); createDepthResources(); } void createInstance() { constexpr vk::ApplicationInfo appInfo{ .pApplicationName = "Hello Triangle", .applicationVersion = VK_MAKE_VERSION(1, 0, 0), .pEngineName = "No Engine", .engineVersion = VK_MAKE_VERSION(1, 0, 0), .apiVersion = VK_API_VERSION_1_3 }; auto extensions = getRequiredExtensions(); vk::InstanceCreateInfo createInfo{ .pApplicationInfo = &appInfo, .enabledExtensionCount = static_cast(extensions.size()), .ppEnabledExtensionNames = extensions.data() }; instance = vk::raii::Instance(context, createInfo); LOGI("Vulkan instance created"); } void setupDebugMessenger() { // Debug messenger setup is disabled for now to avoid compatibility issues // This is a simplified approach to get the code compiling if (!enableValidationLayers) return; LOGI("Debug messenger setup skipped for compatibility"); } void createSurface() { #if PLATFORM_DESKTOP VkSurfaceKHR _surface; if (glfwCreateWindowSurface(*instance, window, nullptr, &_surface) != VK_SUCCESS) { throw std::runtime_error("failed to create window surface!"); } surface = vk::raii::SurfaceKHR(instance, _surface); #else VkSurfaceKHR _surface; VkAndroidSurfaceCreateInfoKHR createInfo{ .sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR, .window = androidAppState.nativeWindow }; if (vkCreateAndroidSurfaceKHR(*instance, &createInfo, nullptr, &_surface) != VK_SUCCESS) { throw std::runtime_error("failed to create Android surface!"); } surface = vk::raii::SurfaceKHR(instance, _surface); #endif } void pickPhysicalDevice() { std::vector devices = instance.enumeratePhysicalDevices(); const auto devIter = std::ranges::find_if( devices, [&](auto const& device) { // Check if the device supports the Vulkan 1.3 API version bool supportsVulkan1_3 = device.getProperties().apiVersion >= VK_API_VERSION_1_3; // Check if any of the queue families support graphics operations auto queueFamilies = device.getQueueFamilyProperties(); bool supportsGraphics = std::ranges::any_of(queueFamilies, [](auto const& qfp) { return !!(qfp.queueFlags & vk::QueueFlagBits::eGraphics); }); // Check if all required device extensions are available auto availableDeviceExtensions = device.enumerateDeviceExtensionProperties(); bool supportsAllRequiredExtensions = std::ranges::all_of(requiredDeviceExtension, [&availableDeviceExtensions](auto const& requiredDeviceExtension) { return std::ranges::any_of(availableDeviceExtensions, [requiredDeviceExtension](auto const& availableDeviceExtension) { return strcmp(availableDeviceExtension.extensionName, requiredDeviceExtension) == 0; }); }); auto features = device.template getFeatures2(); bool supportsRequiredFeatures = features.template get().dynamicRendering && features.template get().extendedDynamicState; return supportsVulkan1_3 && supportsGraphics && supportsAllRequiredExtensions && supportsRequiredFeatures; }); if (devIter != devices.end()) { physicalDevice = *devIter; // Check for Vulkan profile support VpProfileProperties profileProperties; #if PLATFORM_ANDROID strcpy(profileProperties.name, VP_KHR_ROADMAP_2022_NAME); #else strcpy(profileProperties.profileName, VP_KHR_ROADMAP_2022_NAME); #endif profileProperties.specVersion = VP_KHR_ROADMAP_2022_SPEC_VERSION; VkBool32 supported = VK_FALSE; bool result = false; #if PLATFORM_ANDROID // Create a vp::ProfileDesc from our VpProfileProperties vp::ProfileDesc profileDesc = { profileProperties.name, profileProperties.specVersion }; // Use vp::GetProfileSupport for Android result = vp::GetProfileSupport( *physicalDevice, // Pass the physical device directly &profileDesc, // Pass the profile description &supported // Output parameter for support status ); #else // Use vpGetPhysicalDeviceProfileSupport for Desktop VkResult vk_result = vpGetPhysicalDeviceProfileSupport( *instance, *physicalDevice, &profileProperties, &supported ); result = vk_result == static_cast(vk::Result::eSuccess); #endif const char* name = nullptr; #ifdef PLATFORM_ANDROID name = profileProperties.name; #else name = profileProperties.profileName; #endif if (result && supported == VK_TRUE) { appInfo.profileSupported = true; appInfo.profile = profileProperties; LOGI("Device supports Vulkan profile: %s", name); } else { LOGI("Device does not support Vulkan profile: %s", name); } } else { throw std::runtime_error("failed to find a suitable GPU!"); } } void createLogicalDevice() { std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); // get the first index into queueFamilyProperties which supports both graphics and present for (uint32_t qfpIndex = 0; qfpIndex < queueFamilyProperties.size(); qfpIndex++) { if ((queueFamilyProperties[qfpIndex].queueFlags & vk::QueueFlagBits::eGraphics) && physicalDevice.getSurfaceSupportKHR(qfpIndex, *surface)) { // found a queue family that supports both graphics and present queueIndex = qfpIndex; break; } } if (queueIndex == ~0) { throw std::runtime_error("Could not find a queue for graphics and present -> terminating"); } // query for Vulkan 1.3 features auto features = physicalDevice.getFeatures2(); vk::PhysicalDeviceVulkan13Features vulkan13Features; vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT extendedDynamicStateFeatures; vulkan13Features.dynamicRendering = vk::True; vulkan13Features.synchronization2 = vk::True; extendedDynamicStateFeatures.extendedDynamicState = vk::True; vulkan13Features.pNext = &extendedDynamicStateFeatures; features.pNext = &vulkan13Features; // create a Device float queuePriority = 0.0f; vk::DeviceQueueCreateInfo deviceQueueCreateInfo { .queueFamilyIndex = queueIndex, .queueCount = 1, .pQueuePriorities = &queuePriority }; vk::DeviceCreateInfo deviceCreateInfo{ .pNext = &features, .queueCreateInfoCount = 1, .pQueueCreateInfos = &deviceQueueCreateInfo, .enabledExtensionCount = static_cast(requiredDeviceExtension.size()), .ppEnabledExtensionNames = requiredDeviceExtension.data() }; // Create the device with the appropriate features device = vk::raii::Device(physicalDevice, deviceCreateInfo); queue = vk::raii::Queue(device, queueIndex, 0); } void createSwapChain() { auto surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(*surface); swapChainImageFormat = chooseSwapSurfaceFormat(physicalDevice.getSurfaceFormatsKHR(*surface)); swapChainExtent = chooseSwapExtent(surfaceCapabilities); auto minImageCount = std::max(3u, surfaceCapabilities.minImageCount); minImageCount = (surfaceCapabilities.maxImageCount > 0 && minImageCount > surfaceCapabilities.maxImageCount) ? surfaceCapabilities.maxImageCount : minImageCount; vk::SwapchainCreateInfoKHR swapChainCreateInfo{ .surface = *surface, .minImageCount = minImageCount, .imageFormat = swapChainImageFormat, .imageColorSpace = vk::ColorSpaceKHR::eSrgbNonlinear, .imageExtent = swapChainExtent, .imageArrayLayers =1, .imageUsage = vk::ImageUsageFlagBits::eColorAttachment, .imageSharingMode = vk::SharingMode::eExclusive, .preTransform = surfaceCapabilities.currentTransform, .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque, .presentMode = chooseSwapPresentMode(physicalDevice.getSurfacePresentModesKHR(*surface)), .clipped = true }; swapChain = vk::raii::SwapchainKHR(device, swapChainCreateInfo); swapChainImages = swapChain.getImages(); } void createImageViews() { vk::ImageViewCreateInfo imageViewCreateInfo{ .viewType = vk::ImageViewType::e2D, .format = swapChainImageFormat, .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } }; for ( auto image : swapChainImages ) { imageViewCreateInfo.image = image; swapChainImageViews.emplace_back( device, imageViewCreateInfo ); } } void createDescriptorSetLayout() { std::array bindings = { vk::DescriptorSetLayoutBinding( 0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex, nullptr), vk::DescriptorSetLayoutBinding( 1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, nullptr) }; vk::DescriptorSetLayoutCreateInfo layoutInfo{ .bindingCount = static_cast(bindings.size()), .pBindings = bindings.data() }; descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); } void createGraphicsPipeline() { vk::raii::ShaderModule shaderModule = createShaderModule(this->readFile("shaders/slang.spv")); vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eVertex, .module = *shaderModule, .pName = "vertMain" }; vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ .stage = vk::ShaderStageFlagBits::eFragment, .module = *shaderModule, .pName = "fragMain" }; vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; auto bindingDescription = Vertex::getBindingDescription(); auto attributeDescriptions = Vertex::getAttributeDescriptions(); vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ .vertexBindingDescriptionCount = 1, .pVertexBindingDescriptions = &bindingDescription, .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), .pVertexAttributeDescriptions = attributeDescriptions.data() }; vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ .topology = vk::PrimitiveTopology::eTriangleList, .primitiveRestartEnable = vk::False }; vk::PipelineViewportStateCreateInfo viewportState{ .viewportCount = 1, .scissorCount = 1 }; vk::PipelineRasterizationStateCreateInfo rasterizer{ .depthClampEnable = vk::False, .rasterizerDiscardEnable = vk::False, .polygonMode = vk::PolygonMode::eFill, .cullMode = vk::CullModeFlagBits::eBack, // Re-enabled culling for better performance .frontFace = vk::FrontFace::eClockwise, // Keeping Clockwise for glTF .depthBiasEnable = vk::False }; rasterizer.lineWidth = 1.0f; vk::PipelineMultisampleStateCreateInfo multisampling{ .rasterizationSamples = vk::SampleCountFlagBits::e1, .sampleShadingEnable = vk::False }; vk::PipelineDepthStencilStateCreateInfo depthStencil{ .depthTestEnable = vk::True, .depthWriteEnable = vk::True, .depthCompareOp = vk::CompareOp::eLess, .depthBoundsTestEnable = vk::False, .stencilTestEnable = vk::False }; vk::PipelineColorBlendAttachmentState colorBlendAttachment; colorBlendAttachment.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; colorBlendAttachment.blendEnable = vk::False; vk::PipelineColorBlendStateCreateInfo colorBlending{ .logicOpEnable = vk::False, .logicOp = vk::LogicOp::eCopy, .attachmentCount = 1, .pAttachments = &colorBlendAttachment }; std::vector dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor }; vk::PipelineDynamicStateCreateInfo dynamicState{ .dynamicStateCount = static_cast(dynamicStates.size()), .pDynamicStates = dynamicStates.data() }; vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ .setLayoutCount = 1, .pSetLayouts = &*descriptorSetLayout, .pushConstantRangeCount = 0 }; pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{ .colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainImageFormat }; vk::GraphicsPipelineCreateInfo pipelineInfo{ .pNext = &pipelineRenderingCreateInfo, .stageCount = 2, .pStages = shaderStages, .pVertexInputState = &vertexInputInfo, .pInputAssemblyState = &inputAssembly, .pViewportState = &viewportState, .pRasterizationState = &rasterizer, .pMultisampleState = &multisampling, .pDepthStencilState = &depthStencil, .pColorBlendState = &colorBlending, .pDynamicState = &dynamicState, .layout = *pipelineLayout, .renderPass = nullptr }; graphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); } void createCommandPool() { vk::CommandPoolCreateInfo poolInfo{ .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, .queueFamilyIndex = queueIndex }; commandPool = vk::raii::CommandPool(device, poolInfo); } void createDepthResources() { vk::Format depthFormat = findDepthFormat(); createImage(swapChainExtent.width, swapChainExtent.height, depthFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eDepthStencilAttachment, vk::MemoryPropertyFlagBits::eDeviceLocal, depthImage, depthImageMemory); depthImageView = createImageView(depthImage, depthFormat, vk::ImageAspectFlagBits::eDepth); } vk::Format findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features) const { for (const auto format : candidates) { vk::FormatProperties props = physicalDevice.getFormatProperties(format); if (tiling == vk::ImageTiling::eLinear && (props.linearTilingFeatures & features) == features) { return format; } if (tiling == vk::ImageTiling::eOptimal && (props.optimalTilingFeatures & features) == features) { return format; } } throw std::runtime_error("failed to find supported format!"); } [[nodiscard]] vk::Format findDepthFormat() const { return findSupportedFormat( {vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint}, vk::ImageTiling::eOptimal, vk::FormatFeatureFlagBits::eDepthStencilAttachment ); } static bool hasStencilComponent(vk::Format format) { return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint; } void createTextureImage() { // Load KTX2 texture instead of using stb_image ktxTexture* kTexture; KTX_error_code result = ktxTexture_CreateFromNamedFile( TEXTURE_PATH.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &kTexture); if (result != KTX_SUCCESS) { throw std::runtime_error("failed to load ktx texture image!"); } // Get texture dimensions and data uint32_t texWidth = kTexture->baseWidth; uint32_t texHeight = kTexture->baseHeight; ktx_size_t imageSize = ktxTexture_GetImageSize(kTexture, 0); ktx_uint8_t* ktxTextureData = ktxTexture_GetData(kTexture); vk::raii::Buffer stagingBuffer({}); vk::raii::DeviceMemory stagingBufferMemory({}); createBuffer(imageSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); void* data = stagingBufferMemory.mapMemory(0, imageSize); memcpy(data, ktxTextureData, imageSize); stagingBufferMemory.unmapMemory(); // Determine the Vulkan format from KTX format vk::Format textureFormat; // Check if the KTX texture has a format if (kTexture->classId == ktxTexture2_c) { // For KTX2 files, we can get the format directly auto* ktx2 = reinterpret_cast(kTexture); textureFormat = static_cast(ktx2->vkFormat); if (textureFormat == vk::Format::eUndefined) { // If the format is undefined, fall back to a reasonable default textureFormat = vk::Format::eR8G8B8A8Unorm; } } else { // For KTX1 files or if we can't determine the format, use a reasonable default textureFormat = vk::Format::eR8G8B8A8Unorm; } textureImageFormat = textureFormat; createImage(texWidth, texHeight, textureFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal, textureImage, textureImageMemory); transitionImageLayout(textureImage, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); copyBufferToImage(stagingBuffer, textureImage, texWidth, texHeight); transitionImageLayout(textureImage, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal); ktxTexture_Destroy(kTexture); } void createTextureImageView() { textureImageView = createImageView(textureImage, textureImageFormat, vk::ImageAspectFlagBits::eColor); } void createTextureSampler() { vk::PhysicalDeviceProperties properties = physicalDevice.getProperties(); vk::SamplerCreateInfo samplerInfo{ .magFilter = vk::Filter::eLinear, .minFilter = vk::Filter::eLinear, .mipmapMode = vk::SamplerMipmapMode::eLinear, .addressModeU = vk::SamplerAddressMode::eRepeat, .addressModeV = vk::SamplerAddressMode::eRepeat, .addressModeW = vk::SamplerAddressMode::eRepeat, .mipLodBias = 0.0f, .anisotropyEnable = vk::True, .maxAnisotropy = properties.limits.maxSamplerAnisotropy, .compareEnable = vk::False, .compareOp = vk::CompareOp::eAlways }; textureSampler = vk::raii::Sampler(device, samplerInfo); } vk::raii::ImageView createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags) { vk::ImageViewCreateInfo viewInfo{ .image = *image, .viewType = vk::ImageViewType::e2D, .format = format, .subresourceRange = { aspectFlags, 0, 1, 0, 1 } }; return vk::raii::ImageView(device, viewInfo); } void createImage(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties, vk::raii::Image& image, vk::raii::DeviceMemory& imageMemory) { vk::ImageCreateInfo imageInfo{ .imageType = vk::ImageType::e2D, .format = format, .extent = {width, height, 1}, .mipLevels = 1, .arrayLayers = 1, .samples = vk::SampleCountFlagBits::e1, .tiling = tiling, .usage = usage, .sharingMode = vk::SharingMode::eExclusive, .initialLayout = vk::ImageLayout::eUndefined }; image = vk::raii::Image(device, imageInfo); vk::MemoryRequirements memRequirements = image.getMemoryRequirements(); vk::MemoryAllocateInfo allocInfo{ .allocationSize = memRequirements.size, .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) }; imageMemory = vk::raii::DeviceMemory(device, allocInfo); image.bindMemory(*imageMemory, 0); } void transitionImageLayout(const vk::raii::Image& image, vk::ImageLayout oldLayout, vk::ImageLayout newLayout) { auto commandBuffer = beginSingleTimeCommands(); vk::ImageMemoryBarrier barrier{ .oldLayout = oldLayout, .newLayout = newLayout, .image = *image, .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } }; vk::PipelineStageFlags sourceStage; vk::PipelineStageFlags destinationStage; if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { barrier.srcAccessMask = {}; barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite; sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; destinationStage = vk::PipelineStageFlagBits::eTransfer; } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; sourceStage = vk::PipelineStageFlagBits::eTransfer; destinationStage = vk::PipelineStageFlagBits::eFragmentShader; } else { throw std::invalid_argument("unsupported layout transition!"); } commandBuffer->pipelineBarrier( sourceStage, destinationStage, {}, {}, nullptr, barrier ); endSingleTimeCommands(*commandBuffer); } void copyBufferToImage(const vk::raii::Buffer& buffer, vk::raii::Image& image, uint32_t width, uint32_t height) { std::unique_ptr commandBuffer = beginSingleTimeCommands(); vk::BufferImageCopy region{ .bufferOffset = 0, .bufferRowLength = 0, .bufferImageHeight = 0, .imageSubresource = { vk::ImageAspectFlagBits::eColor, 0, 0, 1 }, .imageOffset = {0, 0, 0}, .imageExtent = {width, height, 1} }; commandBuffer->copyBufferToImage(*buffer, *image, vk::ImageLayout::eTransferDstOptimal, {region}); endSingleTimeCommands(*commandBuffer); } void loadModel() { // Use tinygltf to load the model instead of tinyobjloader tinygltf::Model model; tinygltf::TinyGLTF loader; std::string err; std::string warn; bool ret = loader.LoadBinaryFromFile(&model, &err, &warn, MODEL_PATH); if (!warn.empty()) { std::cout << "glTF warning: " << warn << std::endl; } if (!err.empty()) { std::cout << "glTF error: " << err << std::endl; } if (!ret) { throw std::runtime_error("Failed to load glTF model"); } vertices.clear(); indices.clear(); // Process all meshes in the model for (const auto& mesh : model.meshes) { for (const auto& primitive : mesh.primitives) { // Get indices const tinygltf::Accessor& indexAccessor = model.accessors[primitive.indices]; const tinygltf::BufferView& indexBufferView = model.bufferViews[indexAccessor.bufferView]; const tinygltf::Buffer& indexBuffer = model.buffers[indexBufferView.buffer]; // Get vertex positions const tinygltf::Accessor& posAccessor = model.accessors[primitive.attributes.at("POSITION")]; const tinygltf::BufferView& posBufferView = model.bufferViews[posAccessor.bufferView]; const tinygltf::Buffer& posBuffer = model.buffers[posBufferView.buffer]; // Get texture coordinates if available bool hasTexCoords = primitive.attributes.find("TEXCOORD_0") != primitive.attributes.end(); const tinygltf::Accessor* texCoordAccessor = nullptr; const tinygltf::BufferView* texCoordBufferView = nullptr; const tinygltf::Buffer* texCoordBuffer = nullptr; if (hasTexCoords) { texCoordAccessor = &model.accessors[primitive.attributes.at("TEXCOORD_0")]; texCoordBufferView = &model.bufferViews[texCoordAccessor->bufferView]; texCoordBuffer = &model.buffers[texCoordBufferView->buffer]; } uint32_t baseVertex = static_cast(vertices.size()); for (size_t i = 0; i < posAccessor.count; i++) { Vertex vertex{}; const float* pos = reinterpret_cast(&posBuffer.data[posBufferView.byteOffset + posAccessor.byteOffset + i * 12]); // glTF uses a right-handed coordinate system with Y-up // Vulkan uses a right-handed coordinate system with Y-down // We need to flip the Y coordinate vertex.pos = {pos[0], -pos[1], pos[2]}; if (hasTexCoords) { const float* texCoord = reinterpret_cast(&texCoordBuffer->data[texCoordBufferView->byteOffset + texCoordAccessor->byteOffset + i * 8]); vertex.texCoord = {texCoord[0], texCoord[1]}; } else { vertex.texCoord = {0.0f, 0.0f}; } vertex.color = {1.0f, 1.0f, 1.0f}; vertices.push_back(vertex); } const unsigned char* indexData = &indexBuffer.data[indexBufferView.byteOffset + indexAccessor.byteOffset]; size_t indexCount = indexAccessor.count; size_t indexStride = 0; // Determine index stride based on component type if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { indexStride = sizeof(uint16_t); } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { indexStride = sizeof(uint32_t); } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { indexStride = sizeof(uint8_t); } else { throw std::runtime_error("Unsupported index component type"); } indices.reserve(indices.size() + indexCount); for (size_t i = 0; i < indexCount; i++) { uint32_t index = 0; if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { index = *reinterpret_cast(indexData + i * indexStride); } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { index = *reinterpret_cast(indexData + i * indexStride); } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { index = *reinterpret_cast(indexData + i * indexStride); } indices.push_back(baseVertex + index); } } } } void createVertexBuffer() { vk::DeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); vk::raii::Buffer stagingBuffer({}); vk::raii::DeviceMemory stagingBufferMemory({}); createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); void* dataStaging = stagingBufferMemory.mapMemory(0, bufferSize); memcpy(dataStaging, vertices.data(), bufferSize); stagingBufferMemory.unmapMemory(); createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal, vertexBuffer, vertexBufferMemory); copyBuffer(stagingBuffer, vertexBuffer, bufferSize); } void createIndexBuffer() { vk::DeviceSize bufferSize = sizeof(indices[0]) * indices.size(); vk::raii::Buffer stagingBuffer({}); vk::raii::DeviceMemory stagingBufferMemory({}); createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); void* data = stagingBufferMemory.mapMemory(0, bufferSize); memcpy(data, indices.data(), bufferSize); stagingBufferMemory.unmapMemory(); createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal, indexBuffer, indexBufferMemory); copyBuffer(stagingBuffer, indexBuffer, bufferSize); } void createUniformBuffers() { uniformBuffers.clear(); uniformBuffersMemory.clear(); uniformBuffersMapped.clear(); for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { vk::DeviceSize bufferSize = sizeof(UniformBufferObject); vk::raii::Buffer buffer({}); vk::raii::DeviceMemory bufferMem({}); createBuffer(bufferSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, buffer, bufferMem); uniformBuffers.emplace_back(std::move(buffer)); uniformBuffersMemory.emplace_back(std::move(bufferMem)); uniformBuffersMapped.emplace_back( uniformBuffersMemory[i].mapMemory(0, bufferSize)); } } void createDescriptorPool() { std::array poolSize { vk::DescriptorPoolSize( vk::DescriptorType::eUniformBuffer, MAX_FRAMES_IN_FLIGHT), vk::DescriptorPoolSize( vk::DescriptorType::eCombinedImageSampler, MAX_FRAMES_IN_FLIGHT) }; vk::DescriptorPoolCreateInfo poolInfo{ .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, .maxSets = MAX_FRAMES_IN_FLIGHT, .poolSizeCount = static_cast(poolSize.size()), .pPoolSizes = poolSize.data() }; descriptorPool = vk::raii::DescriptorPool(device, poolInfo); } void createDescriptorSets() { std::vector layouts(MAX_FRAMES_IN_FLIGHT, *descriptorSetLayout); vk::DescriptorSetAllocateInfo allocInfo{ .descriptorPool = *descriptorPool, .descriptorSetCount = static_cast(layouts.size()), .pSetLayouts = layouts.data() }; descriptorSets.clear(); descriptorSets = device.allocateDescriptorSets(allocInfo); for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { vk::DescriptorBufferInfo bufferInfo{ .buffer = *uniformBuffers[i], .offset = 0, .range = sizeof(UniformBufferObject) }; vk::DescriptorImageInfo imageInfo{ .sampler = *textureSampler, .imageView = *textureImageView, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal }; std::array descriptorWrites{ vk::WriteDescriptorSet{ .dstSet = *descriptorSets[i], .dstBinding = 0, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo }, vk::WriteDescriptorSet{ .dstSet = *descriptorSets[i], .dstBinding = 1, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &imageInfo } }; device.updateDescriptorSets(descriptorWrites, {}); } } void createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties, vk::raii::Buffer& buffer, vk::raii::DeviceMemory& bufferMemory) { vk::BufferCreateInfo bufferInfo{ .size = size, .usage = usage, .sharingMode = vk::SharingMode::eExclusive }; buffer = vk::raii::Buffer(device, bufferInfo); vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); vk::MemoryAllocateInfo allocInfo{ .allocationSize = memRequirements.size, .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) }; bufferMemory = vk::raii::DeviceMemory(device, allocInfo); buffer.bindMemory(*bufferMemory, 0); } std::unique_ptr beginSingleTimeCommands() { vk::CommandBufferAllocateInfo allocInfo{ .commandPool = *commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 }; std::unique_ptr commandBuffer = std::make_unique(std::move(vk::raii::CommandBuffers(device, allocInfo).front())); vk::CommandBufferBeginInfo beginInfo{ .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit }; commandBuffer->begin(beginInfo); return commandBuffer; } void endSingleTimeCommands(const vk::raii::CommandBuffer& commandBuffer) const { commandBuffer.end(); vk::SubmitInfo submitInfo{ .commandBufferCount = 1, .pCommandBuffers = &*commandBuffer }; queue.submit(submitInfo, nullptr); queue.waitIdle(); } void copyBuffer(vk::raii::Buffer & srcBuffer, vk::raii::Buffer & dstBuffer, vk::DeviceSize size) { vk::CommandBufferAllocateInfo allocInfo{ .commandPool = *commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 }; vk::raii::CommandBuffer commandCopyBuffer = std::move(device.allocateCommandBuffers(allocInfo).front()); commandCopyBuffer.begin(vk::CommandBufferBeginInfo{ .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit }); commandCopyBuffer.copyBuffer(*srcBuffer, *dstBuffer, vk::BufferCopy{ .size = size }); commandCopyBuffer.end(); queue.submit(vk::SubmitInfo{ .commandBufferCount = 1, .pCommandBuffers = &*commandCopyBuffer }, nullptr); queue.waitIdle(); } uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) { vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { return i; } } throw std::runtime_error("failed to find suitable memory type!"); } void createCommandBuffers() { commandBuffers.clear(); vk::CommandBufferAllocateInfo allocInfo{ .commandPool = *commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = MAX_FRAMES_IN_FLIGHT }; commandBuffers = vk::raii::CommandBuffers(device, allocInfo); } void recordCommandBuffer(uint32_t imageIndex) { commandBuffers[currentFrame].begin({}); transition_image_layout( imageIndex, vk::ImageLayout::eUndefined, vk::ImageLayout::eColorAttachmentOptimal, {}, vk::AccessFlagBits2::eColorAttachmentWrite, vk::PipelineStageFlagBits2::eTopOfPipe, vk::PipelineStageFlagBits2::eColorAttachmentOutput ); vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); vk::RenderingAttachmentInfo attachmentInfo = { .imageView = *swapChainImageViews[imageIndex], .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, .clearValue = clearColor }; vk::RenderingInfo renderingInfo = { .renderArea = { .offset = { 0, 0 }, .extent = swapChainExtent }, .layerCount = 1, .colorAttachmentCount = 1, .pColorAttachments = &attachmentInfo }; commandBuffers[currentFrame].beginRendering(renderingInfo); commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, *graphicsPipeline); commandBuffers[currentFrame].setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f)); commandBuffers[currentFrame].setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); commandBuffers[currentFrame].bindVertexBuffers(0, *vertexBuffer, {0}); commandBuffers[currentFrame].bindIndexBuffer( *indexBuffer, 0, vk::IndexType::eUint32 ); commandBuffers[currentFrame].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, *descriptorSets[currentFrame], nullptr); commandBuffers[currentFrame].drawIndexed(indices.size(), 1, 0, 0, 0); commandBuffers[currentFrame].endRendering(); transition_image_layout( imageIndex, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::ePresentSrcKHR, vk::AccessFlagBits2::eColorAttachmentWrite, {}, vk::PipelineStageFlagBits2::eColorAttachmentOutput, vk::PipelineStageFlagBits2::eBottomOfPipe ); commandBuffers[currentFrame].end(); } void transition_image_layout( uint32_t imageIndex, vk::ImageLayout old_layout, vk::ImageLayout new_layout, vk::AccessFlags2 src_access_mask, vk::AccessFlags2 dst_access_mask, vk::PipelineStageFlags2 src_stage_mask, vk::PipelineStageFlags2 dst_stage_mask ) { vk::ImageMemoryBarrier2 barrier = { .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 = swapChainImages[imageIndex], .subresourceRange = { .aspectMask = vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1 } }; vk::DependencyInfo dependency_info = { .dependencyFlags = {}, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &barrier }; commandBuffers[currentFrame].pipelineBarrier2(dependency_info); } void createSyncObjects() { presentCompleteSemaphore.clear(); renderFinishedSemaphore.clear(); inFlightFences.clear(); for (size_t i = 0; i < swapChainImages.size(); i++) { presentCompleteSemaphore.emplace_back(device, vk::SemaphoreCreateInfo()); renderFinishedSemaphore.emplace_back(device, vk::SemaphoreCreateInfo()); } for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { inFlightFences.emplace_back(device, vk::FenceCreateInfo{ .flags = vk::FenceCreateFlagBits::eSignaled }); } } void updateUniformBuffer(uint32_t currentImage) const { static auto startTime = std::chrono::high_resolution_clock::now(); auto currentTime = std::chrono::high_resolution_clock::now(); float time = std::chrono::duration(currentTime - startTime).count(); UniformBufferObject ubo{}; glm::mat4 initialRotation = glm::rotate(glm::mat4(1.0f), glm::radians(-90.0f), glm::vec3(1.0f, 0.0f, 0.0f)); glm::mat4 continuousRotation = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); ubo.model = continuousRotation * initialRotation; ubo.view = lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)); ubo.proj = glm::perspective(glm::radians(45.0f), static_cast(swapChainExtent.width) / static_cast(swapChainExtent.height), 0.1f, 10.0f); ubo.proj[1][1] *= -1; memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); } void drawFrame() { while ( vk::Result::eTimeout == device.waitForFences( *inFlightFences[currentFrame], vk::True, UINT64_MAX ) ) ; auto [result, imageIndex] = swapChain.acquireNextImage( UINT64_MAX, *presentCompleteSemaphore[currentFrame], nullptr ); if (result == vk::Result::eErrorOutOfDateKHR) { recreateSwapChain(); return; } if (result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR) { throw std::runtime_error("failed to acquire swap chain image!"); } updateUniformBuffer(currentFrame); device.resetFences( *inFlightFences[currentFrame] ); commandBuffers[currentFrame].reset(); recordCommandBuffer(imageIndex); vk::PipelineStageFlags waitDestinationStageMask( vk::PipelineStageFlagBits::eColorAttachmentOutput ); const vk::SubmitInfo submitInfo{ .waitSemaphoreCount = 1, .pWaitSemaphores = &*presentCompleteSemaphore[currentFrame], .pWaitDstStageMask = &waitDestinationStageMask, .commandBufferCount = 1, .pCommandBuffers = &*commandBuffers[currentFrame], .signalSemaphoreCount = 1, .pSignalSemaphores = &*renderFinishedSemaphore[imageIndex] }; queue.submit(submitInfo, *inFlightFences[currentFrame]); const vk::PresentInfoKHR presentInfoKHR{ .waitSemaphoreCount = 1, .pWaitSemaphores = &*renderFinishedSemaphore[imageIndex], .swapchainCount = 1, .pSwapchains = &*swapChain, .pImageIndices = &imageIndex }; result = queue.presentKHR(presentInfoKHR); if (result == vk::Result::eErrorOutOfDateKHR || result == vk::Result::eSuboptimalKHR || framebufferResized) { framebufferResized = false; recreateSwapChain(); } else if (result != vk::Result::eSuccess) { throw std::runtime_error("failed to present swap chain image!"); } currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; } [[nodiscard]] vk::raii::ShaderModule createShaderModule(const std::vector& code) const { vk::ShaderModuleCreateInfo createInfo{ .codeSize = code.size(), .pCode = reinterpret_cast(code.data()) }; vk::raii::ShaderModule shaderModule{ device, createInfo }; return shaderModule; } static vk::Format chooseSwapSurfaceFormat(const std::vector& availableFormats) { return (availableFormats[0].format == vk::Format::eUndefined) ? vk::Format::eB8G8R8A8Unorm : availableFormats[0].format; } static vk::PresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { return std::ranges::any_of(availablePresentModes, [](const vk::PresentModeKHR value) { return vk::PresentModeKHR::eMailbox == value; } ) ? vk::PresentModeKHR::eMailbox : vk::PresentModeKHR::eFifo; } vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) { if (capabilities.currentExtent.width != std::numeric_limits::max()) { return capabilities.currentExtent; } #if PLATFORM_DESKTOP int width, height; glfwGetFramebufferSize(window, &width, &height); #else ANativeWindow* window = androidAppState.nativeWindow; int width = ANativeWindow_getWidth(window); int height = ANativeWindow_getHeight(window); #endif return { std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height) }; } [[nodiscard]] std::vector getRequiredExtensions() const { std::vector extensions; #if PLATFORM_DESKTOP // Get GLFW extensions uint32_t glfwExtensionCount = 0; auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); extensions.assign(glfwExtensions, glfwExtensions + glfwExtensionCount); #else // Android extensions extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME); extensions.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME); #endif // Add debug extensions if validation layers are enabled if (enableValidationLayers) { extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); } return extensions; } [[nodiscard]] bool checkValidationLayerSupport() const { return (std::ranges::any_of(context.enumerateInstanceLayerProperties(), []( vk::LayerProperties const & lp ) { return ( strcmp( "VK_LAYER_KHRONOS_validation", lp.layerName ) == 0 ); } ) ); } static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback(vk::DebugUtilsMessageSeverityFlagBitsEXT severity, vk::DebugUtilsMessageTypeFlagsEXT type, const vk::DebugUtilsMessengerCallbackDataEXT* pCallbackData, void*) { if (severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eError || severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) { std::cerr << "validation layer: type " << to_string(type) << " msg: " << pCallbackData->pMessage << std::endl; } return vk::False; } std::vector readFile(const std::string& filename) { #if PLATFORM_ANDROID // Android asset loading if (androidAppState.app == nullptr) { LOGE("Android app not initialized"); throw std::runtime_error("Android app not initialized"); } AAsset* asset = AAssetManager_open(androidAppState.app->activity->assetManager, filename.c_str(), AASSET_MODE_BUFFER); if (!asset) { throw std::runtime_error("failed to open file: " + filename); } size_t size = AAsset_getLength(asset); std::vector buffer(size); AAsset_read(asset, buffer.data(), size); AAsset_close(asset); #else // Desktop file loading std::ifstream file(filename, std::ios::ate | std::ios::binary); if (!file.is_open()) { throw std::runtime_error("failed to open file: " + filename); } size_t fileSize = static_cast(file.tellg()); std::vector buffer(fileSize); file.seekg(0); file.read(buffer.data(), fileSize); file.close(); #endif return buffer; } }; #if PLATFORM_ANDROID void android_main(android_app* app) { app_dummy(); VulkanApplication vulkanApp; vulkanApp.run(app); } #else int main() { try { VulkanApplication app; app.run(); } catch (const std::exception& e) { LOGE("%s", e.what()); return EXIT_FAILURE; } return EXIT_SUCCESS; } #endif