From 0ac7c93a1ddd0a01bd27379936ac5c112161e3c4 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 2 Jul 2025 18:07:59 -0700 Subject: [PATCH 01/29] Add 32_ecosystem_utilities.cpp --- attachments/32_ecosystem_utilities.cpp | 1713 ++++++++++++++++++++++++ 1 file changed, 1713 insertions(+) create mode 100644 attachments/32_ecosystem_utilities.cpp diff --git a/attachments/32_ecosystem_utilities.cpp b/attachments/32_ecosystem_utilities.cpp new file mode 100644 index 00000000..3f34e790 --- /dev/null +++ b/attachments/32_ecosystem_utilities.cpp @@ -0,0 +1,1713 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +import vulkan_hpp; +#include + +#define GLFW_INCLUDE_VULKAN // REQUIRED only for GLFW CreateWindowSurface. +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#define GLM_ENABLE_EXPERIMENTAL +#include +#include +#include + +#define STB_IMAGE_IMPLEMENTATION +#include + +#define TINYOBJLOADER_IMPLEMENTATION +#include + +constexpr uint32_t WIDTH = 800; +constexpr uint32_t HEIGHT = 600; +constexpr uint64_t FenceTimeout = 100000000; +const std::string MODEL_PATH = "models/viking_room.obj"; +const std::string TEXTURE_PATH = "textures/viking_room.png"; +constexpr int MAX_FRAMES_IN_FLIGHT = 2; + +const std::vector validationLayers = { + "VK_LAYER_KHRONOS_validation" +}; + +#ifdef NDEBUG +constexpr bool enableValidationLayers = false; +#else +constexpr bool enableValidationLayers = true; +#endif + +// Application info structure to store feature support flags +struct AppInfo { + bool dynamicRenderingSupported = false; + bool timelineSemaphoresSupported = false; + bool synchronization2Supported = false; +}; + +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 HelloTriangleApplication { +public: + void run() { + initWindow(); + initVulkan(); + mainLoop(); + cleanup(); + } + +private: + GLFWwindow* window = nullptr; + 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::SampleCountFlagBits msaaSamples = vk::SampleCountFlagBits::e1; + vk::raii::Device device = nullptr; + + vk::raii::Queue graphicsQueue = nullptr; + vk::raii::Queue presentQueue = nullptr; + + vk::raii::SwapchainKHR swapChain = nullptr; + std::vector swapChainImages; + vk::Format swapChainImageFormat = vk::Format::eUndefined; + vk::Extent2D swapChainExtent; + std::vector swapChainImageViews; + + // Traditional render pass (fallback for non-dynamic rendering) + vk::raii::RenderPass renderPass = nullptr; + std::vector swapChainFramebuffers; + + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline graphicsPipeline = nullptr; + + vk::raii::Image colorImage = nullptr; + vk::raii::DeviceMemory colorImageMemory = nullptr; + vk::raii::ImageView colorImageView = nullptr; + + vk::raii::Image depthImage = nullptr; + vk::raii::DeviceMemory depthImageMemory = nullptr; + vk::raii::ImageView depthImageView = nullptr; + + uint32_t mipLevels = 0; + vk::raii::Image textureImage = nullptr; + vk::raii::DeviceMemory textureImageMemory = nullptr; + vk::raii::ImageView textureImageView = nullptr; + vk::raii::Sampler textureSampler = nullptr; + + 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; + uint32_t graphicsIndex = 0; + + // Synchronization objects + std::vector presentCompleteSemaphore; + std::vector renderFinishedSemaphore; + std::vector inFlightFences; + vk::raii::Semaphore timelineSemaphore = nullptr; + uint64_t timelineValue = 0; + uint32_t semaphoreIndex = 0; + uint32_t currentFrame = 0; + + bool framebufferResized = false; + + std::vector requiredDeviceExtension = { + vk::KHRSwapchainExtensionName + }; + + void initWindow() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan Compatibility Example", 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; + } + + void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + detectFeatureSupport(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + + // Create traditional render pass if dynamic rendering is not supported + if (!appInfo.dynamicRenderingSupported) { + createRenderPass(); + createFramebuffers(); + } + + createDescriptorSetLayout(); + createGraphicsPipeline(); + createCommandPool(); + createColorResources(); + createDepthResources(); + createTextureImage(); + createTextureImageView(); + createTextureSampler(); + loadModel(); + createVertexBuffer(); + createIndexBuffer(); + createUniformBuffers(); + createDescriptorPool(); + createDescriptorSets(); + createCommandBuffers(); + createSyncObjects(); + + // Print feature support summary + std::cout << "\nFeature support summary:\n"; + std::cout << "- Dynamic Rendering: " << (appInfo.dynamicRenderingSupported ? "Yes" : "No") << "\n"; + std::cout << "- Timeline Semaphores: " << (appInfo.timelineSemaphoresSupported ? "Yes" : "No") << "\n"; + std::cout << "- Synchronization2: " << (appInfo.synchronization2Supported ? "Yes" : "No") << "\n"; + } + + void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } + + device.waitIdle(); + } + + void cleanupSwapChain() { + swapChainFramebuffers.clear(); + swapChainImageViews.clear(); + } + + void cleanup() const { + glfwDestroyWindow(window); + glfwTerminate(); + } + + void recreateSwapChain() { + int width = 0, height = 0; + glfwGetFramebufferSize(window, &width, &height); + while (width == 0 || height == 0) { + glfwGetFramebufferSize(window, &width, &height); + glfwWaitEvents(); + } + + device.waitIdle(); + + cleanupSwapChain(); + createSwapChain(); + createImageViews(); + + // Recreate traditional render pass and framebuffers if dynamic rendering is not supported + if (!appInfo.dynamicRenderingSupported) { + createRenderPass(); + createFramebuffers(); + } + + createColorResources(); + createDepthResources(); + } + + void createInstance() { + if (enableValidationLayers && !checkValidationLayerSupport()) { + throw std::runtime_error("validation layers requested, but not available!"); + } + + 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::ApiVersion14 }; + auto extensions = getRequiredExtensions(); + std::vector enabledLayers; + if (enableValidationLayers) { + enabledLayers.assign(validationLayers.begin(), validationLayers.end()); + } + vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &appInfo, + .enabledLayerCount = static_cast(enabledLayers.size()), + .ppEnabledLayerNames = enabledLayers.data(), + .enabledExtensionCount = static_cast(extensions.size()), + .ppEnabledExtensionNames = extensions.data() }; + instance = vk::raii::Instance(context, createInfo); + } + + void setupDebugMessenger() { + if (!enableValidationLayers) return; + + vk::DebugUtilsMessageSeverityFlagsEXT severityFlags( vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError ); + vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags( vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation ); + vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT{ + .messageSeverity = severityFlags, + .messageType = messageTypeFlags, + .pfnUserCallback = &debugCallback + }; + debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfoEXT); + } + + void createSurface() { + VkSurfaceKHR _surface; + if (glfwCreateWindowSurface(*instance, window, nullptr, &_surface) != 0) { + throw std::runtime_error("failed to create window surface!"); + } + surface = vk::raii::SurfaceKHR(instance, _surface); + } + + void pickPhysicalDevice() { + std::vector devices = instance.enumeratePhysicalDevices(); + const auto devIter = std::ranges::find_if( + devices, + [&]( auto const & device ) + { + // 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; } ); + } ); + + return supportsGraphics && supportsAllRequiredExtensions; + }); + if ( devIter != devices.end() ) + { + physicalDevice = *devIter; + msaaSamples = getMaxUsableSampleCount(); + } + else + { + throw std::runtime_error("failed to find a suitable GPU!"); + } + } + + void detectFeatureSupport() { + // Get device properties to check Vulkan version + vk::PhysicalDeviceProperties deviceProperties = physicalDevice.getProperties(); + + // Get available extensions + std::vector availableExtensions = physicalDevice.enumerateDeviceExtensionProperties(); + + // Check for dynamic rendering support + if (deviceProperties.apiVersion >= VK_VERSION_1_3) { + appInfo.dynamicRenderingSupported = true; + std::cout << "Dynamic rendering supported via Vulkan 1.3\n"; + } else { + // Check for the extension on older Vulkan versions + for (const auto& extension : availableExtensions) { + if (strcmp(extension.extensionName, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME) == 0) { + appInfo.dynamicRenderingSupported = true; + std::cout << "Dynamic rendering supported via extension\n"; + break; + } + } + } + + // Check for timeline semaphores support + if (deviceProperties.apiVersion >= VK_VERSION_1_2) { + appInfo.timelineSemaphoresSupported = true; + std::cout << "Timeline semaphores supported via Vulkan 1.2\n"; + } else { + // Check for the extension on older Vulkan versions + for (const auto& extension : availableExtensions) { + if (strcmp(extension.extensionName, VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME) == 0) { + appInfo.timelineSemaphoresSupported = true; + std::cout << "Timeline semaphores supported via extension\n"; + break; + } + } + } + + // Check for synchronization2 support + if (deviceProperties.apiVersion >= VK_VERSION_1_3) { + appInfo.synchronization2Supported = true; + std::cout << "Synchronization2 supported via Vulkan 1.3\n"; + } else { + // Check for the extension on older Vulkan versions + for (const auto& extension : availableExtensions) { + if (strcmp(extension.extensionName, VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME) == 0) { + appInfo.synchronization2Supported = true; + std::cout << "Synchronization2 supported via extension\n"; + break; + } + } + } + + // Add required extensions based on feature support + if (appInfo.dynamicRenderingSupported && deviceProperties.apiVersion < VK_VERSION_1_3) { + requiredDeviceExtension.push_back(VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME); + } + + if (appInfo.timelineSemaphoresSupported && deviceProperties.apiVersion < VK_VERSION_1_2) { + requiredDeviceExtension.push_back(VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME); + } + + if (appInfo.synchronization2Supported && deviceProperties.apiVersion < VK_VERSION_1_3) { + requiredDeviceExtension.push_back(VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME); + } + } + + void createLogicalDevice() { + // find the index of the first queue family that supports graphics + std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); + + // get the first index into queueFamilyProperties which supports graphics + auto graphicsQueueFamilyProperty = std::ranges::find_if( queueFamilyProperties, []( auto const & qfp ) + { return (qfp.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast(0); } ); + + graphicsIndex = static_cast( std::distance( queueFamilyProperties.begin(), graphicsQueueFamilyProperty ) ); + + // determine a queueFamilyIndex that supports present + // first check if the graphicsIndex is good enough + auto presentIndex = physicalDevice.getSurfaceSupportKHR( graphicsIndex, *surface ) + ? graphicsIndex + : ~0; + if ( presentIndex == queueFamilyProperties.size() ) + { + // the graphicsIndex doesn't support present -> look for another family index that supports both + // graphics and present + for ( size_t i = 0; i < queueFamilyProperties.size(); i++ ) + { + if ( ( queueFamilyProperties[i].queueFlags & vk::QueueFlagBits::eGraphics ) && + physicalDevice.getSurfaceSupportKHR( static_cast( i ), *surface ) ) + { + graphicsIndex = static_cast( i ); + presentIndex = graphicsIndex; + break; + } + } + if ( presentIndex == queueFamilyProperties.size() ) + { + // there's nothing like a single family index that supports both graphics and present -> look for another + // family index that supports present + for ( size_t i = 0; i < queueFamilyProperties.size(); i++ ) + { + if ( physicalDevice.getSurfaceSupportKHR( static_cast( i ), *surface ) ) + { + presentIndex = static_cast( i ); + break; + } + } + } + } + if ( ( graphicsIndex == queueFamilyProperties.size() ) || ( presentIndex == queueFamilyProperties.size() ) ) + { + throw std::runtime_error( "Could not find a queue for graphics or present -> terminating" ); + } + + // Create device with appropriate features + auto features = physicalDevice.getFeatures2(); + + // Setup feature chain based on detected support + void* pNext = nullptr; + + // Add dynamic rendering if supported + vk::PhysicalDeviceVulkan13Features vulkan13Features; + vk::PhysicalDeviceDynamicRenderingFeatures dynamicRenderingFeatures; + + if (appInfo.dynamicRenderingSupported) { + if (appInfo.synchronization2Supported) { + vulkan13Features.dynamicRendering = vk::True; + vulkan13Features.synchronization2 = vk::True; + vulkan13Features.pNext = pNext; + pNext = &vulkan13Features; + } else { + dynamicRenderingFeatures.dynamicRendering = vk::True; + dynamicRenderingFeatures.pNext = pNext; + pNext = &dynamicRenderingFeatures; + } + } + + // Add timeline semaphores if supported + vk::PhysicalDeviceTimelineSemaphoreFeatures timelineSemaphoreFeatures; + if (appInfo.timelineSemaphoresSupported) { + timelineSemaphoreFeatures.timelineSemaphore = vk::True; + timelineSemaphoreFeatures.pNext = pNext; + pNext = &timelineSemaphoreFeatures; + } + + features.pNext = pNext; + + // create a Device + float queuePriority = 0.0f; + vk::DeviceQueueCreateInfo deviceQueueCreateInfo{ .queueFamilyIndex = graphicsIndex, .queueCount = 1, .pQueuePriorities = &queuePriority }; + vk::DeviceCreateInfo deviceCreateInfo{ + .pNext = &features, + .queueCreateInfoCount = 1, + .pQueueCreateInfos = &deviceQueueCreateInfo, + .enabledExtensionCount = static_cast(requiredDeviceExtension.size()), + .ppEnabledExtensionNames = requiredDeviceExtension.data() + }; + + device = vk::raii::Device( physicalDevice, deviceCreateInfo ); + graphicsQueue = vk::raii::Queue( device, graphicsIndex, 0 ); + presentQueue = vk::raii::Queue( device, presentIndex, 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 createRenderPass() { + if (appInfo.dynamicRenderingSupported) { + // No render pass needed with dynamic rendering + std::cout << "Using dynamic rendering, skipping render pass creation\n"; + return; + } + + std::cout << "Creating traditional render pass\n"; + + // Color attachment description + vk::AttachmentDescription colorAttachment{ + .format = swapChainImageFormat, + .samples = msaaSamples, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, + .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, + .initialLayout = vk::ImageLayout::eUndefined, + .finalLayout = vk::ImageLayout::eColorAttachmentOptimal + }; + + vk::AttachmentDescription depthAttachment{ + .format = findDepthFormat(), + .samples = msaaSamples, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eDontCare, + .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, + .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, + .initialLayout = vk::ImageLayout::eUndefined, + .finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal + }; + + vk::AttachmentDescription colorAttachmentResolve{ + .format = swapChainImageFormat, + .samples = vk::SampleCountFlagBits::e1, + .loadOp = vk::AttachmentLoadOp::eDontCare, + .storeOp = vk::AttachmentStoreOp::eStore, + .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, + .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, + .initialLayout = vk::ImageLayout::eUndefined, + .finalLayout = vk::ImageLayout::ePresentSrcKHR + }; + + // Subpass references + vk::AttachmentReference colorAttachmentRef{ + .attachment = 0, + .layout = vk::ImageLayout::eColorAttachmentOptimal + }; + + vk::AttachmentReference depthAttachmentRef{ + .attachment = 1, + .layout = vk::ImageLayout::eDepthStencilAttachmentOptimal + }; + + vk::AttachmentReference colorAttachmentResolveRef{ + .attachment = 2, + .layout = vk::ImageLayout::eColorAttachmentOptimal + }; + + // Subpass description + vk::SubpassDescription subpass{ + .pipelineBindPoint = vk::PipelineBindPoint::eGraphics, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentRef, + .pResolveAttachments = &colorAttachmentResolveRef, + .pDepthStencilAttachment = &depthAttachmentRef + }; + + // Dependency to ensure proper image layout transitions + vk::SubpassDependency dependency{ + .srcSubpass = VK_SUBPASS_EXTERNAL, + .dstSubpass = 0, + .srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests, + .dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests, + .srcAccessMask = vk::AccessFlagBits::eNone, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite + }; + + // Create the render pass + std::array attachments = { colorAttachment, depthAttachment, colorAttachmentResolve }; + vk::RenderPassCreateInfo renderPassInfo{ + .attachmentCount = static_cast(attachments.size()), + .pAttachments = attachments.data(), + .subpassCount = 1, + .pSubpasses = &subpass, + .dependencyCount = 1, + .pDependencies = &dependency + }; + + renderPass = vk::raii::RenderPass(device, renderPassInfo); + } + + void createFramebuffers() { + if (appInfo.dynamicRenderingSupported) { + // No framebuffers needed with dynamic rendering + std::cout << "Using dynamic rendering, skipping framebuffer creation\n"; + return; + } + + std::cout << "Creating traditional framebuffers\n"; + + swapChainFramebuffers.clear(); + + for (size_t i = 0; i < swapChainImageViews.size(); i++) { + std::array attachments = { + *colorImageView, + *depthImageView, + *swapChainImageViews[i] + }; + + vk::FramebufferCreateInfo framebufferInfo{ + .renderPass = *renderPass, + .attachmentCount = static_cast(attachments.size()), + .pAttachments = attachments.data(), + .width = swapChainExtent.width, + .height = swapChainExtent.height, + .layers = 1 + }; + + swapChainFramebuffers.emplace_back(device, framebufferInfo); + } + } + + 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(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, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = vk::False + }; + rasterizer.lineWidth = 1.0f; + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = msaaSamples, + .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::GraphicsPipelineCreateInfo pipelineInfo{ + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = pipelineLayout + }; + + // Configure pipeline based on dynamic rendering support + vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo; + if (appInfo.dynamicRenderingSupported) { + std::cout << "Configuring pipeline for dynamic rendering\n"; + pipelineRenderingCreateInfo.colorAttachmentCount = 1; + pipelineRenderingCreateInfo.pColorAttachmentFormats = &swapChainImageFormat; + pipelineRenderingCreateInfo.depthAttachmentFormat = findDepthFormat(); + + pipelineInfo.pNext = &pipelineRenderingCreateInfo; + pipelineInfo.renderPass = nullptr; + } else { + std::cout << "Configuring pipeline for traditional render pass\n"; + pipelineInfo.pNext = nullptr; + pipelineInfo.renderPass = *renderPass; + pipelineInfo.subpass = 0; + } + + graphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + } + + void createCommandPool() { + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = graphicsIndex + }; + commandPool = vk::raii::CommandPool(device, poolInfo); + } + + void createColorResources() { + vk::Format colorFormat = swapChainImageFormat; + + createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, colorFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransientAttachment | vk::ImageUsageFlagBits::eColorAttachment, vk::MemoryPropertyFlagBits::eDeviceLocal, colorImage, colorImageMemory); + colorImageView = createImageView(colorImage, colorFormat, vk::ImageAspectFlagBits::eColor, 1); + } + + void createDepthResources() { + vk::Format depthFormat = findDepthFormat(); + + createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, depthFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eDepthStencilAttachment, vk::MemoryPropertyFlagBits::eDeviceLocal, depthImage, depthImageMemory); + depthImageView = createImageView(depthImage, depthFormat, vk::ImageAspectFlagBits::eDepth, 1); + } + + 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() { + int texWidth, texHeight, texChannels; + stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + vk::DeviceSize imageSize = texWidth * texHeight * 4; + mipLevels = static_cast(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1; + + if (!pixels) { + throw std::runtime_error("failed to load texture image!"); + } + + 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, pixels, imageSize); + stagingBufferMemory.unmapMemory(); + + stbi_image_free(pixels); + + createImage(texWidth, texHeight, mipLevels, vk::SampleCountFlagBits::e1, vk::Format::eR8G8B8A8Srgb, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal, textureImage, textureImageMemory); + + transitionImageLayout(textureImage, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, mipLevels); + copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); + + generateMipmaps(textureImage, vk::Format::eR8G8B8A8Srgb, texWidth, texHeight, mipLevels); + } + + void generateMipmaps(vk::raii::Image& image, vk::Format imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) { + // Check if image format supports linear blit-ing + vk::FormatProperties formatProperties = physicalDevice.getFormatProperties(imageFormat); + + if (!(formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImageFilterLinear)) { + throw std::runtime_error("texture image format does not support linear blitting!"); + } + + std::unique_ptr commandBuffer = beginSingleTimeCommands(); + + vk::ImageMemoryBarrier barrier = { .srcAccessMask = vk::AccessFlagBits::eTransferWrite, .dstAccessMask =vk::AccessFlagBits::eTransferRead + , .oldLayout = vk::ImageLayout::eTransferDstOptimal, .newLayout = vk::ImageLayout::eTransferSrcOptimal + , .srcQueueFamilyIndex = vk::QueueFamilyIgnored, .dstQueueFamilyIndex = vk::QueueFamilyIgnored, .image = image }; + barrier.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.subresourceRange.levelCount = 1; + + int32_t mipWidth = texWidth; + int32_t mipHeight = texHeight; + + for (uint32_t i = 1; i < mipLevels; i++) { + barrier.subresourceRange.baseMipLevel = i - 1; + barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal; + barrier.newLayout = vk::ImageLayout::eTransferSrcOptimal; + barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + barrier.dstAccessMask = vk::AccessFlagBits::eTransferRead; + + commandBuffer->pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, barrier); + + vk::ArrayWrapper1D offsets, dstOffsets; + offsets[0] = vk::Offset3D(0, 0, 0); + offsets[1] = vk::Offset3D(mipWidth, mipHeight, 1); + dstOffsets[0] = vk::Offset3D(0, 0, 0); + dstOffsets[1] = vk::Offset3D(mipWidth > 1 ? mipWidth / 2 : 1, mipHeight > 1 ? mipHeight / 2 : 1, 1); + vk::ImageBlit blit = { .srcSubresource = {}, .srcOffsets = offsets, + .dstSubresource = {}, .dstOffsets = dstOffsets }; + blit.srcSubresource = vk::ImageSubresourceLayers( vk::ImageAspectFlagBits::eColor, i - 1, 0, 1); + blit.dstSubresource = vk::ImageSubresourceLayers( vk::ImageAspectFlagBits::eColor, i, 0, 1); + + commandBuffer->blitImage(image, vk::ImageLayout::eTransferSrcOptimal, image, vk::ImageLayout::eTransferDstOptimal, { blit }, vk::Filter::eLinear); + + barrier.oldLayout = vk::ImageLayout::eTransferSrcOptimal; + barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + barrier.srcAccessMask = vk::AccessFlagBits::eTransferRead; + barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; + + commandBuffer->pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, {}, {}, barrier); + + if (mipWidth > 1) mipWidth /= 2; + if (mipHeight > 1) mipHeight /= 2; + } + + barrier.subresourceRange.baseMipLevel = mipLevels - 1; + barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal; + barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; + + commandBuffer->pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, {}, {}, barrier); + + endSingleTimeCommands(*commandBuffer); + } + + vk::SampleCountFlagBits getMaxUsableSampleCount() { + vk::PhysicalDeviceProperties physicalDeviceProperties = physicalDevice.getProperties(); + + vk::SampleCountFlags counts = physicalDeviceProperties.limits.framebufferColorSampleCounts & physicalDeviceProperties.limits.framebufferDepthSampleCounts; + if (counts & vk::SampleCountFlagBits::e64) { return vk::SampleCountFlagBits::e64; } + if (counts & vk::SampleCountFlagBits::e32) { return vk::SampleCountFlagBits::e32; } + if (counts & vk::SampleCountFlagBits::e16) { return vk::SampleCountFlagBits::e16; } + if (counts & vk::SampleCountFlagBits::e8) { return vk::SampleCountFlagBits::e8; } + if (counts & vk::SampleCountFlagBits::e4) { return vk::SampleCountFlagBits::e4; } + if (counts & vk::SampleCountFlagBits::e2) { return vk::SampleCountFlagBits::e2; } + + return vk::SampleCountFlagBits::e1; + } + + void createTextureImageView() { + textureImageView = createImageView(textureImage, vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor, mipLevels); + } + + 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); + } + + [[nodiscard]] vk::raii::ImageView createImageView(const vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels) const { + vk::ImageViewCreateInfo viewInfo{ + .image = image, + .viewType = vk::ImageViewType::e2D, + .format = format, + .subresourceRange = { aspectFlags, 0, mipLevels, 0, 1 } + }; + return vk::raii::ImageView( device, viewInfo ); + } + + void createImage(uint32_t width, uint32_t height, uint32_t mipLevels, vk::SampleCountFlagBits numSamples, 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 = mipLevels, + .arrayLayers = 1, + .samples = numSamples, + .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, const vk::ImageLayout oldLayout, const vk::ImageLayout newLayout, uint32_t mipLevels) { + const auto commandBuffer = beginSingleTimeCommands(); + + if (appInfo.synchronization2Supported) { + // Use Synchronization2 API + vk::ImageMemoryBarrier2 barrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eAllCommands, + .dstStageMask = vk::PipelineStageFlagBits2::eAllCommands, + .oldLayout = oldLayout, + .newLayout = newLayout, + .image = image, + .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, mipLevels, 0, 1 } + }; + + if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { + barrier.srcAccessMask = vk::AccessFlagBits2::eNone; + barrier.dstAccessMask = vk::AccessFlagBits2::eTransferWrite; + barrier.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; + barrier.dstStageMask = vk::PipelineStageFlagBits2::eTransfer; + } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { + barrier.srcAccessMask = vk::AccessFlagBits2::eTransferWrite; + barrier.dstAccessMask = vk::AccessFlagBits2::eShaderRead; + barrier.srcStageMask = vk::PipelineStageFlagBits2::eTransfer; + barrier.dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader; + } else { + throw std::invalid_argument("unsupported layout transition!"); + } + + vk::DependencyInfo dependencyInfo{ + .imageMemoryBarrierCount = 1, + .pImageMemoryBarriers = &barrier + }; + + commandBuffer->pipelineBarrier2(dependencyInfo); + } else { + // Use traditional synchronization API + vk::ImageMemoryBarrier barrier{ + .oldLayout = oldLayout, + .newLayout = newLayout, + .image = image, + .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, mipLevels, 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, const 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() { + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + + if (!LoadObj(&attrib, &shapes, &materials, &warn, &err, MODEL_PATH.c_str())) { + throw std::runtime_error(warn + err); + } + + std::unordered_map uniqueVertices{}; + + for (const auto& shape : shapes) { + for (const auto& index : shape.mesh.indices) { + Vertex vertex{}; + + vertex.pos = { + attrib.vertices[3 * index.vertex_index + 0], + attrib.vertices[3 * index.vertex_index + 1], + attrib.vertices[3 * index.vertex_index + 2] + }; + + vertex.texCoord = { + attrib.texcoords[2 * index.texcoord_index + 0], + 1.0f - attrib.texcoords[2 * index.texcoord_index + 1] + }; + + vertex.color = {1.0f, 1.0f, 1.0f}; + + if (!uniqueVertices.contains(vertex)) { + uniqueVertices[vertex] = static_cast(vertices.size()); + vertices.push_back(vertex); + } + + indices.push_back(uniqueVertices[vertex]); + } + } + } + + 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 }; + graphicsQueue.submit(submitInfo, nullptr); + graphicsQueue.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(); + graphicsQueue.submit(vk::SubmitInfo{ .commandBufferCount = 1, .pCommandBuffers = &*commandCopyBuffer }, nullptr); + graphicsQueue.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({}); + + vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); + vk::ClearValue clearDepth = vk::ClearDepthStencilValue(1.0f, 0); + std::array clearValues = { clearColor, clearDepth }; + + if (appInfo.dynamicRenderingSupported) { + // Use dynamic rendering + std::cout << "Recording command buffer with dynamic rendering\n"; + + // Transition attachments to the correct layout + if (appInfo.synchronization2Supported) { + // Use Synchronization2 API for image transitions + vk::ImageMemoryBarrier2 colorBarrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe, + .srcAccessMask = vk::AccessFlagBits2::eNone, + .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .image = *colorImage, + .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } + }; + + vk::ImageMemoryBarrier2 depthBarrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eEarlyFragmentTests | vk::PipelineStageFlagBits2::eLateFragmentTests, + .srcAccessMask = vk::AccessFlagBits2::eNone, + .dstStageMask = vk::PipelineStageFlagBits2::eEarlyFragmentTests | vk::PipelineStageFlagBits2::eLateFragmentTests, + .dstAccessMask = vk::AccessFlagBits2::eDepthStencilAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .image = *depthImage, + .subresourceRange = { vk::ImageAspectFlagBits::eDepth, 0, 1, 0, 1 } + }; + + vk::ImageMemoryBarrier2 swapchainBarrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe, + .srcAccessMask = vk::AccessFlagBits2::eNone, + .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .image = swapChainImages[imageIndex], + .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } + }; + + std::array barriers = { colorBarrier, depthBarrier, swapchainBarrier }; + vk::DependencyInfo dependencyInfo{ + .imageMemoryBarrierCount = static_cast(barriers.size()), + .pImageMemoryBarriers = barriers.data() + }; + + commandBuffers[currentFrame].pipelineBarrier2(dependencyInfo); + } else { + // Use traditional synchronization API + vk::ImageMemoryBarrier colorBarrier{ + .srcAccessMask = vk::AccessFlagBits::eNone, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *colorImage, + .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } + }; + + vk::ImageMemoryBarrier depthBarrier{ + .srcAccessMask = vk::AccessFlagBits::eNone, + .dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *depthImage, + .subresourceRange = { vk::ImageAspectFlagBits::eDepth, 0, 1, 0, 1 } + }; + + vk::ImageMemoryBarrier swapchainBarrier{ + .srcAccessMask = vk::AccessFlagBits::eNone, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = swapChainImages[imageIndex], + .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } + }; + + std::array barriers = { colorBarrier, depthBarrier, swapchainBarrier }; + commandBuffers[currentFrame].pipelineBarrier( + vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests, + vk::DependencyFlagBits::eByRegion, + 0, nullptr, + 0, nullptr, + static_cast(barriers.size()), barriers.data() + ); + } + + // Setup rendering attachments + vk::RenderingAttachmentInfo colorAttachment{ + .imageView = *colorImageView, + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .resolveMode = vk::ResolveModeFlagBits::eAverage, + .resolveImageView = *swapChainImageViews[imageIndex], + .resolveImageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearColor + }; + + vk::RenderingAttachmentInfo depthAttachment{ + .imageView = *depthImageView, + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eDontCare, + .clearValue = clearDepth + }; + + vk::RenderingInfo renderingInfo{ + .renderArea = {{0, 0}, swapChainExtent}, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachment, + .pDepthAttachment = &depthAttachment + }; + + commandBuffers[currentFrame].beginRendering(renderingInfo); + } else { + // Use traditional render pass + std::cout << "Recording command buffer with traditional render pass\n"; + + vk::RenderPassBeginInfo renderPassInfo{ + .renderPass = *renderPass, + .framebuffer = *swapChainFramebuffers[imageIndex], + .renderArea = {{0, 0}, swapChainExtent}, + .clearValueCount = static_cast(clearValues.size()), + .pClearValues = clearValues.data() + }; + + commandBuffers[currentFrame].beginRenderPass(renderPassInfo, vk::SubpassContents::eInline); + } + + // Common rendering commands + 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); + + if (appInfo.dynamicRenderingSupported) { + commandBuffers[currentFrame].endRendering(); + + // Transition swapchain image to present layout + if (appInfo.synchronization2Supported) { + vk::ImageMemoryBarrier2 barrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eBottomOfPipe, + .dstAccessMask = vk::AccessFlagBits2::eNone, + .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, + .newLayout = vk::ImageLayout::ePresentSrcKHR, + .image = swapChainImages[imageIndex], + .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } + }; + + vk::DependencyInfo dependencyInfo{ + .imageMemoryBarrierCount = 1, + .pImageMemoryBarriers = &barrier + }; + + commandBuffers[currentFrame].pipelineBarrier2(dependencyInfo); + } else { + vk::ImageMemoryBarrier barrier{ + .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + .dstAccessMask = vk::AccessFlagBits::eNone, + .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, + .newLayout = vk::ImageLayout::ePresentSrcKHR, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = swapChainImages[imageIndex], + .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } + }; + + commandBuffers[currentFrame].pipelineBarrier( + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::PipelineStageFlagBits::eBottomOfPipe, + vk::DependencyFlagBits::eByRegion, + 0, nullptr, + 0, nullptr, + 1, &barrier + ); + } + } else { + commandBuffers[currentFrame].endRenderPass(); + } + + commandBuffers[currentFrame].end(); + } + + void createSyncObjects() { + presentCompleteSemaphore.clear(); + renderFinishedSemaphore.clear(); + inFlightFences.clear(); + + if (appInfo.timelineSemaphoresSupported) { + // Create timeline semaphore + std::cout << "Creating timeline semaphores\n"; + vk::SemaphoreTypeCreateInfo timelineCreateInfo{ + .semaphoreType = vk::SemaphoreType::eTimeline, + .initialValue = 0 + }; + + vk::SemaphoreCreateInfo semaphoreInfo{ + .pNext = &timelineCreateInfo + }; + + timelineSemaphore = vk::raii::Semaphore(device, semaphoreInfo); + + // Still need binary semaphores for swapchain operations + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + presentCompleteSemaphore.emplace_back(device, vk::SemaphoreCreateInfo()); + renderFinishedSemaphore.emplace_back(device, vk::SemaphoreCreateInfo()); + } + } else { + // Create binary semaphores and fences + std::cout << "Creating binary semaphores and fences\n"; + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; 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{}; + ubo.model = rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); + 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, FenceTimeout)) + ; + auto [result, imageIndex] = swapChain.acquireNextImage(UINT64_MAX, *presentCompleteSemaphore[semaphoreIndex], 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); + + if (appInfo.timelineSemaphoresSupported) { + // Use timeline semaphores for GPU synchronization + uint64_t waitValue = timelineValue; + uint64_t signalValue = ++timelineValue; + + vk::TimelineSemaphoreSubmitInfo timelineInfo{ + .waitSemaphoreValueCount = 0, // We'll still use binary semaphore for swapchain + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = &signalValue + }; + + std::array waitSemaphores = { *presentCompleteSemaphore[semaphoreIndex], *timelineSemaphore }; + std::array waitStages = { vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eVertexInput }; + std::array waitValues = { 0, waitValue }; // Binary semaphore value is ignored + + std::array signalSemaphores = { *renderFinishedSemaphore[imageIndex], *timelineSemaphore }; + std::array signalValues = { 0, signalValue }; // Binary semaphore value is ignored + + timelineInfo.waitSemaphoreValueCount = 1; // Only for the timeline semaphore + timelineInfo.pWaitSemaphoreValues = &waitValues[1]; + timelineInfo.signalSemaphoreValueCount = 1; // Only for the timeline semaphore + timelineInfo.pSignalSemaphoreValues = &signalValues[1]; + + vk::SubmitInfo submitInfo{ + .pNext = &timelineInfo, + .waitSemaphoreCount = 1, // Only wait on the binary semaphore + .pWaitSemaphores = &waitSemaphores[0], + .pWaitDstStageMask = &waitStages[0], + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffers[currentFrame], + .signalSemaphoreCount = 2, // Signal both semaphores + .pSignalSemaphores = signalSemaphores.data() + }; + + graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); + } else { + // Use traditional binary semaphores + vk::PipelineStageFlags waitDestinationStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput); + const vk::SubmitInfo submitInfo{ + .waitSemaphoreCount = 1, + .pWaitSemaphores = &*presentCompleteSemaphore[semaphoreIndex], + .pWaitDstStageMask = &waitDestinationStageMask, + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffers[currentFrame], + .signalSemaphoreCount = 1, + .pSignalSemaphores = &*renderFinishedSemaphore[imageIndex] + }; + graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); + } + + const vk::PresentInfoKHR presentInfoKHR{ + .waitSemaphoreCount = 1, + .pWaitSemaphores = &*renderFinishedSemaphore[imageIndex], + .swapchainCount = 1, + .pSwapchains = &*swapChain, + .pImageIndices = &imageIndex + }; + result = presentQueue.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!"); + } + semaphoreIndex = (semaphoreIndex + 1) % presentCompleteSemaphore.size(); + 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; + } + + [[nodiscard]] vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) const { + if (capabilities.currentExtent.width != std::numeric_limits::max()) { + return capabilities.currentExtent; + } + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + return { + std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), + std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height) + }; + } + + [[nodiscard]] std::vector getRequiredExtensions() const { + uint32_t glfwExtensionCount = 0; + auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + + std::vector props = context.enumerateInstanceExtensionProperties(); + if (const auto propsIterator = std::ranges::find_if(props, []( vk::ExtensionProperties const & ep ) { return strcmp( ep.extensionName, vk::EXTDebugUtilsExtensionName ) == 0; } ); propsIterator == props.end() ) + { + std::cout << "Something went very wrong, cannot find VK_EXT_debug_utils extension" << std::endl; + exit( 1 ); + } + std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); + if (enableValidationLayers) { + extensions.push_back(vk::EXTDebugUtilsExtensionName ); + } + + 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; + } + + static std::vector readFile(const std::string& filename) { + std::ifstream file(filename, std::ios::ate | std::ios::binary); + + if (!file.is_open()) { + throw std::runtime_error("failed to open file!"); + } + std::vector buffer(file.tellg()); + file.seekg(0, std::ios::beg); + file.read(buffer.data(), static_cast(buffer.size())); + file.close(); + + return buffer; + } +}; + +int main() { + try { + HelloTriangleApplication app; + app.run(); + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} From e04bc4c28bf97817e17699cbbd067d72fb1831fd Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 2 Jul 2025 20:47:49 -0700 Subject: [PATCH 02/29] Add support for new chapters on ecosystem utilities and Vulkan profiles - Introduced `12_Ecosystem_Utilities_and_Compatibility.adoc` and `13_Vulkan_Profiles.adoc` documentation. - Updated navigation to include new chapters. - Added new example code for ecosystem utilities (`32_ecosystem_utilities.cpp`) and Vulkan profiles. - Replaced hard-coded validation layer logic with external `vulkanconfig` management for cleaner implementation and flexibility. - Enhanced CMake setup for new chapters `32_ecosystem_utilities` and `33_vulkan_profiles`. --- antora/modules/ROOT/nav.adoc | 4 +- attachments/32_ecosystem_utilities.cpp | 104 +- attachments/33_vulkan_profiles.cpp | 1749 +++++++++++++++++ attachments/CMakeLists.txt | 12 + ...Ecosystem_Utilities_and_Compatibility.adoc | 542 +++++ en/13_Vulkan_Profiles.adoc | 318 +++ 6 files changed, 2681 insertions(+), 48 deletions(-) create mode 100644 attachments/33_vulkan_profiles.cpp create mode 100644 en/12_Ecosystem_Utilities_and_Compatibility.adoc create mode 100644 en/13_Vulkan_Profiles.adoc diff --git a/antora/modules/ROOT/nav.adoc b/antora/modules/ROOT/nav.adoc index 784fe7c9..44a795b8 100644 --- a/antora/modules/ROOT/nav.adoc +++ b/antora/modules/ROOT/nav.adoc @@ -46,4 +46,6 @@ * xref:09_Generating_Mipmaps.adoc[Generating Mipmaps] * xref:10_Multisampling.adoc[Multisampling] * xref:11_Compute_Shader.adoc[Compute Shader] -* xref:90_FAQ.adoc[FAQ] \ No newline at end of file +* xref:12_Ecosystem_Utilities_and_Compatibility.adoc[Ecosystem Utilities and GPU Compatibility] +* xref:13_Vulkan_Profiles.adoc[Vulkan Profiles] +* xref:90_FAQ.adoc[FAQ] diff --git a/attachments/32_ecosystem_utilities.cpp b/attachments/32_ecosystem_utilities.cpp index 3f34e790..c5a2c49b 100644 --- a/attachments/32_ecosystem_utilities.cpp +++ b/attachments/32_ecosystem_utilities.cpp @@ -37,15 +37,8 @@ const std::string MODEL_PATH = "models/viking_room.obj"; const std::string TEXTURE_PATH = "textures/viking_room.png"; constexpr int MAX_FRAMES_IN_FLIGHT = 2; -const std::vector validationLayers = { - "VK_LAYER_KHRONOS_validation" -}; - -#ifdef NDEBUG -constexpr bool enableValidationLayers = false; -#else -constexpr bool enableValidationLayers = true; -#endif +// Validation layers are now managed by vulkanconfig instead of being hard-coded +// See the Ecosystem Utilities chapter for details on using vulkanconfig // Application info structure to store feature support flags struct AppInfo { @@ -273,40 +266,56 @@ class HelloTriangleApplication { } void createInstance() { - if (enableValidationLayers && !checkValidationLayerSupport()) { - throw std::runtime_error("validation layers requested, but not available!"); - } + // Validation layers are now managed by vulkanconfig instead of being hard-coded + + 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::ApiVersion14 + }; - 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::ApiVersion14 }; auto extensions = getRequiredExtensions(); - std::vector enabledLayers; - if (enableValidationLayers) { - enabledLayers.assign(validationLayers.begin(), validationLayers.end()); - } + vk::InstanceCreateInfo createInfo{ .pApplicationInfo = &appInfo, - .enabledLayerCount = static_cast(enabledLayers.size()), - .ppEnabledLayerNames = enabledLayers.data(), .enabledExtensionCount = static_cast(extensions.size()), - .ppEnabledExtensionNames = extensions.data() }; + .ppEnabledExtensionNames = extensions.data() + }; + instance = vk::raii::Instance(context, createInfo); } void setupDebugMessenger() { - if (!enableValidationLayers) return; + // Always set up the debug messenger + // It will only be used if validation layers are enabled via vulkanconfig + + vk::DebugUtilsMessageSeverityFlagsEXT severityFlags( + vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eError + ); + + vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags( + vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | + vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | + vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation + ); - vk::DebugUtilsMessageSeverityFlagsEXT severityFlags( vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError ); - vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags( vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation ); vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT{ .messageSeverity = severityFlags, .messageType = messageTypeFlags, .pfnUserCallback = &debugCallback }; - debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfoEXT); + + try { + debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfoEXT); + } catch (vk::SystemError& err) { + // If the debug utils extension is not available, this will fail + // That's okay, it just means validation layers aren't enabled + std::cout << "Debug messenger not available. Validation layers may not be enabled." << std::endl; + } } void createSurface() { @@ -1377,9 +1386,9 @@ class HelloTriangleApplication { vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests, vk::DependencyFlagBits::eByRegion, - 0, nullptr, - 0, nullptr, - static_cast(barriers.size()), barriers.data() + {}, + {}, + barriers ); } @@ -1474,9 +1483,9 @@ class HelloTriangleApplication { vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eBottomOfPipe, vk::DependencyFlagBits::eByRegion, - 0, nullptr, - 0, nullptr, - 1, &barrier + {}, + {}, + { barrier } ); } } else { @@ -1655,28 +1664,29 @@ class HelloTriangleApplication { } [[nodiscard]] std::vector getRequiredExtensions() const { + // Get the required extensions from GLFW uint32_t glfwExtensionCount = 0; auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); + // Check if the debug utils extension is available std::vector props = context.enumerateInstanceExtensionProperties(); - if (const auto propsIterator = std::ranges::find_if(props, []( vk::ExtensionProperties const & ep ) { return strcmp( ep.extensionName, vk::EXTDebugUtilsExtensionName ) == 0; } ); propsIterator == props.end() ) - { - std::cout << "Something went very wrong, cannot find VK_EXT_debug_utils extension" << std::endl; - exit( 1 ); - } - std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); - if (enableValidationLayers) { - extensions.push_back(vk::EXTDebugUtilsExtensionName ); + bool debugUtilsAvailable = std::ranges::any_of(props, + [](vk::ExtensionProperties const & ep) { + return strcmp(ep.extensionName, vk::EXTDebugUtilsExtensionName) == 0; + }); + + // Always include the debug utils extension if available + // This allows validation layers to be enabled via vulkanconfig + if (debugUtilsAvailable) { + extensions.push_back(vk::EXTDebugUtilsExtensionName); + } else { + std::cout << "VK_EXT_debug_utils extension not available. Validation layers may not work." << std::endl; } 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; diff --git a/attachments/33_vulkan_profiles.cpp b/attachments/33_vulkan_profiles.cpp new file mode 100644 index 00000000..44199bfe --- /dev/null +++ b/attachments/33_vulkan_profiles.cpp @@ -0,0 +1,1749 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +import vulkan_hpp; +#include +#include + +#define GLFW_INCLUDE_VULKAN // REQUIRED only for GLFW CreateWindowSurface. +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#define GLM_ENABLE_EXPERIMENTAL +#include +#include +#include + +#define STB_IMAGE_IMPLEMENTATION +#include + +#define TINYOBJLOADER_IMPLEMENTATION +#include + +constexpr uint32_t WIDTH = 800; +constexpr uint32_t HEIGHT = 600; +constexpr uint64_t FenceTimeout = 100000000; +const std::string MODEL_PATH = "models/viking_room.obj"; +const std::string TEXTURE_PATH = "textures/viking_room.png"; +constexpr int MAX_FRAMES_IN_FLIGHT = 2; + +// Application info structure to store profile support flags +struct AppInfo { + bool profileSupported = false; + VpProfileProperties profile; +}; + +// Moved struct definitions inside the class + +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 HelloTriangleApplication { +public: + void run() { + initWindow(); + initVulkan(); + mainLoop(); + cleanup(); + } + +private: + GLFWwindow* window = nullptr; + 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; + vk::raii::Queue graphicsQueue = nullptr; + vk::raii::Queue presentQueue = nullptr; + vk::raii::SwapchainKHR swapChain = nullptr; + std::vector swapChainImages; + vk::Format swapChainImageFormat = {}; + vk::Extent2D swapChainExtent; + std::vector swapChainImageViews; + vk::raii::RenderPass renderPass = nullptr; + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline graphicsPipeline = nullptr; + std::vector swapChainFramebuffers; + vk::raii::CommandPool commandPool = nullptr; + std::vector commandBuffers; + std::vector imageAvailableSemaphores; + std::vector renderFinishedSemaphores; + std::vector inFlightFences; + std::vector presentCompleteSemaphore; + uint32_t currentFrame = 0; + bool framebufferResized = false; + 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::Image textureImage = nullptr; + vk::raii::DeviceMemory textureImageMemory = nullptr; + vk::raii::ImageView textureImageView = nullptr; + vk::raii::Sampler textureSampler = nullptr; + vk::raii::Image depthImage = nullptr; + vk::raii::DeviceMemory depthImageMemory = nullptr; + vk::raii::ImageView depthImageView = nullptr; + std::vector vertices; + std::vector indices; + vk::SampleCountFlagBits msaaSamples = vk::SampleCountFlagBits::e1; + vk::raii::Image colorImage = nullptr; + vk::raii::DeviceMemory colorImageMemory = nullptr; + vk::raii::ImageView colorImageView = nullptr; + + // Application info to store profile support + AppInfo appInfo = {}; + + struct SwapChainSupportDetails { + vk::SurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector presentModes; + }; + + struct QueueFamilyIndices { + std::optional graphicsFamily; + std::optional presentFamily; + + bool isComplete() const { + return graphicsFamily.has_value() && presentFamily.has_value(); + } + }; + + const std::vector requiredDeviceExtension = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME + }; + + void initWindow() { + glfwInit(); + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan Profiles Demo", nullptr, nullptr); + glfwSetWindowUserPointer(window, this); + glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); + } + + static void framebufferResizeCallback(GLFWwindow* window, int, int) { + auto app = reinterpret_cast(glfwGetWindowUserPointer(window)); + app->framebufferResized = true; + } + + void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + checkFeatureSupport(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + + // Create render pass only if not using dynamic rendering + if (!appInfo.profileSupported) { + createRenderPass(); + } + + createDescriptorSetLayout(); + createGraphicsPipeline(); + + // Create framebuffers only if not using dynamic rendering + if (!appInfo.profileSupported) { + createFramebuffers(); + } + + createCommandPool(); + createColorResources(); + createDepthResources(); + createTextureImage(); + createTextureImageView(); + createTextureSampler(); + loadModel(); + createVertexBuffer(); + createIndexBuffer(); + createUniformBuffers(); + createDescriptorPool(); + createDescriptorSets(); + createCommandBuffers(); + createSyncObjects(); + } + + void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } + + device.waitIdle(); + } + + void cleanupSwapChain() { + colorImageView = nullptr; + colorImage = nullptr; + colorImageMemory = nullptr; + + depthImageView = nullptr; + depthImage = nullptr; + depthImageMemory = nullptr; + + for (auto& framebuffer : swapChainFramebuffers) { + framebuffer = nullptr; + } + + for (auto& imageView : swapChainImageViews) { + imageView = nullptr; + } + + swapChain = nullptr; + } + + void cleanup() { + glfwDestroyWindow(window); + glfwTerminate(); + } + + void recreateSwapChain() { + int width = 0, height = 0; + glfwGetFramebufferSize(window, &width, &height); + while (width == 0 || height == 0) { + glfwGetFramebufferSize(window, &width, &height); + glfwWaitEvents(); + } + + device.waitIdle(); + + cleanupSwapChain(); + + createSwapChain(); + createImageViews(); + + // Recreate traditional render pass and framebuffers if not using profiles + if (!appInfo.profileSupported) { + createRenderPass(); + createFramebuffers(); + } + + createColorResources(); + createDepthResources(); + } + + void createInstance() { + + constexpr vk::ApplicationInfo appInfo{ + .pApplicationName = "Vulkan Profiles Demo", + .applicationVersion = VK_MAKE_VERSION(1, 0, 0), + .pEngineName = "No Engine", + .engineVersion = VK_MAKE_VERSION(1, 0, 0), + .apiVersion = vk::ApiVersion14 + }; + + auto extensions = getRequiredExtensions(); + + vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &appInfo, + .enabledExtensionCount = static_cast(extensions.size()), + .ppEnabledExtensionNames = extensions.data() + }; + + instance = vk::raii::Instance(context, createInfo); + + } + + void setupDebugMessenger() { + // Always set up the debug messenger + // It will only be used if validation layers are enabled via vulkanconfig + + vk::DebugUtilsMessageSeverityFlagsEXT severityFlags( + vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eError + ); + + vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags( + vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | + vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | + vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation + ); + + vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT{ + .messageSeverity = severityFlags, + .messageType = messageTypeFlags, + .pfnUserCallback = &debugCallback + }; + + try { + debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfoEXT); + } catch (vk::SystemError& err) { + // If the debug utils extension is not available, this will fail + // That's okay, it just means validation layers aren't enabled + std::cout << "Debug messenger not available. Validation layers may not be enabled." << std::endl; + } + } + + void createSurface() { + VkSurfaceKHR _surface; + if (glfwCreateWindowSurface(*instance, window, nullptr, &_surface) != 0) { + throw std::runtime_error("failed to create window surface!"); + } + surface = vk::raii::SurfaceKHR(instance, _surface); + } + + void pickPhysicalDevice() { + std::vector devices = instance.enumeratePhysicalDevices(); + const auto devIter = std::ranges::find_if( + devices, + [&](auto const & device) + { + // 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; }); + }); + + return supportsGraphics && supportsAllRequiredExtensions; + }); + + if (devIter != devices.end()) { + physicalDevice = *devIter; + msaaSamples = getMaxUsableSampleCount(); + + // Print device information + vk::PhysicalDeviceProperties deviceProperties = physicalDevice.getProperties(); + std::cout << "Selected GPU: " << deviceProperties.deviceName << std::endl; + std::cout << "API Version: " << VK_VERSION_MAJOR(deviceProperties.apiVersion) << "." + << VK_VERSION_MINOR(deviceProperties.apiVersion) << "." + << VK_VERSION_PATCH(deviceProperties.apiVersion) << std::endl; + } else { + throw std::runtime_error("failed to find a suitable GPU!"); + } + } + + void checkFeatureSupport() { + // Define the KHR roadmap 2022 profile - more widely supported than 2024 + appInfo.profile = { + VP_KHR_ROADMAP_2022_NAME, + VP_KHR_ROADMAP_2022_SPEC_VERSION + }; + + // Check if the profile is supported + VkBool32 supported = VK_FALSE; + VkResult result = vpGetPhysicalDeviceProfileSupport( + *instance, + *physicalDevice, + &appInfo.profile, + &supported + ); + + if (result == VK_SUCCESS && supported == VK_TRUE) { + appInfo.profileSupported = true; + std::cout << "Using KHR roadmap 2022 profile" << std::endl; + } else { + appInfo.profileSupported = false; + std::cout << "Falling back to traditional rendering (profile not supported)" << std::endl; + + // If we wanted to implement fallback, we would call detectFeatureSupport() here + // But for this example, we'll just use traditional rendering if the profile isn't supported + } + } + + void createLogicalDevice() { + QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + + std::vector queueCreateInfos; + std::set uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + + float queuePriority = 1.0f; + for (uint32_t queueFamily : uniqueQueueFamilies) { + vk::DeviceQueueCreateInfo queueCreateInfo{ + .queueFamilyIndex = queueFamily, + .queueCount = 1, + .pQueuePriorities = &queuePriority + }; + queueCreateInfos.push_back(queueCreateInfo); + } + + if (appInfo.profileSupported) { + // Create device with Best Practices profile + + // Enable required features + vk::PhysicalDeviceFeatures2 features2; + vk::PhysicalDeviceFeatures deviceFeatures{}; + deviceFeatures.samplerAnisotropy = VK_TRUE; + deviceFeatures.sampleRateShading = VK_TRUE; + features2.features = deviceFeatures; + + // Enable dynamic rendering + vk::PhysicalDeviceDynamicRenderingFeatures dynamicRenderingFeatures; + dynamicRenderingFeatures.dynamicRendering = VK_TRUE; + features2.pNext = &dynamicRenderingFeatures; + + // Create a vk::DeviceCreateInfo with the required features + vk::DeviceCreateInfo vkDeviceCreateInfo{ + .pNext = &features2, + .queueCreateInfoCount = static_cast(queueCreateInfos.size()), + .pQueueCreateInfos = queueCreateInfos.data(), + .enabledExtensionCount = static_cast(requiredDeviceExtension.size()), + .ppEnabledExtensionNames = requiredDeviceExtension.data() + }; + + // Create the device with the vk::DeviceCreateInfo + device = vk::raii::Device(physicalDevice, vkDeviceCreateInfo); + + std::cout << "Created logical device using KHR roadmap 2022 profile" << std::endl; + } else { + // Fallback to manual device creation + vk::PhysicalDeviceFeatures deviceFeatures{}; + deviceFeatures.samplerAnisotropy = VK_TRUE; + deviceFeatures.sampleRateShading = VK_TRUE; + + vk::DeviceCreateInfo createInfo{ + .queueCreateInfoCount = static_cast(queueCreateInfos.size()), + .pQueueCreateInfos = queueCreateInfos.data(), + .enabledExtensionCount = static_cast(requiredDeviceExtension.size()), + .ppEnabledExtensionNames = requiredDeviceExtension.data(), + .pEnabledFeatures = &deviceFeatures + }; + + device = vk::raii::Device(physicalDevice, createInfo); + + std::cout << "Created logical device using manual feature selection" << std::endl; + } + + graphicsQueue = device.getQueue(indices.graphicsFamily.value(), 0); + presentQueue = device.getQueue(indices.presentFamily.value(), 0); + } + + void createSwapChain() { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice); + + vk::SurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats); + vk::PresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes); + vk::Extent2D extent = chooseSwapExtent(swapChainSupport.capabilities); + + uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1; + if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) { + imageCount = swapChainSupport.capabilities.maxImageCount; + } + + vk::SwapchainCreateInfoKHR createInfo{ + .surface = *surface, + .minImageCount = imageCount, + .imageFormat = surfaceFormat.format, + .imageColorSpace = surfaceFormat.colorSpace, + .imageExtent = extent, + .imageArrayLayers = 1, + .imageUsage = vk::ImageUsageFlagBits::eColorAttachment + }; + + QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + + if (indices.graphicsFamily != indices.presentFamily) { + createInfo.imageSharingMode = vk::SharingMode::eConcurrent; + createInfo.queueFamilyIndexCount = 2; + createInfo.pQueueFamilyIndices = queueFamilyIndices; + } else { + createInfo.imageSharingMode = vk::SharingMode::eExclusive; + } + + createInfo.preTransform = swapChainSupport.capabilities.currentTransform; + createInfo.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque; + createInfo.presentMode = presentMode; + createInfo.clipped = VK_TRUE; + + swapChain = device.createSwapchainKHR(createInfo); + swapChainImages = swapChain.getImages(); + swapChainImageFormat = surfaceFormat.format; + swapChainExtent = extent; + } + + void createImageViews() { + swapChainImageViews.reserve(swapChainImages.size()); + + for (const auto& image : swapChainImages) { + swapChainImageViews.push_back(createImageView(image, swapChainImageFormat, vk::ImageAspectFlagBits::eColor, 1)); + } + } + + void createRenderPass() { + // This is only called if the Best Practices profile is not supported + // or if dynamic rendering is not available + vk::AttachmentDescription colorAttachment{ + .format = swapChainImageFormat, + .samples = msaaSamples, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, + .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, + .initialLayout = vk::ImageLayout::eUndefined, + .finalLayout = vk::ImageLayout::eColorAttachmentOptimal + }; + + vk::AttachmentDescription depthAttachment{ + .format = findDepthFormat(), + .samples = msaaSamples, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eDontCare, + .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, + .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, + .initialLayout = vk::ImageLayout::eUndefined, + .finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal + }; + + vk::AttachmentDescription colorAttachmentResolve{ + .format = swapChainImageFormat, + .samples = vk::SampleCountFlagBits::e1, + .loadOp = vk::AttachmentLoadOp::eDontCare, + .storeOp = vk::AttachmentStoreOp::eStore, + .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, + .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, + .initialLayout = vk::ImageLayout::eUndefined, + .finalLayout = vk::ImageLayout::ePresentSrcKHR + }; + + vk::AttachmentReference colorAttachmentRef{ + .attachment = 0, + .layout = vk::ImageLayout::eColorAttachmentOptimal + }; + + vk::AttachmentReference depthAttachmentRef{ + .attachment = 1, + .layout = vk::ImageLayout::eDepthStencilAttachmentOptimal + }; + + vk::AttachmentReference colorAttachmentResolveRef{ + .attachment = 2, + .layout = vk::ImageLayout::eColorAttachmentOptimal + }; + + vk::SubpassDescription subpass{ + .pipelineBindPoint = vk::PipelineBindPoint::eGraphics, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentRef, + .pResolveAttachments = &colorAttachmentResolveRef, + .pDepthStencilAttachment = &depthAttachmentRef + }; + + vk::SubpassDependency dependency{ + .srcSubpass = VK_SUBPASS_EXTERNAL, + .dstSubpass = 0, + .srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests, + .dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests, + .srcAccessMask = vk::AccessFlagBits::eNone, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite + }; + + std::array attachments = {colorAttachment, depthAttachment, colorAttachmentResolve}; + vk::RenderPassCreateInfo renderPassInfo{ + .attachmentCount = static_cast(attachments.size()), + .pAttachments = attachments.data(), + .subpassCount = 1, + .pSubpasses = &subpass, + .dependencyCount = 1, + .pDependencies = &dependency + }; + + renderPass = device.createRenderPass(renderPassInfo); + } + + void createDescriptorSetLayout() { + vk::DescriptorSetLayoutBinding uboLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex + }; + + vk::DescriptorSetLayoutBinding samplerLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment + }; + + std::array bindings = {uboLayoutBinding, samplerLayoutBinding}; + vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data() + }; + + descriptorSetLayout = device.createDescriptorSetLayout(layoutInfo); + } + + void createGraphicsPipeline() { + auto vertShaderCode = readFile("shaders/vert.spv"); + auto fragShaderCode = readFile("shaders/frag.spv"); + + vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); + vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *vertShaderModule, + .pName = "main" + }; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *fragShaderModule, + .pName = "main" + }; + + 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, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .lineWidth = 1.0f + }; + + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = msaaSamples, + .sampleShadingEnable = VK_TRUE, + .minSampleShading = 0.2f + }; + + vk::PipelineDepthStencilStateCreateInfo depthStencil{ + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = vk::CompareOp::eLess, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE + }; + + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + + 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 + }; + + pipelineLayout = device.createPipelineLayout(pipelineLayoutInfo); + + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *pipelineLayout + }; + + // Configure pipeline based on whether we're using the KHR roadmap 2022 profile + if (appInfo.profileSupported) { + // With the KHR roadmap 2022 profile, we can use dynamic rendering + vk::Format colorFormat = swapChainImageFormat; + vk::Format depthFormat = findDepthFormat(); + + vk::PipelineRenderingCreateInfo renderingInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &colorFormat, + .depthAttachmentFormat = depthFormat + }; + + pipelineInfo.pNext = &renderingInfo; + pipelineInfo.renderPass = nullptr; + + std::cout << "Creating pipeline with dynamic rendering (KHR roadmap 2022 profile)" << std::endl; + } else { + // Without the profile, use traditional render pass if dynamic rendering is not available + pipelineInfo.pNext = nullptr; + pipelineInfo.renderPass = *renderPass; + pipelineInfo.subpass = 0; + + std::cout << "Creating pipeline with traditional render pass (fallback)" << std::endl; + } + + graphicsPipeline = device.createGraphicsPipeline(nullptr, pipelineInfo); + } + + void createFramebuffers() { + // This is only called if the Best Practices profile is not supported + // or if dynamic rendering is not available + swapChainFramebuffers.reserve(swapChainImageViews.size()); + + for (size_t i = 0; i < swapChainImageViews.size(); i++) { + std::array attachments = { + *colorImageView, + *depthImageView, + *swapChainImageViews[i] + }; + + vk::FramebufferCreateInfo framebufferInfo{ + .renderPass = *renderPass, + .attachmentCount = static_cast(attachments.size()), + .pAttachments = attachments.data(), + .width = swapChainExtent.width, + .height = swapChainExtent.height, + .layers = 1 + }; + + swapChainFramebuffers.push_back(device.createFramebuffer(framebufferInfo)); + } + } + + void createCommandPool() { + QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice); + + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.graphicsFamily.value() + }; + + commandPool = device.createCommandPool(poolInfo); + } + + void createColorResources() { + vk::Format colorFormat = swapChainImageFormat; + + createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, colorFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransientAttachment | vk::ImageUsageFlagBits::eColorAttachment, vk::MemoryPropertyFlagBits::eDeviceLocal, colorImage, colorImageMemory); + colorImageView = createImageView(*colorImage, colorFormat, vk::ImageAspectFlagBits::eColor, 1); + } + + void createDepthResources() { + vk::Format depthFormat = findDepthFormat(); + + createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, depthFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eDepthStencilAttachment, vk::MemoryPropertyFlagBits::eDeviceLocal, depthImage, depthImageMemory); + depthImageView = createImageView(*depthImage, depthFormat, vk::ImageAspectFlagBits::eDepth, 1); + } + + vk::Format findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features) { + for (vk::Format format : candidates) { + vk::FormatProperties props = physicalDevice.getFormatProperties(format); + + if (tiling == vk::ImageTiling::eLinear && (props.linearTilingFeatures & features) == features) { + return format; + } else if (tiling == vk::ImageTiling::eOptimal && (props.optimalTilingFeatures & features) == features) { + return format; + } + } + + throw std::runtime_error("failed to find supported format!"); + } + + vk::Format findDepthFormat() { + return findSupportedFormat( + {vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint}, + vk::ImageTiling::eOptimal, + vk::FormatFeatureFlagBits::eDepthStencilAttachment + ); + } + + bool hasStencilComponent(vk::Format format) { + return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint; + } + + void createTextureImage() { + int texWidth, texHeight, texChannels; + stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + vk::DeviceSize imageSize = texWidth * texHeight * 4; + uint32_t mipLevels = static_cast(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1; + + if (!pixels) { + throw std::runtime_error("failed to load texture image!"); + } + + vk::raii::Buffer stagingBuffer = nullptr; + vk::raii::DeviceMemory stagingBufferMemory = nullptr; + + createBuffer(imageSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); + + void* data = stagingBufferMemory.mapMemory(0, imageSize); + memcpy(data, pixels, static_cast(imageSize)); + stagingBufferMemory.unmapMemory(); + + stbi_image_free(pixels); + + createImage(texWidth, texHeight, mipLevels, vk::SampleCountFlagBits::e1, vk::Format::eR8G8B8A8Srgb, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal, textureImage, textureImageMemory); + + transitionImageLayout(*textureImage, vk::Format::eR8G8B8A8Srgb, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, mipLevels); + copyBufferToImage(*stagingBuffer, *textureImage, static_cast(texWidth), static_cast(texHeight)); + + generateMipmaps(*textureImage, vk::Format::eR8G8B8A8Srgb, texWidth, texHeight, mipLevels); + } + + void generateMipmaps(vk::Image image, vk::Format imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) { + vk::FormatProperties formatProperties = physicalDevice.getFormatProperties(imageFormat); + + if (!(formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImageFilterLinear)) { + throw std::runtime_error("texture image format does not support linear blitting!"); + } + + vk::raii::CommandBuffer commandBuffer = beginSingleTimeCommands(); + + vk::ImageMemoryBarrier barrier{ + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + } + }; + + int32_t mipWidth = texWidth; + int32_t mipHeight = texHeight; + + for (uint32_t i = 1; i < mipLevels; i++) { + barrier.subresourceRange.baseMipLevel = i - 1; + barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal; + barrier.newLayout = vk::ImageLayout::eTransferSrcOptimal; + barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + barrier.dstAccessMask = vk::AccessFlagBits::eTransferRead; + + commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eTransfer, + {}, + std::array{}, + std::array{}, + std::array{barrier}); + + vk::ImageBlit blit{ + .srcSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = i - 1, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .srcOffsets = std::array{ + vk::Offset3D{0, 0, 0}, + vk::Offset3D{mipWidth, mipHeight, 1} + }, + .dstSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = i, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .dstOffsets = std::array{ + vk::Offset3D{0, 0, 0}, + vk::Offset3D{mipWidth > 1 ? mipWidth / 2 : 1, mipHeight > 1 ? mipHeight / 2 : 1, 1} + } + }; + + commandBuffer.blitImage( + image, vk::ImageLayout::eTransferSrcOptimal, + image, vk::ImageLayout::eTransferDstOptimal, + std::array{blit}, + vk::Filter::eLinear); + + barrier.oldLayout = vk::ImageLayout::eTransferSrcOptimal; + barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + barrier.srcAccessMask = vk::AccessFlagBits::eTransferRead; + barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; + + commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eFragmentShader, + {}, + std::array{}, + std::array{}, + std::array{barrier}); + + if (mipWidth > 1) mipWidth /= 2; + if (mipHeight > 1) mipHeight /= 2; + } + + barrier.subresourceRange.baseMipLevel = mipLevels - 1; + barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal; + barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; + + commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eFragmentShader, + {}, + std::array{}, + std::array{}, + std::array{barrier}); + + endSingleTimeCommands(commandBuffer); + } + + vk::raii::ImageView createImageView(vk::Image image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels) { + vk::ImageViewCreateInfo viewInfo{ + .image = image, + .viewType = vk::ImageViewType::e2D, + .format = format, + .subresourceRange = { + .aspectMask = aspectFlags, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + return device.createImageView(viewInfo); + } + + void createTextureImageView() { + textureImageView = createImageView(*textureImage, vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor, 1); + } + + 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, + .minLod = 0.0f, + .maxLod = 0.0f, + .borderColor = vk::BorderColor::eIntOpaqueBlack, + .unnormalizedCoordinates = VK_FALSE + }; + + textureSampler = device.createSampler(samplerInfo); + } + + void createVertexBuffer() { + vk::DeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); + + vk::raii::Buffer stagingBuffer = nullptr; + vk::raii::DeviceMemory stagingBufferMemory = nullptr; + createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); + + void* data = stagingBufferMemory.mapMemory(0, bufferSize); + memcpy(data, vertices.data(), (size_t) 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 = nullptr; + vk::raii::DeviceMemory stagingBufferMemory = nullptr; + createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); + + void* data = stagingBufferMemory.mapMemory(0, bufferSize); + memcpy(data, indices.data(), (size_t) bufferSize); + stagingBufferMemory.unmapMemory(); + + createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal, indexBuffer, indexBufferMemory); + + copyBuffer(*stagingBuffer, *indexBuffer, bufferSize); + } + + void createUniformBuffers() { + vk::DeviceSize bufferSize = sizeof(UniformBufferObject); + + // Reserve space but don't resize, as RAII objects can't be default-constructed + uniformBuffers.reserve(MAX_FRAMES_IN_FLIGHT); + uniformBuffersMemory.reserve(MAX_FRAMES_IN_FLIGHT); + uniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vk::raii::Buffer buffer = nullptr; + vk::raii::DeviceMemory bufferMemory = nullptr; + createBuffer(bufferSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, buffer, bufferMemory); + + uniformBuffers.push_back(std::move(buffer)); + uniformBuffersMemory.push_back(std::move(bufferMemory)); + uniformBuffersMapped[i] = uniformBuffersMemory[i].mapMemory(0, bufferSize); + } + } + + void createDescriptorPool() { + std::array poolSizes{}; + poolSizes[0].type = vk::DescriptorType::eUniformBuffer; + poolSizes[0].descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); + poolSizes[1].type = vk::DescriptorType::eCombinedImageSampler; + poolSizes[1].descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); + + vk::DescriptorPoolCreateInfo poolInfo{ + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = static_cast(MAX_FRAMES_IN_FLIGHT), + .poolSizeCount = static_cast(poolSizes.size()), + .pPoolSizes = poolSizes.data() + }; + + descriptorPool = device.createDescriptorPool(poolInfo); + } + + void createDescriptorSets() { + std::vector layouts(MAX_FRAMES_IN_FLIGHT, *descriptorSetLayout); + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT), + .pSetLayouts = layouts.data() + }; + + 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{}; + + descriptorWrites[0].dstSet = *descriptorSets[i]; + descriptorWrites[0].dstBinding = 0; + descriptorWrites[0].dstArrayElement = 0; + descriptorWrites[0].descriptorType = vk::DescriptorType::eUniformBuffer; + descriptorWrites[0].descriptorCount = 1; + descriptorWrites[0].pBufferInfo = &bufferInfo; + + descriptorWrites[1].dstSet = *descriptorSets[i]; + descriptorWrites[1].dstBinding = 1; + descriptorWrites[1].dstArrayElement = 0; + descriptorWrites[1].descriptorType = vk::DescriptorType::eCombinedImageSampler; + descriptorWrites[1].descriptorCount = 1; + descriptorWrites[1].pImageInfo = &imageInfo; + + device.updateDescriptorSets(descriptorWrites, nullptr); + } + } + + 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 = device.createBuffer(bufferInfo); + + vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) + }; + + bufferMemory = device.allocateMemory(allocInfo); + buffer.bindMemory(*bufferMemory, 0); + } + + void copyBuffer(vk::Buffer srcBuffer, vk::Buffer dstBuffer, vk::DeviceSize size) { + vk::raii::CommandBuffer commandBuffer = beginSingleTimeCommands(); + + vk::BufferCopy copyRegion{ + .size = size + }; + commandBuffer.copyBuffer(srcBuffer, dstBuffer, copyRegion); + + endSingleTimeCommands(commandBuffer); + } + + void copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height) { + vk::raii::CommandBuffer commandBuffer = beginSingleTimeCommands(); + + vk::BufferImageCopy region{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = { + width, + height, + 1 + } + }; + + commandBuffer.copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, region); + + endSingleTimeCommands(commandBuffer); + } + + void createImage(uint32_t width, uint32_t height, uint32_t mipLevels, vk::SampleCountFlagBits numSamples, 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 = width, + .height = height, + .depth = 1 + }, + .mipLevels = mipLevels, + .arrayLayers = 1, + .samples = numSamples, + .tiling = tiling, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined + }; + + image = device.createImage(imageInfo); + + vk::MemoryRequirements memRequirements = image.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) + }; + + imageMemory = device.allocateMemory(allocInfo); + image.bindMemory(*imageMemory, 0); + } + + void transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels) { + vk::raii::CommandBuffer commandBuffer = beginSingleTimeCommands(); + + vk::ImageMemoryBarrier barrier{ + .oldLayout = oldLayout, + .newLayout = newLayout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = { + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + if (newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) { + barrier.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eDepth; + + if (hasStencilComponent(format)) { + barrier.subresourceRange.aspectMask |= vk::ImageAspectFlagBits::eStencil; + } + } else { + barrier.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; + } + + vk::PipelineStageFlags sourceStage; + vk::PipelineStageFlags destinationStage; + + if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { + barrier.srcAccessMask = vk::AccessFlagBits::eNone; + 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 if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) { + barrier.srcAccessMask = vk::AccessFlagBits::eNone; + barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite; + + sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; + destinationStage = vk::PipelineStageFlagBits::eEarlyFragmentTests; + } else { + throw std::invalid_argument("unsupported layout transition!"); + } + + commandBuffer.pipelineBarrier( + sourceStage, + destinationStage, + {}, + std::array{}, + std::array{}, + std::array{barrier} + ); + + endSingleTimeCommands(commandBuffer); + } + + vk::raii::CommandBuffer beginSingleTimeCommands() { + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + vk::raii::CommandBuffer commandBuffer = std::move(device.allocateCommandBuffers(allocInfo).front()); + + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + + commandBuffer.begin(beginInfo); + + return commandBuffer; + } + + void endSingleTimeCommands(vk::raii::CommandBuffer& commandBuffer) { + commandBuffer.end(); + + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + + graphicsQueue.submit(submitInfo, nullptr); + graphicsQueue.waitIdle(); + } + + void loadModel() { + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + + if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, MODEL_PATH.c_str())) { + throw std::runtime_error(warn + err); + } + + std::unordered_map uniqueVertices{}; + + for (const auto& shape : shapes) { + for (const auto& index : shape.mesh.indices) { + Vertex vertex{}; + + vertex.pos = { + attrib.vertices[3 * index.vertex_index + 0], + attrib.vertices[3 * index.vertex_index + 1], + attrib.vertices[3 * index.vertex_index + 2] + }; + + vertex.texCoord = { + attrib.texcoords[2 * index.texcoord_index + 0], + 1.0f - attrib.texcoords[2 * index.texcoord_index + 1] + }; + + vertex.color = {1.0f, 1.0f, 1.0f}; + + if (uniqueVertices.count(vertex) == 0) { + uniqueVertices[vertex] = static_cast(vertices.size()); + vertices.push_back(vertex); + } + + indices.push_back(uniqueVertices[vertex]); + } + } + } + + void createCommandBuffers() { + commandBuffers.reserve(MAX_FRAMES_IN_FLIGHT); + + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = static_cast(MAX_FRAMES_IN_FLIGHT) + }; + + commandBuffers = device.allocateCommandBuffers(allocInfo); + } + + void recordCommandBuffer(uint32_t imageIndex) { + commandBuffers[currentFrame].begin({}); + + // Transition the swapchain image to the correct layout for rendering + vk::ImageMemoryBarrier imageBarrier{ + .srcAccessMask = vk::AccessFlagBits::eNone, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .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 + } + }; + + commandBuffers[currentFrame].pipelineBarrier( + vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::DependencyFlagBits::eByRegion, + std::array{}, + std::array{}, + std::array{imageBarrier} + ); + + // Clear values for color and depth + vk::ClearValue clearColor{}; + clearColor.color = vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f}); + + vk::ClearValue clearDepth{}; + clearDepth.depthStencil = vk::ClearDepthStencilValue{1.0f, 0}; + + std::array clearValues = {clearColor, clearDepth}; + + // Use different rendering approach based on profile support + if (appInfo.profileSupported) { + // Use dynamic rendering with the KHR roadmap 2022 profile + vk::RenderingAttachmentInfo colorAttachment{ + .imageView = *colorImageView, + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .resolveMode = vk::ResolveModeFlagBits::eAverage, + .resolveImageView = *swapChainImageViews[imageIndex], + .resolveImageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearColor + }; + + vk::RenderingAttachmentInfo depthAttachment{ + .imageView = *depthImageView, + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eDontCare, + .clearValue = clearDepth + }; + + vk::RenderingInfo renderingInfo{ + .renderArea = {{0, 0}, swapChainExtent}, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachment, + .pDepthAttachment = &depthAttachment + }; + + commandBuffers[currentFrame].beginRendering(renderingInfo); + + } else { + // Use traditional render pass if not using the KHR roadmap 2022 profile + vk::RenderPassBeginInfo renderPassInfo{ + .renderPass = *renderPass, + .framebuffer = *swapChainFramebuffers[imageIndex], + .renderArea = {{0, 0}, swapChainExtent}, + .clearValueCount = static_cast(clearValues.size()), + .pClearValues = clearValues.data() + }; + + commandBuffers[currentFrame].beginRenderPass(renderPassInfo, vk::SubpassContents::eInline); + + } + + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, *graphicsPipeline); + + vk::Viewport viewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(swapChainExtent.width), + .height = static_cast(swapChainExtent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f + }; + commandBuffers[currentFrame].setViewport(0, viewport); + + vk::Rect2D scissor{ + .offset = {0, 0}, + .extent = swapChainExtent + }; + commandBuffers[currentFrame].setScissor(0, scissor); + + 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(static_cast(indices.size()), 1, 0, 0, 0); + + if (appInfo.profileSupported) { + commandBuffers[currentFrame].endRendering(); + + // Transition the swapchain image to the correct layout for presentation + vk::ImageMemoryBarrier barrier{ + .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + .dstAccessMask = vk::AccessFlagBits::eNone, + .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, + .newLayout = vk::ImageLayout::ePresentSrcKHR, + .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 + } + }; + + commandBuffers[currentFrame].pipelineBarrier( + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::PipelineStageFlagBits::eBottomOfPipe, + vk::DependencyFlagBits::eByRegion, + std::array{}, + std::array{}, + std::array{barrier} + ); + } else { + commandBuffers[currentFrame].endRenderPass(); + // Traditional render pass already transitions the image to the correct layout + } + + commandBuffers[currentFrame].end(); + } + + void createSyncObjects() { + imageAvailableSemaphores.reserve(MAX_FRAMES_IN_FLIGHT); + renderFinishedSemaphores.reserve(MAX_FRAMES_IN_FLIGHT); + inFlightFences.reserve(MAX_FRAMES_IN_FLIGHT); + presentCompleteSemaphore.reserve(swapChainImages.size()); + + vk::SemaphoreCreateInfo semaphoreInfo{}; + vk::FenceCreateInfo fenceInfo{ + .flags = vk::FenceCreateFlagBits::eSignaled + }; + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + imageAvailableSemaphores.push_back(device.createSemaphore(semaphoreInfo)); + renderFinishedSemaphores.push_back(device.createSemaphore(semaphoreInfo)); + inFlightFences.push_back(device.createFence(fenceInfo)); + } + + for (size_t i = 0; i < swapChainImages.size(); i++) { + presentCompleteSemaphore.push_back(device.createSemaphore(semaphoreInfo)); + } + } + + void updateUniformBuffer(uint32_t currentImage) { + 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{}; + ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); + ubo.view = glm::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), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f); + ubo.proj[1][1] *= -1; + + memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); + } + + void drawFrame() { + static_cast(device.waitForFences({*inFlightFences[currentFrame]}, VK_TRUE, FenceTimeout)); + + uint32_t imageIndex; + try { + auto [result, idx] = swapChain.acquireNextImage(FenceTimeout, *imageAvailableSemaphores[currentFrame]); + imageIndex = idx; + } catch (vk::OutOfDateKHRError&) { + recreateSwapChain(); + return; + } + + updateUniformBuffer(currentFrame); + + device.resetFences({*inFlightFences[currentFrame]}); + + commandBuffers[currentFrame].reset(); + recordCommandBuffer(imageIndex); + + vk::PipelineStageFlags waitDestinationStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput); + const vk::SubmitInfo submitInfo{ + .waitSemaphoreCount = 1, + .pWaitSemaphores = &*imageAvailableSemaphores[currentFrame], + .pWaitDstStageMask = &waitDestinationStageMask, + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffers[currentFrame], + .signalSemaphoreCount = 1, + .pSignalSemaphores = &*presentCompleteSemaphore[imageIndex] + }; + graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); + + const vk::PresentInfoKHR presentInfoKHR{ + .waitSemaphoreCount = 1, + .pWaitSemaphores = &*presentCompleteSemaphore[imageIndex], + .swapchainCount = 1, + .pSwapchains = &*swapChain, + .pImageIndices = &imageIndex + }; + + vk::Result result; + try { + result = presentQueue.presentKHR(presentInfoKHR); + } catch (vk::OutOfDateKHRError&) { + result = vk::Result::eErrorOutOfDateKHR; + } + + 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; + } + + vk::SampleCountFlagBits getMaxUsableSampleCount() { + vk::PhysicalDeviceProperties physicalDeviceProperties = physicalDevice.getProperties(); + + vk::SampleCountFlags counts = physicalDeviceProperties.limits.framebufferColorSampleCounts & physicalDeviceProperties.limits.framebufferDepthSampleCounts; + if (counts & vk::SampleCountFlagBits::e64) { return vk::SampleCountFlagBits::e64; } + if (counts & vk::SampleCountFlagBits::e32) { return vk::SampleCountFlagBits::e32; } + if (counts & vk::SampleCountFlagBits::e16) { return vk::SampleCountFlagBits::e16; } + if (counts & vk::SampleCountFlagBits::e8) { return vk::SampleCountFlagBits::e8; } + if (counts & vk::SampleCountFlagBits::e4) { return vk::SampleCountFlagBits::e4; } + if (counts & vk::SampleCountFlagBits::e2) { return vk::SampleCountFlagBits::e2; } + + return vk::SampleCountFlagBits::e1; + } + + 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!"); + } + + vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { + return (availableFormats[0].format == vk::Format::eUndefined) + ? vk::SurfaceFormatKHR{vk::Format::eB8G8R8A8Unorm, availableFormats[0].colorSpace} + : availableFormats[0]; + } + + 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; + } + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + return { + std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), + std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height) + }; + } + + std::vector getRequiredExtensions() { + // Get the required extensions from GLFW + uint32_t glfwExtensionCount = 0; + auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); + + // Check if the debug utils extension is available + std::vector props = context.enumerateInstanceExtensionProperties(); + bool debugUtilsAvailable = std::ranges::any_of(props, + [](vk::ExtensionProperties const & ep) { + return strcmp(ep.extensionName, vk::EXTDebugUtilsExtensionName) == 0; + }); + + // Always include the debug utils extension if available + // This allows validation layers to be enabled via vulkanconfig + if (debugUtilsAvailable) { + extensions.push_back(vk::EXTDebugUtilsExtensionName); + } else { + std::cout << "VK_EXT_debug_utils extension not available. Validation layers may not work." << std::endl; + } + + return extensions; + } + + 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; + } + + vk::raii::ShaderModule createShaderModule(const std::vector& code) { + vk::ShaderModuleCreateInfo createInfo{ .codeSize = code.size(), .pCode = reinterpret_cast(code.data()) }; + vk::raii::ShaderModule shaderModule{ device, createInfo }; + + return shaderModule; + } + + static std::vector readFile(const std::string& filename) { + std::ifstream file(filename, std::ios::ate | std::ios::binary); + + if (!file.is_open()) { + throw std::runtime_error("failed to open file!"); + } + std::vector buffer(file.tellg()); + file.seekg(0, std::ios::beg); + file.read(buffer.data(), static_cast(buffer.size())); + file.close(); + + return buffer; + } + + SwapChainSupportDetails querySwapChainSupport(vk::raii::PhysicalDevice device) { + SwapChainSupportDetails details; + details.capabilities = device.getSurfaceCapabilitiesKHR(*surface); + details.formats = device.getSurfaceFormatsKHR(*surface); + details.presentModes = device.getSurfacePresentModesKHR(*surface); + + return details; + } + + QueueFamilyIndices findQueueFamilies(vk::raii::PhysicalDevice device) { + QueueFamilyIndices indices; + + std::vector queueFamilies = device.getQueueFamilyProperties(); + + uint32_t i = 0; + for (const auto& queueFamily : queueFamilies) { + if (queueFamily.queueFlags & vk::QueueFlagBits::eGraphics) { + indices.graphicsFamily = i; + } + + vk::Bool32 presentSupport = device.getSurfaceSupportKHR(i, *surface); + + if (presentSupport) { + indices.presentFamily = i; + } + + if (indices.isComplete()) { + break; + } + + i++; + } + + return indices; + } +}; + +int main() { + try { + HelloTriangleApplication app; + app.run(); + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/attachments/CMakeLists.txt b/attachments/CMakeLists.txt index efe2ea58..75a70109 100644 --- a/attachments/CMakeLists.txt +++ b/attachments/CMakeLists.txt @@ -231,3 +231,15 @@ add_chapter (30_multisampling add_chapter (31_compute_shader SHADER 31_shader_compute LIBS glm::glm) + +add_chapter (32_ecosystem_utilities + SHADER 27_shader_depth + MODELS viking_room.obj + TEXTURES viking_room.png + LIBS glm::glm tinyobjloader::tinyobjloader) + +add_chapter (33_vulkan_profiles + SHADER 27_shader_depth + MODELS viking_room.obj + TEXTURES viking_room.png + LIBS glm::glm tinyobjloader::tinyobjloader) diff --git a/en/12_Ecosystem_Utilities_and_Compatibility.adoc b/en/12_Ecosystem_Utilities_and_Compatibility.adoc new file mode 100644 index 00000000..e8f671da --- /dev/null +++ b/en/12_Ecosystem_Utilities_and_Compatibility.adoc @@ -0,0 +1,542 @@ +:pp: {plus}{plus} + += Ecosystem Utilities and GPU Compatibility + +== Introduction + +In this chapter, we'll explore important ecosystem utilities for Vulkan development and learn how to adapt our code to support a wider range of GPUs. As Vulkan continues to evolve with new versions and features, it's important to understand how to: + +1. Discover what features are supported by different GPUs +2. Modify your code to maintain compatibility with older hardware +3. Conditionally use advanced features when available + +This knowledge is essential for developing Vulkan applications that can run on a diverse range of hardware, from the latest high-end GPUs to older or more limited devices. + +== Vulkan Hardware Database (GPUInfo.org) + +=== Introduction to GPUInfo.org + +The link:https://vulkan.gpuinfo.org/[Vulkan Hardware Database] (GPUInfo.org) is an invaluable resource for Vulkan developers. This community-driven database collects and presents information about Vulkan support across a wide range of GPUs and devices. + +GPUInfo.org provides detailed information about: + +* Supported Vulkan versions +* Available extensions +* Feature support +* Implementation limits +* Format properties +* Queue family properties + +This information is crowdsourced from users who run the Vulkan Hardware Capability Viewer tool, which reports their GPU's capabilities to the database. + +=== Using GPUInfo.org for Development + +When developing a Vulkan application, GPUInfo.org can help you: + +1. *Determine minimum requirements*: Understand what Vulkan version and extensions you need to target to support your desired range of hardware. + +2. *Check feature availability*: Verify if specific features like dynamic rendering, timeline semaphores, or ray tracing are widely supported. + +3. *Identify implementation limits*: Discover the practical limits of various Vulkan features across different hardware. + +4. *Compare vendors and devices*: Understand the differences in Vulkan support between NVIDIA, AMD, Intel, and mobile GPU vendors. + +Let's look at some practical examples of using GPUInfo.org: + +==== Example: Checking Vulkan Version Support + +To determine how widely supported Vulkan 1.3 (which introduced dynamic rendering) is: + +1. Visit link:https://vulkan.gpuinfo.org/[GPUInfo.org] +2. Navigate to "Core Version Support" +3. Check the percentage of devices supporting Vulkan 1.3 + +You'll find that while newer GPUs support Vulkan 1.3+, there are still many devices limited to Vulkan 1.0, 1.1, or 1.2. + +==== Example: Checking Extension Support + +If you're considering using a specific extension: + +1. Visit the "Extensions" section +2. Search for your extension of interest +3. Check its support percentage across different vendors + +This helps you decide whether to require the extension or provide a fallback path. + +==== Example: Using the vulkanconfig Tool + +The `vulkanconfig` tool is included in the Vulkan SDK and provides a convenient way to configure Vulkan settings on your system. Here's how to use it: + +1. *Launch vulkanconfig*: Open a terminal or command prompt and run `vulkanconfig` + +2. *Configure Validation Layers*: + - Navigate to the "Layers" tab + - Enable or disable specific validation layers based on your debugging needs + - For example, enable `VK_LAYER_KHRONOS_validation` during development to catch API usage errors + +3. *Manage Environment Variables*: + - Go to the "Settings" tab + - Set environment variables like `VK_LAYER_PATH` or `VK_ICD_FILENAMES` + - These settings can be applied system-wide or for the current session + +4. *Configure Driver-specific Options*: + - Some GPU vendors provide additional configuration options + - These can be accessed through the vendor-specific tabs + +5. *Export Configuration*: + - Save your configuration for later use or to share with team members + - This ensures consistent Vulkan environments across development machines + +Using `vulkanconfig` is particularly helpful when: +- Debugging Vulkan applications with different validation layer configurations +- Testing your application with different Vulkan settings without modifying code +- Setting up a development environment with specific Vulkan requirements + +==== Using vulkanconfig for Validation Layers Instead of Code + +In many Vulkan applications, validation layers are enabled programmatically during instance creation, typically only in debug builds. Here's how this is commonly done: + +[,c++] +---- +// Define validation layers +const std::vector validationLayers = { + "VK_LAYER_KHRONOS_validation" +}; + +// Enable only in debug builds +#ifdef NDEBUG +constexpr bool enableValidationLayers = false; +#else +constexpr bool enableValidationLayers = true; +#endif + +void createInstance() { + // Check if validation layers are available + if (enableValidationLayers && !checkValidationLayerSupport()) { + throw std::runtime_error("validation layers requested, but not available!"); + } + + // Application info... + + // Enable validation layers if in debug mode + std::vector enabledLayers; + if (enableValidationLayers) { + enabledLayers.assign(validationLayers.begin(), validationLayers.end()); + } + + // Create instance with validation layers + vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &appInfo, + .enabledLayerCount = static_cast(enabledLayers.size()), + .ppEnabledLayerNames = enabledLayers.data(), + // ... other parameters + }; + + instance = vk::raii::Instance(context, createInfo); +} +---- + +While this approach works, it has several drawbacks: + +1. It requires modifying and recompiling code to enable/disable validation +2. It's harder to experiment with different validation layer configurations +3. It adds complexity to your codebase + +A better approach is to use `vulkanconfig` to manage validation layers externally. Here's how to modify your code to take advantage of this: + +[,c++] +---- +void createInstance() { + // Application info... + + // Create instance without explicitly enabling validation layers + vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &appInfo, + // ... other parameters + }; + + instance = vk::raii::Instance(context, createInfo); +} +---- + +With this approach: + +1. You remove all validation layer-specific code from your application +2. You use `vulkanconfig` to enable validation layers when needed +3. You can switch validation configurations without recompiling + +To enable validation layers with `vulkanconfig`: + +1. Launch `vulkanconfig` +2. Go to the "Layers" tab +3. Enable the `VK_LAYER_KHRONOS_validation` layer +4. Apply the settings + +This configuration will apply to all Vulkan applications run in that environment, making it easy to toggle validation on and off without code changes. + +The benefits of this approach include: + +* *Cleaner code*: Your application code doesn't need to handle validation layers +* *Flexibility*: Change validation settings without recompiling +* *Consistency*: Apply the same validation settings across multiple applications +* *Experimentation*: Easily try different validation configurations + +=== Other Useful Ecosystem Tools + +Besides GPUInfo.org, several other tools can help you develop and debug Vulkan applications: + +* *Vulkan SDK Tools*: +** `vulkaninfo`: Displays Vulkan capabilities of your local system +** `vulkanconfig`: A configuration tool for managing Vulkan settings (see <> for details) +** Validation layers: Help identify API usage errors +** RenderDoc: Graphics debugging tool + +* *Vendor-specific Tools*: +** NVIDIA Nsight Graphics +** AMD Radeon GPU Profiler +** Intel Graphics Performance Analyzers + +== Supporting Older GPUs + +Now that we understand how to discover GPU capabilities, let's explore how to modify our code to support older GPUs that don't have Vulkan 1.3/1.4 features like dynamic rendering. + +=== Detecting Available Features + +The first step is to detect what features are available on the user's GPU. This is done during device creation: + +[,c++] +---- +// Check if dynamic rendering is supported +bool dynamicRenderingSupported = false; + +// Check for Vulkan 1.3 support +if (deviceProperties.apiVersion >= VK_VERSION_1_3) { + dynamicRenderingSupported = true; +} else { + // Check for the extension on older Vulkan versions + for (const auto& extension : availableExtensions) { + if (strcmp(extension.extensionName, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME) == 0) { + dynamicRenderingSupported = true; + break; + } + } +} + +// Store this information for later use +appInfo.dynamicRenderingSupported = dynamicRenderingSupported; +---- + +=== Alternative to Dynamic Rendering: Traditional Render Passes + +If dynamic rendering isn't available, we need to use traditional render passes and framebuffers. Here's how to implement this alternative approach: + +==== Creating a Render Pass + +[,c++] +---- +void createRenderPass() { + if (appInfo.dynamicRenderingSupported) { + // No render pass needed with dynamic rendering + return; + } + + // Color attachment description + vk::AttachmentDescription colorAttachment{ + .format = swapChainImageFormat, + .samples = vk::SampleCountFlagBits::e1, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, + .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, + .initialLayout = vk::ImageLayout::eUndefined, + .finalLayout = vk::ImageLayout::ePresentSrcKHR + }; + + // Subpass reference to the color attachment + vk::AttachmentReference colorAttachmentRef{ + .attachment = 0, + .layout = vk::ImageLayout::eColorAttachmentOptimal + }; + + // Subpass description + vk::SubpassDescription subpass{ + .pipelineBindPoint = vk::PipelineBindPoint::eGraphics, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentRef + }; + + // Dependency to ensure proper image layout transitions + vk::SubpassDependency dependency{ + .srcSubpass = VK_SUBPASS_EXTERNAL, + .dstSubpass = 0, + .srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput, + .dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput, + .srcAccessMask = vk::AccessFlagBits::eNone, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite + }; + + // Create the render pass + vk::RenderPassCreateInfo renderPassInfo{ + .attachmentCount = 1, + .pAttachments = &colorAttachment, + .subpassCount = 1, + .pSubpasses = &subpass, + .dependencyCount = 1, + .pDependencies = &dependency + }; + + renderPass = device.createRenderPass(renderPassInfo); +} +---- + +==== Creating Framebuffers + +[,c++] +---- +void createFramebuffers() { + if (appInfo.dynamicRenderingSupported) { + // No framebuffers needed with dynamic rendering + return; + } + + swapChainFramebuffers.resize(swapChainImageViews.size()); + + for (size_t i = 0; i < swapChainImageViews.size(); i++) { + vk::ImageView attachments[] = { + swapChainImageViews[i] + }; + + vk::FramebufferCreateInfo framebufferInfo{ + .renderPass = renderPass, + .attachmentCount = 1, + .pAttachments = attachments, + .width = swapChainExtent.width, + .height = swapChainExtent.height, + .layers = 1 + }; + + swapChainFramebuffers[i] = device.createFramebuffer(framebufferInfo); + } +} +---- + +==== Modifying Pipeline Creation + +When creating the graphics pipeline, we need to specify the render pass if dynamic rendering isn't available: + +[,c++] +---- +void createGraphicsPipeline() { + // ... existing shader stage and fixed function setup ... + + vk::GraphicsPipelineCreateInfo pipelineInfo{}; + + if (appInfo.dynamicRenderingSupported) { + // Use dynamic rendering + vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat + }; + + pipelineInfo.pNext = &pipelineRenderingCreateInfo; + pipelineInfo.renderPass = nullptr; + } else { + // Use traditional render pass + pipelineInfo.pNext = nullptr; + pipelineInfo.renderPass = renderPass; + pipelineInfo.subpass = 0; + } + + // ... rest of pipeline creation ... +} +---- + +==== Adapting Command Buffer Recording + +Finally, we need to modify how we record command buffers: + +[,c++] +---- +void recordCommandBuffer(vk::CommandBuffer commandBuffer, uint32_t imageIndex) { + // ... begin command buffer ... + + if (appInfo.dynamicRenderingSupported) { + // Begin dynamic rendering + vk::RenderingAttachmentInfo colorAttachment{ + .imageView = swapChainImageViews[imageIndex], + .imageLayout = vk::ImageLayout::eAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearColor + }; + + vk::RenderingInfo renderingInfo{ + .renderArea = {{0, 0}, swapChainExtent}, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachment + }; + + commandBuffer.beginRendering(renderingInfo); + } else { + // Begin traditional render pass + vk::RenderPassBeginInfo renderPassInfo{ + .renderPass = renderPass, + .framebuffer = swapChainFramebuffers[imageIndex], + .renderArea = {{0, 0}, swapChainExtent}, + .clearValueCount = 1, + .pClearValues = &clearColor + }; + + commandBuffer.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline); + } + + // ... bind pipeline and draw ... + + if (appInfo.dynamicRenderingSupported) { + commandBuffer.endRendering(); + } else { + commandBuffer.endRenderPass(); + } + + // ... end command buffer ... +} +---- + +=== Handling Other Vulkan 1.3/1.4 Features + +Dynamic rendering is just one example of a feature that might not be available on older GPUs. Here are some other Vulkan 1.3/1.4 features you might need to provide alternatives for: + +==== Timeline Semaphores + +Timeline semaphores (introduced in Vulkan 1.2) provide a more flexible synchronization mechanism than binary semaphores. If they're not available, you'll need to use binary semaphores and fences: + +[,c++] +---- +bool timelineSemaphoresSupported = false; + +// Check for Vulkan 1.2 support or extension +if (deviceProperties.apiVersion >= VK_VERSION_1_2) { + timelineSemaphoresSupported = true; +} else { + // Check for extension + for (const auto& extension : availableExtensions) { + if (strcmp(extension.extensionName, VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME) == 0) { + timelineSemaphoresSupported = true; + break; + } + } +} + +// Create appropriate synchronization primitives +if (timelineSemaphoresSupported) { + // Create timeline semaphore + vk::SemaphoreTypeCreateInfo timelineCreateInfo{ + .semaphoreType = vk::SemaphoreType::eTimeline, + .initialValue = 0 + }; + + vk::SemaphoreCreateInfo semaphoreInfo{ + .pNext = &timelineCreateInfo + }; + + timelineSemaphore = device.createSemaphore(semaphoreInfo); +} else { + // Create binary semaphores and fences + vk::SemaphoreCreateInfo semaphoreInfo{}; + vk::FenceCreateInfo fenceInfo{.flags = vk::FenceCreateFlagBits::eSignaled}; + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + imageAvailableSemaphores[i] = device.createSemaphore(semaphoreInfo); + renderFinishedSemaphores[i] = device.createSemaphore(semaphoreInfo); + inFlightFences[i] = device.createFence(fenceInfo); + } +} +---- + +==== Synchronization2 + +The Synchronization2 feature (Vulkan 1.3) simplifies pipeline barriers and memory dependencies. If it's not available, use the original synchronization commands: + +[,c++] +---- +bool synchronization2Supported = false; + +// Check for Vulkan 1.3 support or extension +if (deviceProperties.apiVersion >= VK_VERSION_1_3) { + synchronization2Supported = true; +} else { + // Check for extension + for (const auto& extension : availableExtensions) { + if (strcmp(extension.extensionName, VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME) == 0) { + synchronization2Supported = true; + break; + } + } +} + +// Use appropriate barrier commands +if (synchronization2Supported) { + // Use Synchronization2 API + vk::ImageMemoryBarrier2 barrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe, + .srcAccessMask = vk::AccessFlagBits2::eNone, + .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eAttachmentOptimal, + .image = swapChainImages[i], + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} + }; + + vk::DependencyInfo dependencyInfo{ + .imageMemoryBarrierCount = 1, + .pImageMemoryBarriers = &barrier + }; + + commandBuffer.pipelineBarrier2(dependencyInfo); +} else { + // Use original synchronization API + vk::ImageMemoryBarrier barrier{ + .srcAccessMask = vk::AccessFlagBits::eNone, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = swapChainImages[i], + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} + }; + + commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::DependencyFlagBits::eByRegion, + {}, + {}, + { barrier } + ); +} +---- + +== Best Practices for Cross-GPU Compatibility + +Based on what we've learned, here are some best practices for developing Vulkan applications that work across a wide range of GPUs: + +1. *Check feature availability at runtime*: Don't assume features are available based on the Vulkan version alone. Always check for specific features and extensions. + +2. *Provide fallback paths*: Implement alternative code paths for when modern features aren't available. + +3. *Use feature structures*: When creating a logical device, use the appropriate feature structures to enable only the features you need and that are available. + +4. *Test on various hardware*: Use GPUInfo.org to identify common hardware configurations and test your application on a representative sample. + +5. *Graceful degradation*: Design your application to gracefully reduce visual quality or functionality when running on less capable hardware. + +6. *Document requirements*: Clearly document the minimum and recommended Vulkan version and extension requirements for your application. + +== Conclusion + +Understanding Vulkan ecosystem utilities and knowing how to adapt your code for different GPU capabilities are essential skills for Vulkan developers. By following the approaches outlined in this chapter, you can create applications that run on a wide range of hardware while still taking advantage of the latest features when available. + +link:/attachments/32_ecosystem_utilities.cpp[C{pp} code] diff --git a/en/13_Vulkan_Profiles.adoc b/en/13_Vulkan_Profiles.adoc new file mode 100644 index 00000000..36ebe149 --- /dev/null +++ b/en/13_Vulkan_Profiles.adoc @@ -0,0 +1,318 @@ +:pp: {plus}{plus} + += Vulkan Profiles: Simplifying Feature Detection + +== Introduction + +In this chapter, we'll explore Vulkan profiles, a powerful feature that builds upon the ecosystem utilities we discussed in the previous chapter. Vulkan profiles provide a standardized way to: + +1. Define a set of features, extensions, and limits that your application requires +2. Automatically check for compatibility with the user's hardware +3. Eliminate the need for manual feature detection and fallback paths +4. Significantly reduce boilerplate code + +Vulkan profiles are particularly valuable for developers who want to ensure their applications work consistently across a wide range of hardware without the complexity of manually checking for feature support. + +== Understanding Vulkan Profiles + +=== What Are Vulkan Profiles? + +Vulkan profiles are predefined collections of features, extensions, limits, and formats that represent a specific target environment or set of best practices. They provide a higher-level abstraction over the low-level Vulkan API, making it easier to: + +* Target specific hardware capabilities +* Ensure compatibility across different GPUs +* Implement best practices consistently +* Reduce boilerplate code for feature detection + +Instead of manually checking for each feature and extension and implementing fallback paths, you can simply specify a profile that your application requires. The Vulkan profiles library will handle the compatibility checks and provide appropriate error messages if the user's hardware doesn't meet the requirements. + +=== Types of Vulkan Profiles + +Several types of profiles are available: + +1. *API Profiles*: Represent specific Vulkan API versions (e.g., Vulkan 1.1, 1.2, 1.3) +2. *Vendor Profiles*: Target specific hardware vendors (e.g., NVIDIA, AMD, Intel) +3. *Platform Profiles*: Target specific platforms (e.g., Windows, Linux, Android) +4. *Best Practices Profile*: Implements recommended practices for Vulkan development + +In this chapter, we'll use the Best Practices profile as an example, +additionally, we will demonstrate how profiles can simplify your code by +eliminating the need for manual feature detection. + +== How Profiles Simplify Your Code + +=== Eliminating Manual Feature Detection + +Up until now, we had to manually check for feature support and implement +fallback paths: + +1. Check if the device supports Vulkan 1.3 +2. If not, check if it supports the dynamic rendering extension +3. If neither is supported, fall back to traditional render passes +4. Repeat this process for every feature (timeline semaphores, synchronization2, etc.) +5. Maintain separate code paths for each feature + +This approach leads to complex, hard-to-maintain code with multiple conditional branches. + +With profiles, this entire process is simplified to: + +1. Check if the profile is supported +2. If supported, use all features guaranteed by the profile +3. If not, optionally fall back to a more basic approach + +=== Benefits of Using Profiles + +Using profiles offers several advantages: + +1. *Drastically reduced code complexity*: No need for multiple feature checks and conditional branches +2. *Improved maintainability*: Fewer code paths to test and debug +3. *Future-proofing*: As new Vulkan versions are released, profiles can be updated without changing your code +4. *Clearer requirements*: Profiles provide a clear specification of what your application needs +5. *Simplified error handling*: One check instead of many + +== Implementing Profiles in Your Application + +Let's see how to implement profiles in your Vulkan application. We'll use the Best Practices profile as an example to demonstrate how profiles can replace the manual feature detection we had to do in the previous chapter. + +=== Adding the Vulkan Profiles Library + +First, you need to include the Vulkan profiles header: + +[,c++] +---- +#include +---- + +This header provides the necessary functions and structures to work with Vulkan profiles. + +=== Defining the Profile Requirements + +Instead of manually checking for features and extensions, you can define your profile requirements: + +[,c++] +---- +// Define the Best Practices profile +const VpProfileProperties bestPracticesProfile = { + VP_BEST_PRACTICES_PROFILE_NAME, + VP_BEST_PRACTICES_PROFILE_SPEC_VERSION +}; + +// Check if the profile is supported +VkBool32 supported = false; +vpGetPhysicalDeviceProfileSupport(instance, physicalDevice, &bestPracticesProfile, &supported); + +if (!supported) { + throw std::runtime_error("Best Practices profile is not supported on this device"); +} +---- + +=== Creating a Device with the Profile + +When creating a logical device, you can use the profile to automatically enable the required features and extensions: + +[,c++] +---- +// Create device with Best Practices profile +VkDeviceCreateInfo deviceCreateInfo = {}; +deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + +// Set up queue create infos +// ... + +// Apply the Best Practices profile to the device creation +vpCreateDevice(physicalDevice, &deviceCreateInfo, &bestPracticesProfile, nullptr, &device); +---- + +This automatically enables all the features and extensions required by the Best Practices profile, without having to manually specify them. + +=== Using Profile-Specific Features + +The Best Practices profile may enable specific features that you can use in your application: + +[,c++] +---- +// The profile guarantees these features are available +// No need to check for support or provide fallback paths + +// Example: Using dynamic rendering (guaranteed by the profile) +vk::RenderingAttachmentInfo colorAttachment{ + .imageView = swapChainImageViews[imageIndex], + .imageLayout = vk::ImageLayout::eAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearColor +}; + +vk::RenderingInfo renderingInfo{ + .renderArea = {{0, 0}, swapChainExtent}, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachment +}; + +commandBuffer.beginRendering(renderingInfo); +// ... draw commands ... +commandBuffer.endRendering(); +---- + +=== Error Handling with Profiles + +When using profiles, error handling becomes more straightforward: + +[,c++] +---- +try { + // Try to create a device with the Best Practices profile + vpCreateDevice(physicalDevice, &deviceCreateInfo, &bestPracticesProfile, nullptr, &device); +} catch (const std::exception& e) { + // Profile is not supported, provide user-friendly error message + std::cerr << "Your GPU does not support the required Vulkan features for optimal performance." << std::endl; + std::cerr << "Error: " << e.what() << std::endl; + + // Optionally, try with a more basic profile or exit gracefully + // ... +} +---- + +== Comparing Manual Feature Detection vs. Profiles + +Let's compare the two approaches to understand just how much code and complexity profiles can eliminate: + +=== Manual Feature Detection (Previous Chapter) + +In the previous chapter, we had to write code like this for *each feature* we wanted to use: + +[,c++] +---- +// Check if dynamic rendering is supported +bool dynamicRenderingSupported = false; + +// Check for Vulkan 1.3 support +if (deviceProperties.apiVersion >= VK_VERSION_1_3) { + dynamicRenderingSupported = true; +} else { + // Check for the extension on older Vulkan versions + for (const auto& extension : availableExtensions) { + if (strcmp(extension.extensionName, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME) == 0) { + dynamicRenderingSupported = true; + break; + } + } +} + +// Store this information for later use +appInfo.dynamicRenderingSupported = dynamicRenderingSupported; +---- + +And then we had to create conditional code paths throughout our application: + +[,c++] +---- +// When creating the pipeline +if (appInfo.dynamicRenderingSupported) { + // Use dynamic rendering + vk::PipelineRenderingCreateInfo renderingInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat + }; + pipelineInfo.pNext = &renderingInfo; + pipelineInfo.renderPass = nullptr; +} else { + // Use traditional render pass + pipelineInfo.pNext = nullptr; + pipelineInfo.renderPass = renderPass; + pipelineInfo.subpass = 0; +} + +// When recording command buffers +if (appInfo.dynamicRenderingSupported) { + // Begin dynamic rendering + vk::RenderingAttachmentInfo colorAttachment{/*...*/}; + vk::RenderingInfo renderingInfo{/*...*/}; + commandBuffer.beginRendering(renderingInfo); +} else { + // Begin traditional render pass + vk::RenderPassBeginInfo renderPassInfo{/*...*/}; + commandBuffer.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline); +} + +// And again at the end of the command buffer +if (appInfo.dynamicRenderingSupported) { + commandBuffer.endRendering(); +} else { + commandBuffer.endRenderPass(); +} +---- + +We had to repeat this pattern for *every feature* we wanted to use conditionally (timeline semaphores, synchronization2, etc.), resulting in complex, branching code that's challenging to maintain. + +=== Using Profiles (This Chapter) + +With profiles, all of that complexity is reduced to: + +[,c++] +---- +// Define the profile +const VpProfileProperties bestPracticesProfile = { + VP_BEST_PRACTICES_PROFILE_NAME, + VP_BEST_PRACTICES_PROFILE_SPEC_VERSION +}; + +// Check if the profile is supported +VkBool32 supported = false; +vpGetPhysicalDeviceProfileSupport(instance, physicalDevice, &bestPracticesProfile, &supported); + +if (supported) { + // Create device with the profile - all features enabled automatically + vpCreateDevice(physicalDevice, &deviceCreateInfo, &bestPracticesProfile, nullptr, &device); + + // Now we can use any feature guaranteed by the profile without checks + // For example, dynamic rendering is always available: + vk::RenderingAttachmentInfo colorAttachment{/*...*/}; + vk::RenderingInfo renderingInfo{/*...*/}; + commandBuffer.beginRendering(renderingInfo); + // ... draw commands ... + commandBuffer.endRendering(); +} +---- + +The profile approach eliminates: + +1. Multiple feature detection checks +2. Conditional code paths throughout your application +3. The need to track feature support in your application state +4. The complexity of maintaining and testing multiple code paths + +This results in code that is: + +1. Significantly shorter +2. Easier to read and understand +3. Less prone to errors +4. Easier to maintain and update + +== Best Practices for Using Profiles + +When using Vulkan profiles, consider these best practices: + +1. *Choose the right profile*: Select a profile that matches your application's requirements without being overly restrictive. + +2. *Provide fallback options*: If the Best Practices profile isn't supported, consider falling back to a more basic profile. + +3. *Communicate requirements clearly*: Inform users about the hardware requirements based on the profiles you support. + +4. *Test on various hardware*: Even with profiles, it's important to test your application on different GPUs. + +5. *Stay updated*: Profiles evolve with new Vulkan versions, so keep your implementation up to date. + +== Conclusion + +Vulkan profiles provide a powerful way to simplify your Vulkan code by eliminating the need for manual feature detection and conditional code paths. As we've seen in this chapter, profiles can dramatically reduce the amount of code you need to write and maintain, making your application: + +1. More concise and readable +2. Easier to maintain and update +3. Less prone to errors +4. More consistent across different hardware + +The example we've explored in this chapter demonstrates how profiles can replace the complex feature detection and fallback paths we had to implement in the previous chapter. By using profiles, you can focus more on your application's core functionality and less on the intricacies of hardware compatibility. + +link:/attachments/33_vulkan_profiles.cpp[C{pp} code] From 0e90d7b70491f550c9669d1270eee0bab32a5a28 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 2 Jul 2025 23:02:44 -0700 Subject: [PATCH 03/29] Add initial Vulkan Android support - Introduced Vulkan Android project setup with `AndroidManifest.xml` and platform-specific `34_android.cpp`. - Added cross-platform support for Vulkan initialization, rendering, and asset management between Android and desktop. - Included Android-specific utilities and logging for Vulkan deployment. --- attachments/34_android.cpp | 1728 +++++++++++++++++ attachments/CMakeLists.txt | 6 + attachments/android/app/build.gradle | 64 + .../android/app/src/main/AndroidManifest.xml | 35 + .../android/app/src/main/cpp/CMakeLists.txt | 128 ++ .../app/src/main/cpp/game_activity_bridge.cpp | 32 + .../vulkantutorial/VulkanActivity.java | 20 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 6 + attachments/android/build.gradle | 24 + attachments/android/settings.gradle | 2 + en/14_Android.adoc | 835 ++++++++ 12 files changed, 2883 insertions(+) create mode 100644 attachments/34_android.cpp create mode 100644 attachments/android/app/build.gradle create mode 100644 attachments/android/app/src/main/AndroidManifest.xml create mode 100644 attachments/android/app/src/main/cpp/CMakeLists.txt create mode 100644 attachments/android/app/src/main/cpp/game_activity_bridge.cpp create mode 100644 attachments/android/app/src/main/java/com/example/vulkantutorial/VulkanActivity.java create mode 100644 attachments/android/app/src/main/res/values/strings.xml create mode 100644 attachments/android/app/src/main/res/values/styles.xml create mode 100644 attachments/android/build.gradle create mode 100644 attachments/android/settings.gradle create mode 100644 en/14_Android.adoc diff --git a/attachments/34_android.cpp b/attachments/34_android.cpp new file mode 100644 index 00000000..067f64e2 --- /dev/null +++ b/attachments/34_android.cpp @@ -0,0 +1,1728 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +import vulkan_hpp; +#include +#include + +// Platform detection +#if defined(__ANDROID__) + #define PLATFORM_ANDROID 1 +#else + #define PLATFORM_DESKTOP 1 +#endif + +#define STB_IMAGE_IMPLEMENTATION +#include + +#define TINYOBJLOADER_IMPLEMENTATION +#include + +// Platform-specific includes +#if PLATFORM_ANDROID + // Android-specific includes + #include + #include + #include + #include + + // Define logging macros for Android + #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__)) + #define LOG_INFO(msg) LOGI("%s", msg) + #define LOG_ERROR(msg) LOGE("%s", msg) +#else + // Desktop-specific includes + #define GLFW_INCLUDE_VULKAN + #include + + // Define logging macros for Desktop + #define LOG_INFO(msg) std::cout << msg << std::endl + #define LOG_ERROR(msg) std::cerr << msg << std::endl +#endif + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#define GLM_ENABLE_EXPERIMENTAL +#include +#include +#include + +constexpr uint32_t WIDTH = 800; +constexpr uint32_t HEIGHT = 600; +constexpr uint64_t FenceTimeout = 100000000; +const std::string MODEL_PATH = "models/viking_room.obj"; +const std::string TEXTURE_PATH = "textures/viking_room.png"; +constexpr int MAX_FRAMES_IN_FLIGHT = 2; + +// Application info structure to store profile support flags +struct AppInfo { + bool profileSupported = false; + VpProfileProperties profile; +}; + +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; +}; + +// Cross-platform file reading function +std::vector readFile(const std::string& filename, std::optional assetManager = std::nullopt) { +#if PLATFORM_ANDROID + // On Android, use asset manager if provided + if (assetManager.has_value() && *assetManager != nullptr) { + // Open the asset + AAsset* asset = AAssetManager_open(*assetManager, filename.c_str(), AASSET_MODE_BUFFER); + if (!asset) { + LOGE("Failed to open asset: %s", filename.c_str()); + throw std::runtime_error("Failed to open file: " + filename); + } + + // Get the file size + off_t fileSize = AAsset_getLength(asset); + std::vector buffer(fileSize); + + // Read the file data + AAsset_read(asset, buffer.data(), fileSize); + + // Close the asset + AAsset_close(asset); + + return buffer; + } +#endif + + // Desktop version or Android fallback to filesystem + 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(); + + return buffer; +} + +// Cross-platform application class +class HelloTriangleApplication { +public: +#if PLATFORM_DESKTOP + // Desktop constructor + HelloTriangleApplication() { + // No Android-specific initialization needed + } +#else + // Android constructor + HelloTriangleApplication(android_app* app) : androidApp(app) { + androidApp->userData = this; + androidApp->onAppCmd = handleAppCommand; + androidApp->onInputEvent = handleInputEvent; + + // Get the asset manager + assetManager = androidApp->activity->assetManager; + } +#endif + + void run() { +#if PLATFORM_DESKTOP + // Desktop main loop + initWindow(); + initVulkan(); + mainLoop(); + cleanup(); +#else + // Android main loop is handled by Android + while (!initialized) { + // Wait for app to initialize + int events; + android_poll_source* source; + if (ALooper_pollAll(0, nullptr, &events, (void**)&source) >= 0) { + if (source != nullptr) { + source->process(androidApp, source); + } + } + } +#endif + } + +#if PLATFORM_DESKTOP + // Initialize window (Desktop only) + void initWindow() { + glfwInit(); + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan Cross-Platform", nullptr, nullptr); + glfwSetWindowUserPointer(window, this); + glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); + + LOG_INFO("Desktop window created"); + } + + // Desktop main loop + void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } + + device.waitIdle(); + } + + // Desktop framebuffer resize callback + static void framebufferResizeCallback(GLFWwindow* window, int, int) { + auto app = reinterpret_cast(glfwGetWindowUserPointer(window)); + app->framebufferResized = true; + } +#endif + + void cleanup() { + if (initialized) { + // Wait for device to finish operations + if (device) { + device.waitIdle(); + } + + // Cleanup resources + cleanupSwapChain(); + + // Cleanup other resources + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + uniformBuffers[i] = nullptr; + uniformBuffersMemory[i] = nullptr; + } + + descriptorPool = nullptr; + descriptorSetLayout = nullptr; + + textureImageView = nullptr; + textureImage = nullptr; + textureImageMemory = nullptr; + textureSampler = nullptr; + + indexBuffer = nullptr; + indexBufferMemory = nullptr; + vertexBuffer = nullptr; + vertexBufferMemory = nullptr; + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + imageAvailableSemaphores[i] = nullptr; + renderFinishedSemaphores[i] = nullptr; + inFlightFences[i] = nullptr; + } + + commandPool = nullptr; + graphicsPipeline = nullptr; + pipelineLayout = nullptr; + renderPass = nullptr; + + device = nullptr; + surface = nullptr; + instance = nullptr; + + initialized = false; + } + } + +private: +#if PLATFORM_ANDROID + // Android-specific members + android_app* androidApp = nullptr; + AAssetManager* assetManager = nullptr; +#else + // Desktop-specific members + GLFWwindow* window = nullptr; +#endif + bool initialized = false; + bool framebufferResized = false; + + // Vulkan objects + vk::raii::Context context; + vk::raii::Instance instance = nullptr; + vk::raii::SurfaceKHR surface = nullptr; + vk::raii::PhysicalDevice physicalDevice = nullptr; + vk::raii::Device device = nullptr; + vk::raii::Queue graphicsQueue = nullptr; + vk::raii::Queue presentQueue = nullptr; + vk::raii::SwapchainKHR swapChain = nullptr; + std::vector swapChainImages; + vk::Format swapChainImageFormat = {}; + vk::Extent2D swapChainExtent; + std::vector swapChainImageViews; + vk::raii::RenderPass renderPass = nullptr; + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline graphicsPipeline = nullptr; + std::vector swapChainFramebuffers; + vk::raii::CommandPool commandPool = nullptr; + std::vector commandBuffers; + vk::raii::Buffer vertexBuffer = nullptr; + vk::raii::DeviceMemory vertexBufferMemory = nullptr; + vk::raii::Buffer indexBuffer = nullptr; + vk::raii::DeviceMemory indexBufferMemory = nullptr; + vk::raii::Image textureImage = nullptr; + vk::raii::DeviceMemory textureImageMemory = nullptr; + vk::raii::ImageView textureImageView = nullptr; + vk::raii::Sampler textureSampler = nullptr; + std::vector uniformBuffers; + std::vector uniformBuffersMemory; + vk::raii::DescriptorPool descriptorPool = nullptr; + std::vector descriptorSets; + std::vector imageAvailableSemaphores; + std::vector renderFinishedSemaphores; + std::vector inFlightFences; + uint32_t currentFrame = 0; + + // Application info + AppInfo appInfo; + + // Model data + std::vector vertices; + std::vector indices; + + // Queue family indices + struct QueueFamilyIndices { + std::optional graphicsFamily; + std::optional presentFamily; + + bool isComplete() const { + return graphicsFamily.has_value() && presentFamily.has_value(); + } + }; + + // Swap chain support details + struct SwapChainSupportDetails { + vk::SurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector presentModes; + }; + + // Required device extensions + const std::vector deviceExtensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME + }; + + // Initialize Vulkan + void initVulkan() { + createInstance(); + createSurface(); + pickPhysicalDevice(); + checkFeatureSupport(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createDescriptorSetLayout(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createTextureImage(); + createTextureImageView(); + createTextureSampler(); + loadModel(); + createVertexBuffer(); + createIndexBuffer(); + createUniformBuffers(); + createDescriptorPool(); + createDescriptorSets(); + createCommandBuffers(); + createSyncObjects(); + + initialized = true; + } + + // Create Vulkan instance + void createInstance() { + // Application info + vk::ApplicationInfo appInfo{ + .pApplicationName = "Vulkan Android", + .applicationVersion = VK_MAKE_VERSION(1, 0, 0), + .pEngineName = "No Engine", + .engineVersion = VK_MAKE_VERSION(1, 0, 0), + .apiVersion = VK_API_VERSION_1_3 + }; + + // Get required extensions + std::vector extensions = getRequiredExtensions(); + + // Create instance + vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &appInfo, + .enabledExtensionCount = static_cast(extensions.size()), + .ppEnabledExtensionNames = extensions.data() + }; + + instance = vk::raii::Instance(context, createInfo); + LOGI("Vulkan instance created"); + } + + // Create platform-specific surface + void createSurface() { + VkSurfaceKHR _surface; + +#if PLATFORM_ANDROID + // Create Android surface + VkResult result = vkCreateAndroidSurfaceKHR( + *instance, + &(VkAndroidSurfaceCreateInfoKHR{ + .sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR, + .pNext = nullptr, + .flags = 0, + .window = androidApp->window + }), + nullptr, + &_surface + ); + + if (result != VK_SUCCESS) { + throw std::runtime_error("Failed to create Android surface"); + } + + LOG_INFO("Android surface created"); +#else + // Create desktop surface using GLFW + if (glfwCreateWindowSurface(*instance, window, nullptr, &_surface) != 0) { + throw std::runtime_error("Failed to create window surface"); + } + + LOG_INFO("Desktop surface created"); +#endif + + surface = vk::raii::SurfaceKHR(instance, _surface); + } + + // Pick physical device + void pickPhysicalDevice() { + std::vector devices = instance.enumeratePhysicalDevices(); + const auto devIter = std::ranges::find_if( + devices, + [&](auto const& device) { + // 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(deviceExtensions, + [&availableDeviceExtensions](auto const& requiredDeviceExtension) { + return std::ranges::any_of(availableDeviceExtensions, + [requiredDeviceExtension](auto const& availableDeviceExtension) { + return strcmp(availableDeviceExtension.extensionName, requiredDeviceExtension) == 0; + }); + }); + + return supportsGraphics && supportsAllRequiredExtensions; + }); + + if (devIter != devices.end()) { + physicalDevice = *devIter; + + // Print device information + vk::PhysicalDeviceProperties deviceProperties = physicalDevice.getProperties(); + LOGI("Selected GPU: %s", deviceProperties.deviceName); + } else { + throw std::runtime_error("Failed to find a suitable GPU"); + } + } + + // Check feature support + void checkFeatureSupport() { + // Define the KHR roadmap 2022 profile + appInfo.profile = { + VP_KHR_ROADMAP_2022_NAME, + VP_KHR_ROADMAP_2022_SPEC_VERSION + }; + + // Check if the profile is supported + VkBool32 supported = VK_FALSE; + VkResult result = vpGetPhysicalDeviceProfileSupport( + *instance, + *physicalDevice, + &appInfo.profile, + &supported + ); + + if (result == VK_SUCCESS && supported == VK_TRUE) { + appInfo.profileSupported = true; + LOGI("Using KHR roadmap 2022 profile"); + } else { + appInfo.profileSupported = false; + LOGI("Falling back to traditional rendering (profile not supported)"); + } + } + + // Create logical device + void createLogicalDevice() { + QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + + std::vector queueCreateInfos; + std::set uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + + float queuePriority = 1.0f; + for (uint32_t queueFamily : uniqueQueueFamilies) { + vk::DeviceQueueCreateInfo queueCreateInfo{ + .queueFamilyIndex = queueFamily, + .queueCount = 1, + .pQueuePriorities = &queuePriority + }; + queueCreateInfos.push_back(queueCreateInfo); + } + + if (appInfo.profileSupported) { + // Enable required features + vk::PhysicalDeviceFeatures2 features2; + vk::PhysicalDeviceFeatures deviceFeatures{}; + deviceFeatures.samplerAnisotropy = VK_TRUE; + deviceFeatures.sampleRateShading = VK_TRUE; + features2.features = deviceFeatures; + + // Enable dynamic rendering + vk::PhysicalDeviceDynamicRenderingFeatures dynamicRenderingFeatures; + dynamicRenderingFeatures.dynamicRendering = VK_TRUE; + features2.pNext = &dynamicRenderingFeatures; + + // Create a vk::DeviceCreateInfo with the required features + vk::DeviceCreateInfo vkDeviceCreateInfo{ + .pNext = &features2, + .queueCreateInfoCount = static_cast(queueCreateInfos.size()), + .pQueueCreateInfos = queueCreateInfos.data(), + .enabledExtensionCount = static_cast(deviceExtensions.size()), + .ppEnabledExtensionNames = deviceExtensions.data() + }; + + // Create the device with the vk::DeviceCreateInfo + device = vk::raii::Device(physicalDevice, vkDeviceCreateInfo); + } else { + // Fallback to manual device creation + vk::PhysicalDeviceFeatures deviceFeatures{}; + deviceFeatures.samplerAnisotropy = VK_TRUE; + deviceFeatures.sampleRateShading = VK_TRUE; + + vk::DeviceCreateInfo createInfo{ + .queueCreateInfoCount = static_cast(queueCreateInfos.size()), + .pQueueCreateInfos = queueCreateInfos.data(), + .enabledExtensionCount = static_cast(deviceExtensions.size()), + .ppEnabledExtensionNames = deviceExtensions.data(), + .pEnabledFeatures = &deviceFeatures + }; + + device = vk::raii::Device(physicalDevice, createInfo); + } + + graphicsQueue = device.getQueue(indices.graphicsFamily.value(), 0); + presentQueue = device.getQueue(indices.presentFamily.value(), 0); + } + + // Create swap chain + void createSwapChain() { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice); + + vk::SurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats); + vk::PresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes); + vk::Extent2D extent = chooseSwapExtent(swapChainSupport.capabilities); + + uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1; + if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) { + imageCount = swapChainSupport.capabilities.maxImageCount; + } + + vk::SwapchainCreateInfoKHR createInfo{ + .surface = *surface, + .minImageCount = imageCount, + .imageFormat = surfaceFormat.format, + .imageColorSpace = surfaceFormat.colorSpace, + .imageExtent = extent, + .imageArrayLayers = 1, + .imageUsage = vk::ImageUsageFlagBits::eColorAttachment + }; + + QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + + if (indices.graphicsFamily != indices.presentFamily) { + createInfo.imageSharingMode = vk::SharingMode::eConcurrent; + createInfo.queueFamilyIndexCount = 2; + createInfo.pQueueFamilyIndices = queueFamilyIndices; + } else { + createInfo.imageSharingMode = vk::SharingMode::eExclusive; + } + + createInfo.preTransform = swapChainSupport.capabilities.currentTransform; + createInfo.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque; + createInfo.presentMode = presentMode; + createInfo.clipped = VK_TRUE; + + swapChain = device.createSwapchainKHR(createInfo); + swapChainImages = swapChain.getImages(); + swapChainImageFormat = surfaceFormat.format; + swapChainExtent = extent; + } + + // Create image views + void createImageViews() { + swapChainImageViews.reserve(swapChainImages.size()); + + for (const auto& image : swapChainImages) { + vk::ImageViewCreateInfo createInfo{ + .image = image, + .viewType = vk::ImageViewType::e2D, + .format = swapChainImageFormat, + .components = { + .r = vk::ComponentSwizzle::eIdentity, + .g = vk::ComponentSwizzle::eIdentity, + .b = vk::ComponentSwizzle::eIdentity, + .a = vk::ComponentSwizzle::eIdentity + }, + .subresourceRange = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + swapChainImageViews.push_back(device.createImageView(createInfo)); + } + } + + // Create render pass + void createRenderPass() { + vk::AttachmentDescription colorAttachment{ + .format = swapChainImageFormat, + .samples = vk::SampleCountFlagBits::e1, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, + .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, + .initialLayout = vk::ImageLayout::eUndefined, + .finalLayout = vk::ImageLayout::ePresentSrcKHR + }; + + vk::AttachmentReference colorAttachmentRef{ + .attachment = 0, + .layout = vk::ImageLayout::eColorAttachmentOptimal + }; + + vk::SubpassDescription subpass{ + .pipelineBindPoint = vk::PipelineBindPoint::eGraphics, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentRef + }; + + vk::SubpassDependency dependency{ + .srcSubpass = VK_SUBPASS_EXTERNAL, + .dstSubpass = 0, + .srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput, + .dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput, + .srcAccessMask = vk::AccessFlagBits::eNone, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite + }; + + vk::RenderPassCreateInfo renderPassInfo{ + .attachmentCount = 1, + .pAttachments = &colorAttachment, + .subpassCount = 1, + .pSubpasses = &subpass, + .dependencyCount = 1, + .pDependencies = &dependency + }; + + renderPass = device.createRenderPass(renderPassInfo); + } + + // Create descriptor set layout + void createDescriptorSetLayout() { + vk::DescriptorSetLayoutBinding uboLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex + }; + + vk::DescriptorSetLayoutBinding samplerLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment + }; + + std::array bindings = {uboLayoutBinding, samplerLayoutBinding}; + + vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data() + }; + + descriptorSetLayout = device.createDescriptorSetLayout(layoutInfo); + } + + // Create graphics pipeline + void createGraphicsPipeline() { + // Load shader code from asset files + LOGI("Loading shaders from assets"); + + // Load shader files using cross-platform function +#if PLATFORM_ANDROID + std::optional optionalAssetManager = assetManager; +#else + std::optional optionalAssetManager = std::nullopt; +#endif + std::vector vertShaderCode = readFile("shaders/vert.spv", optionalAssetManager); + std::vector fragShaderCode = readFile("shaders/frag.spv", optionalAssetManager); + + LOGI("Shaders loaded successfully"); + + // Create shader modules + vk::ShaderModuleCreateInfo vertShaderModuleInfo{ + .codeSize = vertShaderCode.size(), + .pCode = reinterpret_cast(vertShaderCode.data()) + }; + vk::raii::ShaderModule vertShaderModule = device.createShaderModule(vertShaderModuleInfo); + + vk::ShaderModuleCreateInfo fragShaderModuleInfo{ + .codeSize = fragShaderCode.size(), + .pCode = reinterpret_cast(fragShaderCode.data()) + }; + vk::raii::ShaderModule fragShaderModule = device.createShaderModule(fragShaderModuleInfo); + + // Create shader stages + vk::PipelineShaderStageCreateInfo shaderStages[] = { + { + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *vertShaderModule, + .pName = "main" + }, + { + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *fragShaderModule, + .pName = "main" + } + }; + + // Vertex input + auto bindingDescription = Vertex::getBindingDescription(); + auto attributeDescriptions = Vertex::getAttributeDescriptions(); + + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = 1, + .pVertexBindingDescriptions = &bindingDescription, + .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), + .pVertexAttributeDescriptions = attributeDescriptions.data() + }; + + // Input assembly + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE + }; + + // Viewport and scissor + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .scissorCount = 1 + }; + + // Rasterization + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .lineWidth = 1.0f + }; + + // Multisampling + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE + }; + + // Color blending + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment + }; + + // Dynamic states + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState{ + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data() + }; + + // Pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*descriptorSetLayout + }; + + pipelineLayout = device.createPipelineLayout(pipelineLayoutInfo); + + // Create the graphics pipeline + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = nullptr, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *pipelineLayout, + .renderPass = *renderPass, + .subpass = 0 + }; + + // Create the pipeline + graphicsPipeline = device.createGraphicsPipeline(nullptr, pipelineInfo); + } + + // Create framebuffers + void createFramebuffers() { + swapChainFramebuffers.reserve(swapChainImageViews.size()); + + for (size_t i = 0; i < swapChainImageViews.size(); i++) { + vk::ImageView attachments[] = { + *swapChainImageViews[i] + }; + + vk::FramebufferCreateInfo framebufferInfo{ + .renderPass = *renderPass, + .attachmentCount = 1, + .pAttachments = attachments, + .width = swapChainExtent.width, + .height = swapChainExtent.height, + .layers = 1 + }; + + swapChainFramebuffers.push_back(device.createFramebuffer(framebufferInfo)); + } + } + + // Create command pool + void createCommandPool() { + QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice); + + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.graphicsFamily.value() + }; + + commandPool = device.createCommandPool(poolInfo); + } + + // Create texture image + void createTextureImage() { + // Load texture image + int texWidth, texHeight, texChannels; + stbi_uc* pixels = nullptr; + +#if PLATFORM_ANDROID + // Load image from Android assets + std::optional optionalAssetManager = assetManager; + std::vector imageData = readFile(TEXTURE_PATH, optionalAssetManager); + pixels = stbi_load_from_memory( + reinterpret_cast(imageData.data()), + static_cast(imageData.size()), + &texWidth, &texHeight, &texChannels, STBI_rgb_alpha + ); +#else + // Load image from filesystem + pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); +#endif + + if (!pixels) { + throw std::runtime_error("Failed to load texture image: " + TEXTURE_PATH); + } + + LOG_INFO("Texture loaded successfully"); + + vk::DeviceSize imageSize = texWidth * texHeight * 4; + + // Create staging buffer + vk::raii::Buffer stagingBuffer = nullptr; + vk::raii::DeviceMemory stagingBufferMemory = nullptr; + + createBuffer(imageSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); + + // Copy pixel data to staging buffer + void* data; + data = stagingBufferMemory.mapMemory(0, imageSize); + memcpy(data, pixels, static_cast(imageSize)); + stagingBufferMemory.unmapMemory(); + + // Free the pixel data + if (pixels != nullptr) { + stbi_image_free(pixels); + } + + // Create image + vk::ImageCreateInfo imageInfo{ + .imageType = vk::ImageType::e2D, + .format = vk::Format::eR8G8B8A8Srgb, + .extent = { + .width = static_cast(texWidth), + .height = static_cast(texHeight), + .depth = 1 + }, + .mipLevels = 1, + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + .tiling = vk::ImageTiling::eOptimal, + .usage = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined + }; + + textureImage = device.createImage(imageInfo); + + // Allocate memory for the image + vk::MemoryRequirements memRequirements = textureImage.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, vk::MemoryPropertyFlagBits::eDeviceLocal) + }; + + textureImageMemory = device.allocateMemory(allocInfo); + textureImage.bindMemory(*textureImageMemory, 0); + + // Transition image layout and copy buffer to image + transitionImageLayout(textureImage, vk::Format::eR8G8B8A8Srgb, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); + copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); + transitionImageLayout(textureImage, vk::Format::eR8G8B8A8Srgb, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal); + } + + // Create texture image view + void createTextureImageView() { + textureImageView = createImageView(textureImage, vk::Format::eR8G8B8A8Srgb); + } + + // Create texture sampler + void createTextureSampler() { + 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, + .anisotropyEnable = VK_TRUE, + .maxAnisotropy = 16.0f, + .compareEnable = VK_FALSE, + .compareOp = vk::CompareOp::eAlways, + .borderColor = vk::BorderColor::eIntOpaqueBlack, + .unnormalizedCoordinates = VK_FALSE + }; + + textureSampler = device.createSampler(samplerInfo); + } + + // Load model + void loadModel() { + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + +#if PLATFORM_ANDROID + // Load OBJ file from Android assets + std::optional optionalAssetManager = assetManager; + std::vector objData = readFile(MODEL_PATH, optionalAssetManager); + std::string objString(objData.begin(), objData.end()); + std::istringstream objStream(objString); + + if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, &objStream)) { + throw std::runtime_error("Failed to load model: " + MODEL_PATH + " - " + warn + err); + } +#else + // Load OBJ file from filesystem + if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, MODEL_PATH.c_str())) { + throw std::runtime_error("Failed to load model: " + MODEL_PATH + " - " + warn + err); + } +#endif + + std::unordered_map uniqueVertices{}; + + for (const auto& shape : shapes) { + for (const auto& index : shape.mesh.indices) { + Vertex vertex{}; + + vertex.pos = { + attrib.vertices[3 * index.vertex_index + 0], + attrib.vertices[3 * index.vertex_index + 1], + attrib.vertices[3 * index.vertex_index + 2] + }; + + vertex.texCoord = { + attrib.texcoords[2 * index.texcoord_index + 0], + 1.0f - attrib.texcoords[2 * index.texcoord_index + 1] + }; + + vertex.color = {1.0f, 1.0f, 1.0f}; + + if (uniqueVertices.count(vertex) == 0) { + uniqueVertices[vertex] = static_cast(vertices.size()); + vertices.push_back(vertex); + } + + indices.push_back(uniqueVertices[vertex]); + } + } + + LOG_INFO("Model loaded successfully"); + } + + // Create vertex buffer + void createVertexBuffer() { + vk::DeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); + + vk::raii::Buffer stagingBuffer = nullptr; + vk::raii::DeviceMemory stagingBufferMemory = nullptr; + createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); + + void* data; + data = stagingBufferMemory.mapMemory(0, bufferSize); + memcpy(data, vertices.data(), (size_t) bufferSize); + stagingBufferMemory.unmapMemory(); + + createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal, vertexBuffer, vertexBufferMemory); + + copyBuffer(stagingBuffer, vertexBuffer, bufferSize); + } + + // Create index buffer + void createIndexBuffer() { + vk::DeviceSize bufferSize = sizeof(indices[0]) * indices.size(); + + vk::raii::Buffer stagingBuffer = nullptr; + vk::raii::DeviceMemory stagingBufferMemory = nullptr; + createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); + + void* data; + data = stagingBufferMemory.mapMemory(0, bufferSize); + memcpy(data, indices.data(), (size_t) bufferSize); + stagingBufferMemory.unmapMemory(); + + createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal, indexBuffer, indexBufferMemory); + + copyBuffer(stagingBuffer, indexBuffer, bufferSize); + } + + // Create uniform buffers + void createUniformBuffers() { + vk::DeviceSize bufferSize = sizeof(UniformBufferObject); + + uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT); + uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + createBuffer(bufferSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, uniformBuffers[i], uniformBuffersMemory[i]); + } + } + + // Create descriptor pool + void createDescriptorPool() { + std::array poolSizes = { + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eUniformBuffer, + .descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT) + }, + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT) + } + }; + + vk::DescriptorPoolCreateInfo poolInfo{ + .maxSets = static_cast(MAX_FRAMES_IN_FLIGHT), + .poolSizeCount = static_cast(poolSizes.size()), + .pPoolSizes = poolSizes.data() + }; + + descriptorPool = device.createDescriptorPool(poolInfo); + } + + // Create descriptor sets + void createDescriptorSets() { + std::vector layouts(MAX_FRAMES_IN_FLIGHT, *descriptorSetLayout); + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT), + .pSetLayouts = layouts.data() + }; + + 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, nullptr); + } + } + + // Create command buffers + void createCommandBuffers() { + commandBuffers.reserve(MAX_FRAMES_IN_FLIGHT); + + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = static_cast(MAX_FRAMES_IN_FLIGHT) + }; + + commandBuffers = device.allocateCommandBuffers(allocInfo); + } + + // Create synchronization objects + void createSyncObjects() { + imageAvailableSemaphores.reserve(MAX_FRAMES_IN_FLIGHT); + renderFinishedSemaphores.reserve(MAX_FRAMES_IN_FLIGHT); + inFlightFences.reserve(MAX_FRAMES_IN_FLIGHT); + + vk::SemaphoreCreateInfo semaphoreInfo{}; + vk::FenceCreateInfo fenceInfo{ + .flags = vk::FenceCreateFlagBits::eSignaled + }; + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + imageAvailableSemaphores.push_back(device.createSemaphore(semaphoreInfo)); + renderFinishedSemaphores.push_back(device.createSemaphore(semaphoreInfo)); + inFlightFences.push_back(device.createFence(fenceInfo)); + } + } + + // Clean up swap chain + void cleanupSwapChain() { + for (auto& framebuffer : swapChainFramebuffers) { + framebuffer = nullptr; + } + + for (auto& imageView : swapChainImageViews) { + imageView = nullptr; + } + + swapChain = nullptr; + } + + // Record command buffer + void recordCommandBuffer(vk::raii::CommandBuffer& commandBuffer, uint32_t imageIndex) { + vk::CommandBufferBeginInfo beginInfo{}; + commandBuffer.begin(beginInfo); + + vk::RenderPassBeginInfo renderPassInfo{ + .renderPass = *renderPass, + .framebuffer = *swapChainFramebuffers[imageIndex], + .renderArea = { + .offset = {0, 0}, + .extent = swapChainExtent + } + }; + + vk::ClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; + renderPassInfo.clearValueCount = 1; + renderPassInfo.pClearValues = &clearColor; + + commandBuffer.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline); + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *graphicsPipeline); + + vk::Viewport viewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(swapChainExtent.width), + .height = static_cast(swapChainExtent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f + }; + commandBuffer.setViewport(0, viewport); + + vk::Rect2D scissor{ + .offset = {0, 0}, + .extent = swapChainExtent + }; + commandBuffer.setScissor(0, scissor); + + commandBuffer.bindVertexBuffers(0, {*vertexBuffer}, {0}); + commandBuffer.bindIndexBuffer(*indexBuffer, 0, vk::IndexType::eUint32); + commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, {*descriptorSets[currentFrame]}, nullptr); + commandBuffer.drawIndexed(static_cast(indices.size()), 1, 0, 0, 0); + + commandBuffer.endRenderPass(); + commandBuffer.end(); + } + + // Draw frame + void drawFrame() { + static_cast(device.waitForFences({*inFlightFences[currentFrame]}, VK_TRUE, FenceTimeout)); + + uint32_t imageIndex; + try { + auto [result, idx] = swapChain.acquireNextImage(FenceTimeout, *imageAvailableSemaphores[currentFrame]); + imageIndex = idx; + } catch (vk::OutOfDateKHRError&) { + recreateSwapChain(); + return; + } + + device.resetFences({*inFlightFences[currentFrame]}); + + commandBuffers[currentFrame].reset(); + recordCommandBuffer(commandBuffers[currentFrame], imageIndex); + + vk::PipelineStageFlags waitDestinationStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput); + const vk::SubmitInfo submitInfo{ + .waitSemaphoreCount = 1, + .pWaitSemaphores = &*imageAvailableSemaphores[currentFrame], + .pWaitDstStageMask = &waitDestinationStageMask, + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffers[currentFrame], + .signalSemaphoreCount = 1, + .pSignalSemaphores = &*renderFinishedSemaphores[currentFrame] + }; + graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); + + const vk::PresentInfoKHR presentInfoKHR{ + .waitSemaphoreCount = 1, + .pWaitSemaphores = &*renderFinishedSemaphores[currentFrame], + .swapchainCount = 1, + .pSwapchains = &*swapChain, + .pImageIndices = &imageIndex + }; + + vk::Result result; + try { + result = presentQueue.presentKHR(presentInfoKHR); + } catch (vk::OutOfDateKHRError&) { + result = vk::Result::eErrorOutOfDateKHR; + } + + 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; + } + + // Recreate swap chain + void recreateSwapChain() { + // Wait for device to finish operations + device.waitIdle(); + + // Clean up old swap chain + cleanupSwapChain(); + + // Create new swap chain + createSwapChain(); + createImageViews(); + createFramebuffers(); + } + + // Get required extensions + std::vector getRequiredExtensions() { +#if PLATFORM_ANDROID + // Android requires these extensions + std::vector extensions = { + VK_KHR_SURFACE_EXTENSION_NAME, + VK_KHR_ANDROID_SURFACE_EXTENSION_NAME + }; +#else + // Get the required extensions from GLFW + uint32_t glfwExtensionCount = 0; + auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); +#endif + + // Check if the debug utils extension is available + std::vector props = context.enumerateInstanceExtensionProperties(); + bool debugUtilsAvailable = std::ranges::any_of(props, + [](vk::ExtensionProperties const & ep) { + return strcmp(ep.extensionName, vk::EXTDebugUtilsExtensionName) == 0; + }); + + // Always include the debug utils extension if available + if (debugUtilsAvailable) { + extensions.push_back(vk::EXTDebugUtilsExtensionName); +#if PLATFORM_DESKTOP + } else { + LOG_INFO("VK_EXT_debug_utils extension not available. Validation layers may not work."); +#endif + } + + return extensions; + } + + // Choose swap surface format + vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { + // Prefer SRGB format + for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && + availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) { + return availableFormat; + } + } + + // If not available, just use the first format + return availableFormats[0]; + } + + // Choose swap present mode + vk::PresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { + // Prefer mailbox mode for triple buffering + for (const auto& availablePresentMode : availablePresentModes) { + if (availablePresentMode == vk::PresentModeKHR::eMailbox) { + return availablePresentMode; + } + } + + // Fallback to FIFO (guaranteed to be available) + return vk::PresentModeKHR::eFifo; + } + + // Choose swap extent + vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) { + if (capabilities.currentExtent.width != std::numeric_limits::max()) { + return capabilities.currentExtent; + } else { +#if PLATFORM_ANDROID + // Get the window size from Android + int32_t width = ANativeWindow_getWidth(androidApp->window); + int32_t height = ANativeWindow_getHeight(androidApp->window); +#else + // Get the window size from GLFW + int width, height; + glfwGetFramebufferSize(window, &width, &height); +#endif + + vk::Extent2D actualExtent = { + static_cast(width), + static_cast(height) + }; + + actualExtent.width = std::clamp(actualExtent.width, + capabilities.minImageExtent.width, + capabilities.maxImageExtent.width); + actualExtent.height = std::clamp(actualExtent.height, + capabilities.minImageExtent.height, + capabilities.maxImageExtent.height); + + return actualExtent; + } + } + + // Query swap chain support + SwapChainSupportDetails querySwapChainSupport(vk::raii::PhysicalDevice device) { + SwapChainSupportDetails details; + details.capabilities = device.getSurfaceCapabilitiesKHR(*surface); + details.formats = device.getSurfaceFormatsKHR(*surface); + details.presentModes = device.getSurfacePresentModesKHR(*surface); + return details; + } + + // Find queue families + QueueFamilyIndices findQueueFamilies(vk::raii::PhysicalDevice device) { + QueueFamilyIndices indices; + + std::vector queueFamilies = device.getQueueFamilyProperties(); + + uint32_t i = 0; + for (const auto& queueFamily : queueFamilies) { + if (queueFamily.queueFlags & vk::QueueFlagBits::eGraphics) { + indices.graphicsFamily = i; + } + + vk::Bool32 presentSupport = device.getSurfaceSupportKHR(i, *surface); + if (presentSupport) { + indices.presentFamily = i; + } + + if (indices.isComplete()) { + break; + } + + i++; + } + + return indices; + } + + // Create buffer + 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 = device.createBuffer(bufferInfo); + + vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) + }; + + bufferMemory = device.allocateMemory(allocInfo); + buffer.bindMemory(*bufferMemory, 0); + } + + // Copy buffer + 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 commandBuffer = std::move(device.allocateCommandBuffers(allocInfo)[0]); + + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + commandBuffer.begin(beginInfo); + + vk::BufferCopy copyRegion{ + .srcOffset = 0, + .dstOffset = 0, + .size = size + }; + commandBuffer.copyBuffer(*srcBuffer, *dstBuffer, copyRegion); + + commandBuffer.end(); + + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + + graphicsQueue.submit(submitInfo, nullptr); + graphicsQueue.waitIdle(); + } + + // Find memory type + 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"); + } + + // Create image view + vk::raii::ImageView createImageView(vk::raii::Image& image, vk::Format format) { + vk::ImageViewCreateInfo viewInfo{ + .image = *image, + .viewType = vk::ImageViewType::e2D, + .format = format, + .subresourceRange = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + return device.createImageView(viewInfo); + } + + // Transition image layout + void transitionImageLayout(vk::raii::Image& image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout) { + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + vk::raii::CommandBuffer commandBuffer = std::move(device.allocateCommandBuffers(allocInfo)[0]); + + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + commandBuffer.begin(beginInfo); + + vk::ImageMemoryBarrier barrier{ + .oldLayout = oldLayout, + .newLayout = newLayout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *image, + .subresourceRange = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + vk::PipelineStageFlags sourceStage; + vk::PipelineStageFlags destinationStage; + + if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { + barrier.srcAccessMask = vk::AccessFlagBits::eNone; + 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, + vk::DependencyFlagBits::eByRegion, + nullptr, + nullptr, + barrier + ); + + commandBuffer.end(); + + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + + graphicsQueue.submit(submitInfo, nullptr); + graphicsQueue.waitIdle(); + } + + // Copy buffer to image + void copyBufferToImage(vk::raii::Buffer& buffer, vk::raii::Image& image, uint32_t width, uint32_t height) { + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + vk::raii::CommandBuffer commandBuffer = std::move(device.allocateCommandBuffers(allocInfo)[0]); + + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + commandBuffer.begin(beginInfo); + + vk::BufferImageCopy region{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {width, height, 1} + }; + + commandBuffer.copyBufferToImage( + *buffer, + *image, + vk::ImageLayout::eTransferDstOptimal, + region + ); + + commandBuffer.end(); + + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + + graphicsQueue.submit(submitInfo, nullptr); + graphicsQueue.waitIdle(); + } + + // Update uniform buffer + void updateUniformBuffer(uint32_t currentImage) { + 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{}; + ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); + ubo.view = glm::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), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f); + ubo.proj[1][1] *= -1; + + void* data; + data = uniformBuffersMemory[currentImage].mapMemory(0, sizeof(ubo)); + memcpy(data, &ubo, sizeof(ubo)); + uniformBuffersMemory[currentImage].unmapMemory(); + } + +#if PLATFORM_ANDROID + // Handle app commands + static void handleAppCommand(android_app* app, int32_t cmd) { + auto* vulkanApp = static_cast(app->userData); + switch (cmd) { + case APP_CMD_INIT_WINDOW: + // Window created, initialize Vulkan + if (app->window != nullptr) { + vulkanApp->initVulkan(); + } + break; + case APP_CMD_TERM_WINDOW: + // Window destroyed, clean up Vulkan + vulkanApp->cleanup(); + break; + default: + break; + } + } + + // Handle input events + static int32_t handleInputEvent(android_app* app, AInputEvent* event) { + auto* vulkanApp = static_cast(app->userData); + if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) { + // Handle touch events + float x = AMotionEvent_getX(event, 0); + float y = AMotionEvent_getY(event, 0); + + // Process touch coordinates + LOGI("Touch at: %f, %f", x, y); + + return 1; + } + return 0; + } +#endif +}; + +// Platform-specific entry point +#if PLATFORM_ANDROID +// Android main entry point +void android_main(android_app* app) { + // Make sure glue isn't stripped + app_dummy(); + + try { + // Create and run the Vulkan application + HelloTriangleApplication vulkanApp(app); + vulkanApp.run(); + } catch (const std::exception& e) { + LOGE("Exception caught: %s", e.what()); + } +} +#else +// Desktop main entry point +int main() { + try { + HelloTriangleApplication app; + app.run(); + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} +#endif diff --git a/attachments/CMakeLists.txt b/attachments/CMakeLists.txt index 75a70109..5d30f0bd 100644 --- a/attachments/CMakeLists.txt +++ b/attachments/CMakeLists.txt @@ -243,3 +243,9 @@ add_chapter (33_vulkan_profiles MODELS viking_room.obj TEXTURES viking_room.png LIBS glm::glm tinyobjloader::tinyobjloader) + +add_chapter (34_android + SHADER 27_shader_depth + MODELS viking_room.obj + TEXTURES viking_room.png + LIBS glm::glm tinyobjloader::tinyobjloader) diff --git a/attachments/android/app/build.gradle b/attachments/android/app/build.gradle new file mode 100644 index 00000000..d7a1115e --- /dev/null +++ b/attachments/android/app/build.gradle @@ -0,0 +1,64 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdkVersion 33 + defaultConfig { + applicationId "com.vulkan.tutorial" + minSdkVersion 24 + targetSdkVersion 33 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + version "3.22.1" + } + } + + ndkVersion "25.2.9519653" + + // Use assets from the main project and locally compiled shaders + sourceSets { + main { + assets { + srcDirs = [ + // Point to the main project's assets + '../../../../', // For models and textures in the attachments directory + // Use locally compiled shaders from the build directory for all ABIs + // These paths are relative to the app directory + '.externalNativeBuild/cmake/debug/arm64-v8a/shaders', + '.externalNativeBuild/cmake/debug/armeabi-v7a/shaders', + '.externalNativeBuild/cmake/debug/x86/shaders', + '.externalNativeBuild/cmake/debug/x86_64/shaders', + // Also include release build paths + '.externalNativeBuild/cmake/release/arm64-v8a/shaders', + '.externalNativeBuild/cmake/release/armeabi-v7a/shaders', + '.externalNativeBuild/cmake/release/x86/shaders', + '.externalNativeBuild/cmake/release/x86_64/shaders' + ] + } + } + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.androidgamesdk:game-activity:1.2.0' +} diff --git a/attachments/android/app/src/main/AndroidManifest.xml b/attachments/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..31ba18b5 --- /dev/null +++ b/attachments/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/attachments/android/app/src/main/cpp/CMakeLists.txt b/attachments/android/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..1352c12e --- /dev/null +++ b/attachments/android/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,128 @@ +cmake_minimum_required(VERSION 3.22.1) + +project(vulkan_tutorial_android) + +# Set the path to the main CMakeLists.txt relative to this file +set(MAIN_CMAKE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../CMakeLists.txt") + +# Find the Vulkan package +find_package(Vulkan REQUIRED) + +# Find the tinyobjloader package +find_package(tinyobjloader REQUIRED) + +# Find the glm package +find_package(glm REQUIRED) + +# Find the stb_image.h header +find_path(STB_INCLUDEDIR stb_image.h PATH_SUFFIXES stb) +if (NOT STB_INCLUDEDIR) + message(FATAL_ERROR "stb_image.h not found") +endif() + +# Set up shader compilation tools +add_executable(glslang::validator IMPORTED) +find_program(GLSLANG_VALIDATOR "glslangValidator" HINTS $ENV{VULKAN_SDK}/bin REQUIRED) +set_property(TARGET glslang::validator PROPERTY IMPORTED_LOCATION "${GLSLANG_VALIDATOR}") + +# Define shader building function +function(add_shaders_target TARGET) + cmake_parse_arguments("SHADER" "" "CHAPTER_NAME" "SOURCES" ${ARGN}) + set(SHADERS_DIR ${SHADER_CHAPTER_NAME}/shaders) + add_custom_command( + OUTPUT ${SHADERS_DIR} + COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADERS_DIR} + ) + add_custom_command( + OUTPUT ${SHADERS_DIR}/frag.spv ${SHADERS_DIR}/vert.spv + COMMAND glslang::validator + ARGS --target-env vulkan1.0 ${SHADER_SOURCES} --quiet + WORKING_DIRECTORY ${SHADERS_DIR} + DEPENDS ${SHADERS_DIR} ${SHADER_SOURCES} + COMMENT "Compiling Shaders" + VERBATIM + ) + add_custom_target(${TARGET} DEPENDS ${SHADERS_DIR}/frag.spv ${SHADERS_DIR}/vert.spv) +endfunction() + +# Include the game-activity library +find_package(game-activity REQUIRED CONFIG) +include_directories(${ANDROID_NDK}/sources/android/game-activity/include) + +# Set C++ standard to match the main project +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add the Vulkan C++ module +add_library(VulkanCppModule SHARED) +target_compile_definitions(VulkanCppModule + PUBLIC VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS=1 +) +target_include_directories(VulkanCppModule + PRIVATE + "${Vulkan_INCLUDE_DIR}" +) +target_link_libraries(VulkanCppModule + PUBLIC + ${Vulkan_LIBRARIES} +) +set_target_properties(VulkanCppModule PROPERTIES CXX_STANDARD 20) + +# Set up the C++ module file set +target_sources(VulkanCppModule + PUBLIC + FILE_SET cxx_modules TYPE CXX_MODULES + BASE_DIRS + "${Vulkan_INCLUDE_DIR}" + FILES + "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" +) + +# Set up shader compilation for 34_android +set(SHADER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments") +set(SHADER_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders") +file(MAKE_DIRECTORY ${SHADER_OUTPUT_DIR}) + +# Copy shader source files to the build directory +configure_file( + "${SHADER_SOURCE_DIR}/27_shader_depth.frag" + "${SHADER_OUTPUT_DIR}/27_shader_depth.frag" + COPYONLY +) +configure_file( + "${SHADER_SOURCE_DIR}/27_shader_depth.vert" + "${SHADER_OUTPUT_DIR}/27_shader_depth.vert" + COPYONLY +) + +# Compile shaders +set(SHADER_SOURCES "${SHADER_OUTPUT_DIR}/27_shader_depth.frag" "${SHADER_OUTPUT_DIR}/27_shader_depth.vert") +add_shaders_target(android_shaders CHAPTER_NAME "${SHADER_OUTPUT_DIR}" SOURCES ${SHADER_SOURCES}) + +# Add the main native library +add_library(vulkan_tutorial_android SHARED + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/34_android.cpp + game_activity_bridge.cpp +) + +# Add dependency on shader compilation +add_dependencies(vulkan_tutorial_android android_shaders) + +# Set include directories +target_include_directories(vulkan_tutorial_android PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${Vulkan_INCLUDE_DIR} + ${ANDROID_NDK}/sources/android/game-activity/include + ${STB_INCLUDEDIR} +) + +# Link against libraries +target_link_libraries(vulkan_tutorial_android + VulkanCppModule + game-activity::game-activity + android + log + ${Vulkan_LIBRARIES} + tinyobjloader::tinyobjloader + glm::glm +) diff --git a/attachments/android/app/src/main/cpp/game_activity_bridge.cpp b/attachments/android/app/src/main/cpp/game_activity_bridge.cpp new file mode 100644 index 00000000..6e44065d --- /dev/null +++ b/attachments/android/app/src/main/cpp/game_activity_bridge.cpp @@ -0,0 +1,32 @@ +#include +#include +#include + +// Define logging macros +#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__)) + +// Forward declaration of the main entry point +extern "C" void android_main(android_app* app); + +// GameActivity entry point +extern "C" { + void GameActivity_onCreate(GameActivity* activity) { + LOGI("GameActivity_onCreate"); + + // Create an android_app structure + android_app* app = new android_app(); + memset(app, 0, sizeof(android_app)); + + // Set up the android_app structure + app->activity = activity; + app->window = activity->window; + + // Call the original android_main function + android_main(app); + + // Clean up + delete app; + } +} diff --git a/attachments/android/app/src/main/java/com/example/vulkantutorial/VulkanActivity.java b/attachments/android/app/src/main/java/com/example/vulkantutorial/VulkanActivity.java new file mode 100644 index 00000000..bdb370f6 --- /dev/null +++ b/attachments/android/app/src/main/java/com/example/vulkantutorial/VulkanActivity.java @@ -0,0 +1,20 @@ +package com.vulkan.tutorial; + +import android.os.Bundle; +import android.view.WindowManager; +import com.google.androidgamesdk.GameActivity; + +public class VulkanActivity extends GameActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Keep the screen on while the app is running + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + // Load the native library + static { + System.loadLibrary("vulkan_tutorial_android"); + } +} diff --git a/attachments/android/app/src/main/res/values/strings.xml b/attachments/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..ed278c96 --- /dev/null +++ b/attachments/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Vulkan Tutorial + diff --git a/attachments/android/app/src/main/res/values/styles.xml b/attachments/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..c63a3a91 --- /dev/null +++ b/attachments/android/app/src/main/res/values/styles.xml @@ -0,0 +1,6 @@ + + + + diff --git a/attachments/android/build.gradle b/attachments/android/build.gradle new file mode 100644 index 00000000..745418c2 --- /dev/null +++ b/attachments/android/build.gradle @@ -0,0 +1,24 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.4.2' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/attachments/android/settings.gradle b/attachments/android/settings.gradle new file mode 100644 index 00000000..5cb940b7 --- /dev/null +++ b/attachments/android/settings.gradle @@ -0,0 +1,2 @@ +include ':app' +rootProject.name = "VulkanTutorial" diff --git a/en/14_Android.adoc b/en/14_Android.adoc new file mode 100644 index 00000000..99d9866d --- /dev/null +++ b/en/14_Android.adoc @@ -0,0 +1,835 @@ +:pp: {plus}{plus} + += Android: Taking Your Vulkan App Mobile + +== Introduction + +In the previous chapter, we explored how Vulkan profiles can simplify feature detection and make your code more maintainable. Now, let's take our Vulkan knowledge a step further by bringing our application to the mobile world with Android. + +While Vulkan was designed to be cross-platform from the ground up, deploying to Android introduces some new challenges and opportunities. The core Vulkan API remains the same, but the surrounding ecosystem - from window management to build systems - requires a different approach. + +This chapter will guide you through adapting your Vulkan application for Android, reusing as much code as possible while addressing platform-specific requirements. You'll see that with the right setup, you can maintain a single codebase that works across desktop and mobile platforms. + +== Android-specific Considerations + +Before diving into implementation details, let's understand the key differences when developing Vulkan applications for Android compared to desktop: + +1. *Window System Integration*: Instead of GLFW, we use Android's native window system and activity lifecycle. +2. *Application Lifecycle*: Android apps can be paused, resumed, or terminated by the system at any time, requiring careful resource management. +3. *Asset Loading*: Resources are packaged in APK files and accessed through Android's asset manager. +4. *Build System*: We use Gradle and CMake together to build Android applications. +5. *Input Handling*: Touch input replaces mouse and keyboard, requiring different event handling. + +These differences might seem daunting at first, but with the right approach, we can address them while maintaining a clean, maintainable codebase. + +== Project Setup + +Now that we understand the key differences, let's set up our Android project. Our goal is to reuse as much code as possible from our desktop implementation while addressing Android-specific requirements. + +=== Prerequisites + +Before we begin, make sure you have the following tools installed: + +* *Android Studio*: The official IDE for Android development +* *Android NDK (Native Development Kit)*: Enables native C++ development on Android +* *Android SDK*: With a recent API level (24+, which corresponds to Android 7.0 or higher) for Vulkan support +* *CMake and Ninja build tools*: For building native code (these can be installed through Android Studio) +* *Vulkan SDK*: For shader compilation tools and validation layers + +[IMPORTANT] +==== +Unlike the desktop environment, Vulkan HPP (the C++ bindings for Vulkan) is NOT included by default in the Android NDK. You'll need to download it separately from the https://github.com/KhronosGroup/Vulkan-Hpp[Vulkan-Hpp GitHub repository] or use the version included in the Vulkan SDK. +==== + +=== Project Structure + +Let's start by understanding the structure of our Android project. We'll follow the standard Android application structure, but with some modifications to efficiently reuse code from our main project: + +[source] +---- +android/ +├── app/ +│ ├── build.gradle // App-level build configuration +│ ├── src/ +│ │ ├── main/ +│ │ │ ├── AndroidManifest.xml // App manifest +│ │ │ ├── cpp/ // Native code +│ │ │ │ ├── CMakeLists.txt // CMake build script +│ │ │ │ └── game_activity_bridge.cpp // Bridge between GameActivity and our Vulkan code +│ │ │ ├── java/ // Java code +│ │ │ │ └── com/example/vulkantutorial/ +│ │ │ │ └── VulkanActivity.java // Main activity (extends GameActivity) +│ │ │ └── res/ // Resources +│ │ │ └── values/ +│ │ │ ├── strings.xml // String resources +│ │ │ └── styles.xml // Style resources +├── build.gradle // Project-level build configuration +├── gradle/ // Gradle wrapper +├── settings.gradle // Project settings +---- + +== Setting Up the Android Project + +With our project structure in place, let's dive into the key components of our Android Vulkan application. We'll start with the essential configuration files and then move on to the native code implementation. + +=== The Manifest File + +Every Android application requires a manifest file that declares important information about the app. For our Vulkan application, the AndroidManifest.xml file is particularly important as it specifies the Vulkan version requirements: + +[source,xml] +---- + + + + + + + + + + + + + + + + + + + + +---- + +Key points: +* We specify a minimum SDK version of 24 (Android 7.0), which is required for Vulkan support. +* We declare that our app uses Vulkan with specific version requirements. +* We set up our main activity (VulkanActivity) as the entry point for our application. + +=== Java Activity + +After configuring the manifest, we need to create the Java side of our application. While most of our Vulkan code will run in native C++, we still need a Java activity to serve as the entry point for our application. + +For our Vulkan application, we'll use the GameActivity from the Android Game SDK instead of the traditional NativeActivity. This modern approach offers better performance and features specifically designed for games and graphics-intensive applications: + +[source,java] +---- +package com.vulkan.tutorial; + +import android.os.Bundle; +import android.view.WindowManager; +import com.google.androidgamesdk.GameActivity; + +public class VulkanActivity extends GameActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Keep the screen on while the app is running + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + // Load the native library + static { + System.loadLibrary("vulkan_tutorial_android"); + } +} +---- + +Key points: +* We extend GameActivity from the Android Game SDK, which provides a more optimized bridge between Java and native code. +* GameActivity offers better performance for games and graphics-intensive applications compared to NativeActivity. +* We load our native library ("vulkan_tutorial_android") which contains our Vulkan implementation. + +=== Build Configuration + +With our Java activity in place, we need to configure the build process. Android uses Gradle as its build system, which we'll configure to work with our native Vulkan code and assets. + +The build configuration is split across multiple files, with different responsibilities: + +Project-level build.gradle: +[source,groovy] +---- +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.2.2' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} +---- + +App-level build.gradle: +[source,groovy] +---- +plugins { + id 'com.android.application' +} + +android { + compileSdkVersion 33 + defaultConfig { + applicationId "com.vulkan.tutorial" + minSdkVersion 24 + targetSdkVersion 33 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + version "3.22.1" + } + } + + ndkVersion "25.2.9519653" + + // Use assets from the main project and locally compiled shaders + sourceSets { + main { + assets { + srcDirs = [ + // Point to the main project's assets + '../../../../', // For models and textures in the attachments directory + // Use locally compiled shaders from the build directory for all ABIs + // These paths are relative to the app directory + '.externalNativeBuild/cmake/debug/arm64-v8a/shaders', + '.externalNativeBuild/cmake/debug/armeabi-v7a/shaders', + '.externalNativeBuild/cmake/debug/x86/shaders', + '.externalNativeBuild/cmake/debug/x86_64/shaders', + // Also include release build paths + '.externalNativeBuild/cmake/release/arm64-v8a/shaders', + '.externalNativeBuild/cmake/release/armeabi-v7a/shaders', + '.externalNativeBuild/cmake/release/x86/shaders', + '.externalNativeBuild/cmake/release/x86_64/shaders' + ] + } + } + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.androidgamesdk:game-activity:1.2.0' +} +---- + +Key points: +* We specify the minimum SDK version as 24 (Android 7.0) for Vulkan support. +* We configure CMake to build our native code. +* We include the game-activity dependency for better performance. +* We set up asset directories to reference the main project's assets and locally compiled shaders. +* This approach avoids duplicating assets and ensures we're using the latest versions. + +=== CMake Configuration + +While Gradle handles the overall Android build process, we use CMake to build our native C++ code. This is where we'll set up our Vulkan environment, compile shaders, and link against the necessary libraries. + +Let's examine our CMakeLists.txt file, which is the heart of our native code configuration: + +[source,cmake] +---- +cmake_minimum_required(VERSION 3.22.1) + +project(vulkan_tutorial_android) + +# Set the path to the main CMakeLists.txt relative to this file +set(MAIN_CMAKE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../CMakeLists.txt") + +# Find the Vulkan package +find_package(Vulkan REQUIRED) + +# Set up shader compilation tools +add_executable(glslang::validator IMPORTED) +find_program(GLSLANG_VALIDATOR "glslangValidator" HINTS $ENV{VULKAN_SDK}/bin REQUIRED) +set_property(TARGET glslang::validator PROPERTY IMPORTED_LOCATION "${GLSLANG_VALIDATOR}") + +# Define shader building function +function(add_shaders_target TARGET) + cmake_parse_arguments("SHADER" "" "CHAPTER_NAME" "SOURCES" ${ARGN}) + set(SHADERS_DIR ${SHADER_CHAPTER_NAME}/shaders) + add_custom_command( + OUTPUT ${SHADERS_DIR} + COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADERS_DIR} + ) + add_custom_command( + OUTPUT ${SHADERS_DIR}/frag.spv ${SHADERS_DIR}/vert.spv + COMMAND glslang::validator + ARGS --target-env vulkan1.0 ${SHADER_SOURCES} --quiet + WORKING_DIRECTORY ${SHADERS_DIR} + DEPENDS ${SHADERS_DIR} ${SHADER_SOURCES} + COMMENT "Compiling Shaders" + VERBATIM + ) + add_custom_target(${TARGET} DEPENDS ${SHADERS_DIR}/frag.spv ${SHADERS_DIR}/vert.spv) +endfunction() + +# Include the game-activity library +find_package(game-activity REQUIRED CONFIG) +include_directories(${ANDROID_NDK}/sources/android/game-activity/include) + +# Set C++ standard to match the main project +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add the Vulkan C++ module +add_library(VulkanCppModule SHARED) +target_compile_definitions(VulkanCppModule + PUBLIC VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS=1 +) +target_include_directories(VulkanCppModule + PRIVATE + "${Vulkan_INCLUDE_DIR}" +) +target_link_libraries(VulkanCppModule + PUBLIC + ${Vulkan_LIBRARIES} +) +set_target_properties(VulkanCppModule PROPERTIES CXX_STANDARD 20) + +# Set up the C++ module file set +target_sources(VulkanCppModule + PUBLIC + FILE_SET cxx_modules TYPE CXX_MODULES + BASE_DIRS + "${Vulkan_INCLUDE_DIR}" + FILES + "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" +) + +# Set up shader compilation for 34_android +set(SHADER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments") +set(SHADER_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders") +file(MAKE_DIRECTORY ${SHADER_OUTPUT_DIR}) + +# Copy shader source files to the build directory +configure_file( + "${SHADER_SOURCE_DIR}/27_shader_depth.frag" + "${SHADER_OUTPUT_DIR}/27_shader_depth.frag" + COPYONLY +) +configure_file( + "${SHADER_SOURCE_DIR}/27_shader_depth.vert" + "${SHADER_OUTPUT_DIR}/27_shader_depth.vert" + COPYONLY +) + +# Compile shaders +set(SHADER_SOURCES "${SHADER_OUTPUT_DIR}/27_shader_depth.frag" "${SHADER_OUTPUT_DIR}/27_shader_depth.vert") +add_shaders_target(android_shaders CHAPTER_NAME "${SHADER_OUTPUT_DIR}" SOURCES ${SHADER_SOURCES}) + +# Add the main native library +add_library(vulkan_tutorial_android SHARED + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/34_android.cpp + game_activity_bridge.cpp +) + +# Add dependency on shader compilation +add_dependencies(vulkan_tutorial_android android_shaders) + +# Set include directories +target_include_directories(vulkan_tutorial_android PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${Vulkan_INCLUDE_DIR} + ${ANDROID_NDK}/sources/android/game-activity/include +) + +# Link against libraries +target_link_libraries(vulkan_tutorial_android + VulkanCppModule + game-activity::game-activity + android + log + ${Vulkan_LIBRARIES} +) +---- + +Key points: +* We find the Vulkan package and include the game-activity library instead of native_app_glue. +* We set up shader compilation tools and define a function to compile shaders. +* We set the C++ standard to C++20 and create a Vulkan C++ module. +* We set up shader compilation for the 34_android chapter, copying shader source files from the main project. +* We add the main native library, which uses the 34_android.cpp file from the main project and a bridge file to connect with GameActivity. +* We link against the necessary libraries, including game-activity. + +== Native Implementation + +Now that we've set up our build configuration, let's dive into the native C++ code that powers our Vulkan application on Android. This is where the real magic happens - we'll see how to adapt our existing Vulkan code to work on Android while minimizing platform-specific changes. + +One of the key advantages of our approach is code reuse. Instead of maintaining separate codebases for desktop and Android, we've structured our project to share as much code as possible: + +1. *34_android.cpp*: This is the same file used in our main project, containing the core Vulkan implementation. By reusing this file, we ensure that our rendering code is identical across platforms. + +2. *game_activity_bridge.cpp*: This small bridge file connects the Android GameActivity to our core Vulkan code. It handles the platform-specific initialization and event processing. + +This separation of concerns allows us to focus on the Vulkan implementation without getting bogged down in platform-specific details. When we make improvements to our rendering code, both desktop and Android versions benefit automatically. + +=== GameActivity Bridge + +Let's take a closer look at our bridge code, which is the key to connecting our Java GameActivity with our native Vulkan implementation. This small but crucial file handles the translation between Android's Java-based activity lifecycle and our C++ code: + +[source,cpp] +---- +#include +#include +#include + +// Define logging macros +#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__)) + +// Forward declaration of the main entry point +extern "C" void android_main(android_app* app); + +// GameActivity entry point +extern "C" { + void GameActivity_onCreate(GameActivity* activity) { + LOGI("GameActivity_onCreate"); + + // Create an android_app structure + android_app* app = new android_app(); + memset(app, 0, sizeof(android_app)); + + // Set up the android_app structure + app->activity = activity; + app->window = activity->window; + + // Call the original android_main function + android_main(app); + + // Clean up + delete app; + } +} +---- + +This bridge code: +1. Creates an android_app structure compatible with our Vulkan code +2. Sets up the necessary connections between GameActivity and our code +3. Calls the android_main function in our 34_android.cpp file + +=== Android Entry Point + +Once our bridge code has created the android_app structure, it calls the android_main function, which serves as the entry point for our native code. This function is defined in our 34_android.cpp file and is analogous to the main() function in desktop applications: + +Let's look at how we initialize our Vulkan application from this entry point: + +[source,cpp] +---- +void android_main(android_app* app) { + try { + // Create and run the Vulkan application + HelloTriangleApplication application(app); + application.run(); + } catch (const std::exception& e) { + LOGE("Exception caught: %s", e.what()); + } +} +---- + +=== Creating the Vulkan Surface + +One of the key platform-specific differences in our Vulkan implementation is how we create the surface. On desktop, we used GLFW to create a window and surface. On Android, we need to use the VK_KHR_android_surface extension to create a surface from the native Android window. + +Here's how we create a Vulkan surface on Android: + +[source,cpp] +---- +void createSurface() { + VkSurfaceKHR _surface; + VkResult result = VK_SUCCESS; + + // Create Android surface + result = vkCreateAndroidSurfaceKHR( + *instance, + &(VkAndroidSurfaceCreateInfoKHR{ + .sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR, + .pNext = nullptr, + .flags = 0, + .window = androidApp->window + }), + nullptr, + &_surface + ); + + if (result != VK_SUCCESS) { + throw std::runtime_error("Failed to create Android surface"); + } + + surface = vk::raii::SurfaceKHR(instance, _surface); +} +---- + +=== Handling Android Events + +Another important platform-specific aspect is event handling. Android applications have a different lifecycle compared to desktop applications - they can be paused, resumed, or terminated by the system at any time. We need to handle these events properly to ensure our Vulkan resources are managed correctly. + +Here's how we handle Android-specific events in our application: + +[source,cpp] +---- +static void handleAppCommand(android_app* app, int32_t cmd) { + auto* vulkanApp = static_cast(app->userData); + switch (cmd) { + case APP_CMD_INIT_WINDOW: + // Window created, initialize Vulkan + if (app->window != nullptr) { + vulkanApp->initVulkan(); + } + break; + case APP_CMD_TERM_WINDOW: + // Window destroyed, clean up Vulkan + vulkanApp->cleanup(); + break; + default: + break; + } +} + +static int32_t handleInputEvent(android_app* app, AInputEvent* event) { + auto* vulkanApp = static_cast(app->userData); + if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) { + // Handle touch events + float x = AMotionEvent_getX(event, 0); + float y = AMotionEvent_getY(event, 0); + + // Process touch coordinates + // ... + + return 1; + } + return 0; +} +---- + +== Cross-Platform Implementation + +While we've focused on Android-specific code so far, our approach allows us to maintain a single codebase that works on both desktop and Android platforms. This is achieved through careful use of preprocessor directives and platform-specific abstractions. + +=== Platform Detection + +The first step in our cross-platform approach is to detect which platform we're building for. We use preprocessor directives to check for platform-specific predefined macros: + +[source,cpp] +---- +// Platform detection +#if defined(__ANDROID__) + #define PLATFORM_ANDROID 1 +#else + #define PLATFORM_DESKTOP 1 +#endif +---- + +This approach leverages the standard predefined macro `__ANDROID__` which is automatically defined by the compiler when building for Android platforms. These platform macros are then used throughout the code to conditionally compile platform-specific code. + +=== Consistent Class Structure + +To maintain a clean and consistent codebase, we use the same class name (`HelloTriangleApplication`) for both platforms. This makes it easier to understand the code and reduces the need for platform-specific branches: + +[source,cpp] +---- +// Cross-platform application class +class HelloTriangleApplication { +public: +#if PLATFORM_DESKTOP + // Desktop constructor + HelloTriangleApplication() { + // No Android-specific initialization needed + } +#else + // Android constructor + HelloTriangleApplication(android_app* app) : androidApp(app) { + // Android-specific initialization + } +#endif + // ... rest of the class ... +}; +---- + +=== Platform-Specific Includes + +Different platforms require different header files. We use preprocessor directives to include the appropriate headers: + +[source,cpp] +---- +// Platform-specific includes +#if PLATFORM_ANDROID + // Android-specific includes + #include + #include + #include + #include +#else + // Desktop-specific includes + #define GLFW_INCLUDE_VULKAN + #include + #include + #include +#endif +---- + +=== Cross-Platform File Loading + +File loading is one of the key differences between desktop and Android platforms. On desktop, we load files from the filesystem, while on Android, we load them from the APK's assets. We've created a cross-platform file loading function that works on both platforms: + +[source,cpp] +---- +// Cross-platform file reading function +std::vector readFile(const std::string& filename, std::optional assetManager = std::nullopt) { +#if PLATFORM_ANDROID + // On Android, use asset manager if provided + if (assetManager.has_value() && *assetManager != nullptr) { + // Open the asset + AAsset* asset = AAssetManager_open(*assetManager, filename.c_str(), AASSET_MODE_BUFFER); + // ... read file from asset ... + return buffer; + } +#endif + + // Desktop version or Android fallback to filesystem + std::ifstream file(filename, std::ios::ate | std::ios::binary); + // ... read file from filesystem ... + return buffer; +} +---- + +=== Platform-Specific Entry Points + +Each platform has its own entry point. On desktop, we use the standard `main()` function, while on Android, we use the `android_main()` function: + +[source,cpp] +---- +// Platform-specific entry point +#if PLATFORM_ANDROID +// Android main entry point +void android_main(android_app* app) { + // Android-specific initialization + try { + HelloTriangleApplication vulkanApp(app); + vulkanApp.run(); + } catch (const std::exception& e) { + LOGE("Exception caught: %s", e.what()); + } +} +#else +// Desktop main entry point +int main() { + try { + HelloTriangleApplication app; + app.run(); + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} +#endif +---- + +=== Build System Integration + +Our cross-platform approach leverages the compiler's built-in platform detection capabilities. Since the `__ANDROID__` macro is automatically defined by the compiler when building for Android, we don't need to explicitly define platform macros in our build system. + +This approach has several advantages: +1. *Simplicity*: We don't need to maintain platform-specific compile definitions in our CMake files. +2. *Reliability*: We rely on standard compiler behavior rather than custom definitions. +3. *Maintainability*: Less build system configuration means fewer potential points of failure. + +By using the compiler's predefined macros, we can maintain a single codebase that works on both desktop and Android platforms, with minimal platform-specific code. When we make improvements to our rendering code, both desktop and Android versions benefit automatically. + +== Shader Handling on Android + +Now that we've covered the core native implementation, let's address another important aspect of Vulkan development on Android: shader handling. Shaders are a critical part of any Vulkan application, and we need to ensure they're properly compiled and loaded on Android. + +In our approach, we compile shaders locally during the build process, similar to how it's done in the main project. This strategy offers several significant advantages: + +1. *Consistency*: We use the same shader source files for both desktop and Android builds, ensuring identical visual results across platforms. +2. *Maintainability*: When we need to update a shader, we only need to change it in one place, and both desktop and Android versions benefit. +3. *Build-time validation*: Shader compilation errors are caught during the build process, not at runtime, making debugging much easier. + +=== Local Shader Compilation + +We've set up our CMake configuration to compile shaders locally during the build process: + +1. *Define a shader building function*: ++ +[source,cmake] +---- +function(add_shaders_target TARGET) + cmake_parse_arguments("SHADER" "" "CHAPTER_NAME" "SOURCES" ${ARGN}) + set(SHADERS_DIR ${SHADER_CHAPTER_NAME}/shaders) + add_custom_command( + OUTPUT ${SHADERS_DIR} + COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADERS_DIR} + ) + add_custom_command( + OUTPUT ${SHADERS_DIR}/frag.spv ${SHADERS_DIR}/vert.spv + COMMAND glslang::validator + ARGS --target-env vulkan1.0 ${SHADER_SOURCES} --quiet + WORKING_DIRECTORY ${SHADERS_DIR} + DEPENDS ${SHADERS_DIR} ${SHADER_SOURCES} + COMMENT "Compiling Shaders" + VERBATIM + ) + add_custom_target(${TARGET} DEPENDS ${SHADERS_DIR}/frag.spv ${SHADERS_DIR}/vert.spv) +endfunction() +---- + +2. *Copy shader source files from the main project*: ++ +[source,cmake] +---- +# Set up shader compilation for 34_android +set(SHADER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments") +set(SHADER_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders") +file(MAKE_DIRECTORY ${SHADER_OUTPUT_DIR}) + +# Copy shader source files to the build directory +configure_file( + "${SHADER_SOURCE_DIR}/27_shader_depth.frag" + "${SHADER_OUTPUT_DIR}/27_shader_depth.frag" + COPYONLY +) +configure_file( + "${SHADER_SOURCE_DIR}/27_shader_depth.vert" + "${SHADER_OUTPUT_DIR}/27_shader_depth.vert" + COPYONLY +) +---- + +3. *Compile the shaders*: ++ +[source,cmake] +---- +# Compile shaders +set(SHADER_SOURCES "${SHADER_OUTPUT_DIR}/27_shader_depth.frag" "${SHADER_OUTPUT_DIR}/27_shader_depth.vert") +add_shaders_target(android_shaders CHAPTER_NAME "${SHADER_OUTPUT_DIR}" SOURCES ${SHADER_SOURCES}) + +# Add dependency on shader compilation +add_dependencies(vulkan_tutorial_android android_shaders) +---- + +4. *Reference the compiled shaders in the Gradle build*: ++ +[source,groovy] +---- +sourceSets { + main { + assets { + srcDirs = [ + // Point to the main project's assets + '../../../../', // For models and textures in the attachments directory + // Use locally compiled shaders from the build directory for all ABIs + '.externalNativeBuild/cmake/debug/arm64-v8a/shaders', + '.externalNativeBuild/cmake/debug/armeabi-v7a/shaders', + // ... other ABIs ... + ] + } + } +} +---- + +=== Loading Assets in a Cross-Platform Way + +Our unified readFile function makes it easy to load assets in a cross-platform way. Here's how we use it to load shader files: + +[source,cpp] +---- +// Load shader files using cross-platform function +#if PLATFORM_ANDROID +std::optional optionalAssetManager = assetManager; +#else +std::optional optionalAssetManager = std::nullopt; +#endif +std::vector vertShaderCode = readFile("shaders/vert.spv", optionalAssetManager); +std::vector fragShaderCode = readFile("shaders/frag.spv", optionalAssetManager); +---- + +We use the same approach to load texture images and model files: + +[source,cpp] +---- +// Load texture image +#if PLATFORM_ANDROID +std::optional optionalAssetManager = assetManager; +std::vector imageData = readFile(TEXTURE_PATH, optionalAssetManager); +// Process the image data... +#else +// Load directly from filesystem +// ... +#endif +---- + +This unified approach gives us the best of both worlds: we use the same code structure for both platforms, with the platform-specific differences handled by the readFile function itself. This makes our code more maintainable and easier to understand. + +== Building and Running + +Now that we've set up our Android project with all the necessary components, let's put everything together and run our Vulkan application on an Android device. + +The process is straightforward: + +1. Open the project in Android Studio. +2. Connect an Android device or start an emulator (make sure it supports Vulkan). +3. Click the "Run" button in Android Studio. + +Android Studio will handle the rest - it will build the application, compile the shaders, package everything into an APK, install it on the device/emulator, and launch it. If everything is set up correctly, you should see your Vulkan application running on Android, rendering the same scene as on desktop. + +== Conclusion + +In this chapter, we've explored how to take our Vulkan application from desktop to mobile by adapting it for Android. We've seen that while the core Vulkan API remains the same across platforms, the surrounding ecosystem requires platform-specific adaptations. + +Our approach demonstrates several key principles that you can apply to your own Vulkan projects: + +1. *Code Reuse*: By structuring our project properly, we can use the same core rendering code (34_android.cpp) for both desktop and Android platforms, minimizing duplication and maintenance overhead. + +2. *Modern Android Integration*: We leverage the GameActivity from the Android Game SDK for better performance and more streamlined integration compared to the older NativeActivity approach. + +3. *Efficient Asset Management*: Instead of duplicating assets, we reference them from the main project, ensuring consistency and reducing APK size. + +4. *Local Shader Compilation*: By compiling shaders during the build process, we catch errors early and ensure compatibility across platforms. + +5. *Minimal Platform-Specific Code*: We isolate platform-specific code in a small bridge file, keeping our core Vulkan implementation clean and portable. + +This approach not only makes it easier to maintain and update our application but also provides a solid foundation for expanding to other platforms in the future. When you make improvements to your core rendering code, both desktop and Android versions benefit automatically. + +The complete Android example can be found in the attachments/android directory. Feel free to use it as a template for your own Vulkan projects on Android. + +Remember that Vulkan HPP is not included by default in the Android NDK, so you'll need to download it separately from the https://github.com/KhronosGroup/Vulkan-Hpp[Vulkan-Hpp GitHub repository] or use the version included in the Vulkan SDK. From 4b4c1335e53cecb2efd75ddb104dbe2656bdd37d Mon Sep 17 00:00:00 2001 From: swinston Date: Thu, 3 Jul 2025 01:41:37 -0700 Subject: [PATCH 04/29] Add support for glTF and KTX textures with improved dependency management - Expanded Linux, Windows, and Android scripts to include `tinygltf`, `nlohmann-json`, and `KTX` dependencies. - Introduced `FindKTX.cmake` and `FindTinyGLTF.cmake` for cross-platform library handling with fallback to FetchContent if unavailable. - Added `35_gltf_ktx` chapter featuring glTF model and KTX texture integration in Vulkan, with Android and desktop support. - Enhanced Gradle and CMake configurations for selective chapter builds and streamlined dependencies. --- attachments/35_gltf_ktx.cpp | 1408 +++++++++++++++++ attachments/CMake/FindKTX.cmake | 105 ++ attachments/CMake/FindTinyGLTF.cmake | 106 ++ attachments/CMakeLists.txt | 11 +- attachments/android/README.md | 63 + attachments/android/app/build.gradle | 4 + .../android/app/src/main/cpp/CMakeLists.txt | 57 +- .../tutorial}/VulkanActivity.java | 0 attachments/viking_room.glb | Bin 0 -> 174032 bytes attachments/viking_room.ktx2 | Bin 0 -> 3145992 bytes en/15_GLTF_KTX2_Migration.adoc | 450 ++++++ scripts/install_dependencies_linux.sh | 20 + scripts/install_dependencies_windows.bat | 9 + 13 files changed, 2226 insertions(+), 7 deletions(-) create mode 100644 attachments/35_gltf_ktx.cpp create mode 100644 attachments/CMake/FindKTX.cmake create mode 100644 attachments/CMake/FindTinyGLTF.cmake create mode 100644 attachments/android/README.md rename attachments/android/app/src/main/java/com/{example/vulkantutorial => vulkan/tutorial}/VulkanActivity.java (100%) create mode 100644 attachments/viking_room.glb create mode 100644 attachments/viking_room.ktx2 create mode 100644 en/15_GLTF_KTX2_Migration.adoc diff --git a/attachments/35_gltf_ktx.cpp b/attachments/35_gltf_ktx.cpp new file mode 100644 index 00000000..596d7303 --- /dev/null +++ b/attachments/35_gltf_ktx.cpp @@ -0,0 +1,1408 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +import vulkan_hpp; +#include +#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 + + #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__)) + #define LOG_INFO(msg) LOGI("%s", msg) + #define LOG_ERROR(msg) LOGE("%s", msg) +#else + #define GLFW_INCLUDE_VULKAN + #include + + #define LOG_INFO(msg) std::cout << msg << std::endl + #define LOG_ERROR(msg) std::cerr << msg << std::endl +#endif + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#define GLM_ENABLE_EXPERIMENTAL +#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; + +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; + app->onInputEvent = handleInputEvent; + + int events; + android_poll_source* source; + + while (app->destroyRequested == 0) { + while (ALooper_pollAll(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; + auto* vulkanApp = static_cast(app->userData); + vulkanApp->initVulkan(); + 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; + + vk::raii::Queue graphicsQueue = nullptr; + vk::raii::Queue presentQueue = 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; + uint32_t graphicsIndex = 0; + + std::vector presentCompleteSemaphore; + std::vector renderFinishedSemaphore; + std::vector inFlightFences; + uint32_t semaphoreIndex = 0; + uint32_t currentFrame = 0; + + bool framebufferResized = false; + + std::vector requiredDeviceExtension = { + vk::KHRSwapchainExtensionName, + vk::KHRSpirv14ExtensionName, + vk::KHRSynchronization2ExtensionName, + 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(); + } + +#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::ApiVersion14 + }; + + auto extensions = getRequiredExtensions(); + + vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &appInfo, + .enabledExtensionCount = static_cast(extensions.size()), + .ppEnabledExtensionNames = extensions.data() + }; + + instance = vk::raii::Instance(context, createInfo); + LOG_INFO("Vulkan instance created"); + } + + void setupDebugMessenger() { + if (!enableValidationLayers) return; + + vk::DebugUtilsMessageSeverityFlagsEXT severityFlags( vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError ); + vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags( vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation ); + vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT{ + .messageSeverity = severityFlags, + .messageType = messageTypeFlags, + .pfnUserCallback = &debugCallback + }; + debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfoEXT); + } + + 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; + strcpy(profileProperties.profileName, VP_KHR_ROADMAP_2022_NAME); + profileProperties.specVersion = VP_KHR_ROADMAP_2022_SPEC_VERSION; + + VkBool32 supported = VK_FALSE; + VkResult result = vpGetPhysicalDeviceProfileSupport(*instance, *physicalDevice, &profileProperties, &supported); + + if (result == VK_SUCCESS && supported == VK_TRUE) { + appInfo.profileSupported = true; + appInfo.profile = profileProperties; + LOG_INFO("Device supports Vulkan profile: " + std::string(profileProperties.profileName)); + } else { + LOG_INFO("Device does not support Vulkan profile: " + std::string(profileProperties.profileName)); + } + } else { + throw std::runtime_error("failed to find a suitable GPU!"); + } + } + + void createLogicalDevice() { + // find the index of the first queue family that supports graphics + std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); + + // get the first index into queueFamilyProperties which supports graphics + auto graphicsQueueFamilyProperty = std::ranges::find_if( queueFamilyProperties, []( auto const & qfp ) + { return (qfp.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast(0); } ); + + graphicsIndex = static_cast( std::distance( queueFamilyProperties.begin(), graphicsQueueFamilyProperty ) ); + + // determine a queueFamilyIndex that supports present + // first check if the graphicsIndex is good enough + auto presentIndex = physicalDevice.getSurfaceSupportKHR( graphicsIndex, *surface ) + ? graphicsIndex + : ~0; + if ( presentIndex == queueFamilyProperties.size() ) + { + // the graphicsIndex doesn't support present -> look for another family index that supports both + // graphics and present + for ( size_t i = 0; i < queueFamilyProperties.size(); i++ ) + { + if ( ( queueFamilyProperties[i].queueFlags & vk::QueueFlagBits::eGraphics ) && + physicalDevice.getSurfaceSupportKHR( static_cast( i ), *surface ) ) + { + graphicsIndex = static_cast( i ); + presentIndex = graphicsIndex; + break; + } + } + if ( presentIndex == queueFamilyProperties.size() ) + { + // there's nothing like a single family index that supports both graphics and present -> look for another + // family index that supports present + for ( size_t i = 0; i < queueFamilyProperties.size(); i++ ) + { + if ( physicalDevice.getSurfaceSupportKHR( static_cast( i ), *surface ) ) + { + presentIndex = static_cast( i ); + break; + } + } + } + } + if ( ( graphicsIndex == queueFamilyProperties.size() ) || ( presentIndex == queueFamilyProperties.size() ) ) + { + throw std::runtime_error( "Could not find a queue for graphics or 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 = graphicsIndex, .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); + + graphicsQueue = vk::raii::Queue(device, graphicsIndex, 0); + presentQueue = vk::raii::Queue(device, presentIndex, 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 = graphicsIndex + }; + 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 + ktxTexture2* 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 }; + graphicsQueue.submit(submitInfo, nullptr); + graphicsQueue.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(); + graphicsQueue.submit(vk::SubmitInfo{ .commandBufferCount = 1, .pCommandBuffers = &*commandCopyBuffer }, nullptr); + graphicsQueue.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[semaphoreIndex], 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[semaphoreIndex], + .pWaitDstStageMask = &waitDestinationStageMask, .commandBufferCount = 1, .pCommandBuffers = &*commandBuffers[currentFrame], + .signalSemaphoreCount = 1, .pSignalSemaphores = &*renderFinishedSemaphore[imageIndex] }; + graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); + + + const vk::PresentInfoKHR presentInfoKHR{ .waitSemaphoreCount = 1, .pWaitSemaphores = &*renderFinishedSemaphore[imageIndex], + .swapchainCount = 1, .pSwapchains = &*swapChain, .pImageIndices = &imageIndex }; + result = presentQueue.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!"); + } + semaphoreIndex = (semaphoreIndex + 1) % presentCompleteSemaphore.size(); + 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) { + LOG_ERROR("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) { + LOG_ERROR(e.what()); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} +#endif diff --git a/attachments/CMake/FindKTX.cmake b/attachments/CMake/FindKTX.cmake new file mode 100644 index 00000000..e23f11dd --- /dev/null +++ b/attachments/CMake/FindKTX.cmake @@ -0,0 +1,105 @@ +# FindKTX.cmake +# +# Finds the KTX library +# +# This will define the following variables +# +# KTX_FOUND +# KTX_INCLUDE_DIRS +# KTX_LIBRARIES +# +# and the following imported targets +# +# KTX::ktx +# + +# Check if we're on Linux - if so, we'll skip the search and directly use FetchContent +if(UNIX AND NOT APPLE) + # On Linux, we assume KTX is not installed and proceed directly to fetching it + set(KTX_FOUND FALSE) +else() + # On non-Linux platforms, try to find KTX using pkg-config first + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_KTX QUIET ktx libktx ktx2 libktx2) + endif() + + # Try to find KTX using standard find_package + find_path(KTX_INCLUDE_DIR + NAMES ktx.h + PATH_SUFFIXES include ktx KTX ktx2 KTX2 + HINTS + ${PC_KTX_INCLUDEDIR} + /usr/include + /usr/local/include + $ENV{KTX_DIR}/include + $ENV{VULKAN_SDK}/include + ${CMAKE_SOURCE_DIR}/external/ktx/include + ) + + find_library(KTX_LIBRARY + NAMES ktx ktx2 libktx libktx2 + PATH_SUFFIXES lib lib64 + HINTS + ${PC_KTX_LIBDIR} + /usr/lib + /usr/lib64 + /usr/local/lib + /usr/local/lib64 + $ENV{KTX_DIR}/lib + $ENV{VULKAN_SDK}/lib + ${CMAKE_SOURCE_DIR}/external/ktx/lib + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(KTX + REQUIRED_VARS KTX_INCLUDE_DIR KTX_LIBRARY + ) + + # Debug output if KTX is not found (only on non-Linux platforms) + if(NOT KTX_FOUND) + message(STATUS "KTX include directory search paths: ${PC_KTX_INCLUDEDIR}, /usr/include, /usr/local/include, $ENV{KTX_DIR}/include, $ENV{VULKAN_SDK}/include, ${CMAKE_SOURCE_DIR}/external/ktx/include") + message(STATUS "KTX library search paths: ${PC_KTX_LIBDIR}, /usr/lib, /usr/lib64, /usr/local/lib, /usr/local/lib64, $ENV{KTX_DIR}/lib, $ENV{VULKAN_SDK}/lib, ${CMAKE_SOURCE_DIR}/external/ktx/lib") + endif() +endif() + +if(KTX_FOUND) + set(KTX_INCLUDE_DIRS ${KTX_INCLUDE_DIR}) + set(KTX_LIBRARIES ${KTX_LIBRARY}) + + if(NOT TARGET KTX::ktx) + add_library(KTX::ktx UNKNOWN IMPORTED) + set_target_properties(KTX::ktx PROPERTIES + IMPORTED_LOCATION "${KTX_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES "${KTX_INCLUDE_DIRS}" + ) + endif() +else() + # If not found, use FetchContent to download and build + include(FetchContent) + + # Only show the message on non-Linux platforms + if(NOT (UNIX AND NOT APPLE)) + message(STATUS "KTX not found, fetching from GitHub...") + endif() + + FetchContent_Declare( + ktx + GIT_REPOSITORY https://github.com/KhronosGroup/KTX-Software.git + GIT_TAG v4.3.1 # Use a specific tag for stability + ) + + # Set options to minimize build time and dependencies + set(KTX_FEATURE_TOOLS OFF CACHE BOOL "Build KTX tools" FORCE) + set(KTX_FEATURE_DOC OFF CACHE BOOL "Build KTX documentation" FORCE) + set(KTX_FEATURE_TESTS OFF CACHE BOOL "Build KTX tests" FORCE) + + FetchContent_MakeAvailable(ktx) + + # Create an alias to match the expected target name + if(NOT TARGET KTX::ktx) + add_library(KTX::ktx ALIAS ktx) + endif() + + set(KTX_FOUND TRUE) +endif() diff --git a/attachments/CMake/FindTinyGLTF.cmake b/attachments/CMake/FindTinyGLTF.cmake new file mode 100644 index 00000000..259b219c --- /dev/null +++ b/attachments/CMake/FindTinyGLTF.cmake @@ -0,0 +1,106 @@ +# FindTinyGLTF.cmake +# +# Finds the TinyGLTF library +# +# This will define the following variables +# +# TinyGLTF_FOUND +# TinyGLTF_INCLUDE_DIRS +# +# and the following imported targets +# +# tinygltf::tinygltf +# + +# First, try to find nlohmann_json +find_package(nlohmann_json QUIET) +if(NOT nlohmann_json_FOUND) + include(FetchContent) + message(STATUS "nlohmann_json not found, fetching from GitHub...") + FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.2 # Use a specific tag for stability + ) + FetchContent_MakeAvailable(nlohmann_json) +endif() + +# Try to find TinyGLTF using standard find_package +find_path(TinyGLTF_INCLUDE_DIR + NAMES tiny_gltf.h + PATH_SUFFIXES include tinygltf +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(TinyGLTF + REQUIRED_VARS TinyGLTF_INCLUDE_DIR +) + +if(TinyGLTF_FOUND) + set(TinyGLTF_INCLUDE_DIRS ${TinyGLTF_INCLUDE_DIR}) + + if(NOT TARGET tinygltf::tinygltf) + add_library(tinygltf::tinygltf INTERFACE IMPORTED) + set_target_properties(tinygltf::tinygltf PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${TinyGLTF_INCLUDE_DIRS}" + INTERFACE_COMPILE_DEFINITIONS "TINYGLTF_IMPLEMENTATION;TINYGLTF_NO_EXTERNAL_IMAGE;TINYGLTF_NO_STB_IMAGE;TINYGLTF_NO_STB_IMAGE_WRITE" + ) + if(TARGET nlohmann_json::nlohmann_json) + target_link_libraries(tinygltf::tinygltf INTERFACE nlohmann_json::nlohmann_json) + endif() + endif() +else() + # If not found, use FetchContent to download and build + include(FetchContent) + + message(STATUS "TinyGLTF not found, fetching from GitHub...") + FetchContent_Declare( + tinygltf + GIT_REPOSITORY https://github.com/syoyo/tinygltf.git + GIT_TAG v2.8.18 # Use a specific tag for stability + ) + + # Configure tinygltf before making it available + FetchContent_GetProperties(tinygltf) + if(NOT tinygltf_POPULATED) + FetchContent_Populate(tinygltf) + + # Update the minimum required CMake version to avoid deprecation warning + file(READ "${tinygltf_SOURCE_DIR}/CMakeLists.txt" TINYGLTF_CMAKE_CONTENT) + string(REPLACE "cmake_minimum_required(VERSION 3.6)" + "cmake_minimum_required(VERSION 3.10)" + TINYGLTF_CMAKE_CONTENT "${TINYGLTF_CMAKE_CONTENT}") + file(WRITE "${tinygltf_SOURCE_DIR}/CMakeLists.txt" "${TINYGLTF_CMAKE_CONTENT}") + + # Create a symbolic link to make nlohmann/json.hpp available + if(EXISTS "${tinygltf_SOURCE_DIR}/json.hpp") + file(MAKE_DIRECTORY "${tinygltf_SOURCE_DIR}/nlohmann") + file(CREATE_LINK "${tinygltf_SOURCE_DIR}/json.hpp" "${tinygltf_SOURCE_DIR}/nlohmann/json.hpp" SYMBOLIC) + endif() + + # Set tinygltf to header-only mode + set(TINYGLTF_HEADER_ONLY ON CACHE BOOL "Use header only version" FORCE) + set(TINYGLTF_INSTALL OFF CACHE BOOL "Do not install tinygltf" FORCE) + + # Add the subdirectory after modifying the CMakeLists.txt + add_subdirectory(${tinygltf_SOURCE_DIR} ${tinygltf_BINARY_DIR}) + endif() + + # Create an alias for the tinygltf target + if(NOT TARGET tinygltf::tinygltf) + add_library(tinygltf_wrapper INTERFACE) + target_link_libraries(tinygltf_wrapper INTERFACE tinygltf) + target_compile_definitions(tinygltf_wrapper INTERFACE + TINYGLTF_IMPLEMENTATION + TINYGLTF_NO_EXTERNAL_IMAGE + TINYGLTF_NO_STB_IMAGE + TINYGLTF_NO_STB_IMAGE_WRITE + ) + if(TARGET nlohmann_json::nlohmann_json) + target_link_libraries(tinygltf_wrapper INTERFACE nlohmann_json::nlohmann_json) + endif() + add_library(tinygltf::tinygltf ALIAS tinygltf_wrapper) + endif() + + set(TinyGLTF_FOUND TRUE) +endif() diff --git a/attachments/CMakeLists.txt b/attachments/CMakeLists.txt index 5d30f0bd..e57e9f29 100644 --- a/attachments/CMakeLists.txt +++ b/attachments/CMakeLists.txt @@ -7,6 +7,9 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/CMake") find_package (glfw3 REQUIRED) find_package (glm REQUIRED) find_package (Vulkan REQUIRED) +find_package (tinyobjloader REQUIRED) +find_package (TinyGLTF REQUIRED) +find_package (KTX REQUIRED) # set up Vulkan C++ module add_library(VulkanCppModule) @@ -35,8 +38,6 @@ target_sources(VulkanCppModule "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" ) -find_package (tinyobjloader REQUIRED) - find_package (PkgConfig) pkg_get_variable (STB_INCLUDEDIR stb includedir) if (NOT STB_INCLUDEDIR) @@ -249,3 +250,9 @@ add_chapter (34_android MODELS viking_room.obj TEXTURES viking_room.png LIBS glm::glm tinyobjloader::tinyobjloader) + +add_chapter (35_gltf_ktx + SHADER 27_shader_depth + MODELS viking_room.glb + TEXTURES viking_room.ktx2 + LIBS glm::glm tinygltf::tinygltf KTX::ktx) diff --git a/attachments/android/README.md b/attachments/android/README.md new file mode 100644 index 00000000..2b04ec88 --- /dev/null +++ b/attachments/android/README.md @@ -0,0 +1,63 @@ +# Android Project for Vulkan Tutorial + +This Android project allows you to run different chapters of the Vulkan Tutorial on Android devices. + +## Selecting a Chapter + +By default, the project builds and runs the `34_android` chapter. You can select a different chapter by setting the `chapter` property in your Gradle build. + +### Available Chapters + +- `34_android`: The Android chapter that uses tinyobjloader to load OBJ models +- `35_gltf_ktx`: The glTF and KTX chapter that uses tinygltf to load glTF models and KTX to load KTX2 textures + +### How to Select a Chapter + +#### From the Command Line + +```bash +./gradlew assembleDebug -Pchapter=35_gltf_ktx +``` + +#### From Android Studio + +1. Edit the `gradle.properties` file in the project root directory +2. Add the following line: + ``` + chapter=35_gltf_ktx + ``` +3. Sync the project and build + +## Adding New Chapters + +To add support for a new chapter: + +1. Add the chapter name to the `SUPPORTED_CHAPTERS` list in `app/src/main/cpp/CMakeLists.txt` +2. Add any chapter-specific libraries and compile definitions in the same file +3. Make sure the chapter's source file exists in the `attachments` directory + +For example, to add support for a hypothetical `36_new_feature` chapter: + +```cmake +# Define the list of supported chapters +set(SUPPORTED_CHAPTERS + "34_android" + "35_gltf_ktx" + "36_new_feature" +) + +# Add chapter-specific libraries and definitions +if(CHAPTER STREQUAL "34_android") + # ... +elseif(CHAPTER STREQUAL "35_gltf_ktx") + # ... +elseif(CHAPTER STREQUAL "36_new_feature") + target_link_libraries(vulkan_tutorial_android + # Add any required libraries here + ) + + target_compile_definitions(vulkan_tutorial_android PRIVATE + # Add any required compile definitions here + ) +endif() +``` diff --git a/attachments/android/app/build.gradle b/attachments/android/app/build.gradle index d7a1115e..4bcfa416 100644 --- a/attachments/android/app/build.gradle +++ b/attachments/android/app/build.gradle @@ -10,6 +10,9 @@ android { targetSdkVersion 33 versionCode 1 versionName "1.0" + + // Define which chapter to build (default to 34_android) + buildConfigField "String", "CHAPTER", "\"${project.findProperty('chapter') ?: '34_android'}\"" } buildTypes { @@ -28,6 +31,7 @@ android { cmake { path "src/main/cpp/CMakeLists.txt" version "3.22.1" + arguments "-DCHAPTER=${project.findProperty('chapter') ?: '34_android'}" } } diff --git a/attachments/android/app/src/main/cpp/CMakeLists.txt b/attachments/android/app/src/main/cpp/CMakeLists.txt index 1352c12e..de2df537 100644 --- a/attachments/android/app/src/main/cpp/CMakeLists.txt +++ b/attachments/android/app/src/main/cpp/CMakeLists.txt @@ -14,6 +14,12 @@ find_package(tinyobjloader REQUIRED) # Find the glm package find_package(glm REQUIRED) +# Find the tinygltf package (needed for 35_gltf_ktx) +find_package(tinygltf REQUIRED) + +# Find the KTX package (needed for 35_gltf_ktx) +find_package(KTX REQUIRED) + # Find the stb_image.h header find_path(STB_INCLUDEDIR stb_image.h PATH_SUFFIXES stb) if (NOT STB_INCLUDEDIR) @@ -78,7 +84,7 @@ target_sources(VulkanCppModule "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" ) -# Set up shader compilation for 34_android +# Set up shader compilation for all chapters set(SHADER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments") set(SHADER_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders") file(MAKE_DIRECTORY ${SHADER_OUTPUT_DIR}) @@ -97,16 +103,35 @@ configure_file( # Compile shaders set(SHADER_SOURCES "${SHADER_OUTPUT_DIR}/27_shader_depth.frag" "${SHADER_OUTPUT_DIR}/27_shader_depth.vert") -add_shaders_target(android_shaders CHAPTER_NAME "${SHADER_OUTPUT_DIR}" SOURCES ${SHADER_SOURCES}) +add_shaders_target(vulkan_tutorial_shaders CHAPTER_NAME "${SHADER_OUTPUT_DIR}" SOURCES ${SHADER_SOURCES}) + +# Set default chapter if not provided +if(NOT DEFINED CHAPTER) + set(CHAPTER "34_android") +endif() + +# Define the list of supported chapters +set(SUPPORTED_CHAPTERS + "34_android" + "35_gltf_ktx" +) + +# Validate the chapter +list(FIND SUPPORTED_CHAPTERS ${CHAPTER} CHAPTER_INDEX) +if(CHAPTER_INDEX EQUAL -1) + message(FATAL_ERROR "Invalid chapter: ${CHAPTER}. Supported chapters are: ${SUPPORTED_CHAPTERS}") +endif() + +message(STATUS "Building chapter: ${CHAPTER}") # Add the main native library add_library(vulkan_tutorial_android SHARED - ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/34_android.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/${CHAPTER}.cpp game_activity_bridge.cpp ) # Add dependency on shader compilation -add_dependencies(vulkan_tutorial_android android_shaders) +add_dependencies(vulkan_tutorial_android vulkan_tutorial_shaders) # Set include directories target_include_directories(vulkan_tutorial_android PRIVATE @@ -123,6 +148,28 @@ target_link_libraries(vulkan_tutorial_android android log ${Vulkan_LIBRARIES} - tinyobjloader::tinyobjloader glm::glm ) + +# Add chapter-specific libraries and definitions +if(CHAPTER STREQUAL "34_android") + target_link_libraries(vulkan_tutorial_android + tinyobjloader::tinyobjloader + ) +elseif(CHAPTER STREQUAL "35_gltf_ktx") + target_link_libraries(vulkan_tutorial_android + tinygltf::tinygltf + KTX::ktx + ) + + # Add necessary definitions for tinygltf and KTX + target_compile_definitions(vulkan_tutorial_android PRIVATE + TINYGLTF_IMPLEMENTATION + TINYGLTF_NO_EXTERNAL_IMAGE + TINYGLTF_NO_STB_IMAGE + TINYGLTF_NO_STB_IMAGE_WRITE + KTX_FEATURE_KTX1 + KTX_FEATURE_KTX2 + KTX_FEATURE_WRITE + ) +endif() diff --git a/attachments/android/app/src/main/java/com/example/vulkantutorial/VulkanActivity.java b/attachments/android/app/src/main/java/com/vulkan/tutorial/VulkanActivity.java similarity index 100% rename from attachments/android/app/src/main/java/com/example/vulkantutorial/VulkanActivity.java rename to attachments/android/app/src/main/java/com/vulkan/tutorial/VulkanActivity.java diff --git a/attachments/viking_room.glb b/attachments/viking_room.glb new file mode 100644 index 0000000000000000000000000000000000000000..0f07ce7ad578a104bb6e863fb945f44049307b0a GIT binary patch literal 174032 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 literal 0 HcmV?d00001 diff --git a/attachments/viking_room.ktx2 b/attachments/viking_room.ktx2 new file mode 100644 index 0000000000000000000000000000000000000000..5e78df73a49ba9a6e804a39d11e7fbc13c147930 GIT binary patch literal 3145992 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
  • |DWHUJ=WPU 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

    UJq&J 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

  • )7!N;c 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�N!^U_J zEe)~?U`Px|M@OPEBKU9mf8J{ZZZ8U-&%2@)_A`ukafwDaNqVf4*V!c-CS$Y=DHGt5 z8IR(PnB7Gqd^q>g<}SM`@&BN3XwW0~O2t(9jMV?M`lZ~(m%uIdVOeM}_lIG)19b8t z@BD=iTC(K&J@d?YTmbCC1DV}>!tva>i|5W=z@B;L!ntP-kDP{P15P^Xz1)6i`plDV zIdK4=8c&3^fCu6Kynh3jcSO}gwlBedAfISIVSfx4;0rZ#1n9>*_yvgou1`gvXITQU zeZig}{!a(|pA>Qip%+}|O1&+>uB9CKpZcG~|KC&of4^DaAA6IMK4~fbf3*HT$--}t z|C^K8K4KoW`r`D3w8Z~kf&aDs=MA(#O+5B6{?9Uj)&ErfcZmPP|6%-}oxh0lhwyJM z06Y)X8~^h`#0yZQhwTaJvkHJ&{GT8{Bh6*N9iaXvUI1Dk_&*x_m%F%E>;K>ah*AIN zN92DB`1}Zr{Uks0f%u>86{xey#>mI)_?1*tm9iYI0+CZoN`jE{|`FS6B{Mo17x}uP#6p@5hcb za1g@Iz+V_&r9w{F)&V#a*0VA?zEZ}&7?Z{*N3ms&<+0^DR@P2CGJwX3HdVLjoNV7C^K>Z)z-OZ$~Xo}exk>h@cK|Y8d_%ZmRnBC8&+fwO7!i&e8 zhKsQa;srD~jq#6*Sqv#@-TiLhe_o0}1^#~}FW}1409O_f5U%j>nG`!ycx>v?olKG- zU<(1{2q1kIjg3RiOx4M2*k9U6Vu*w@SQZOpfTR^@J}jp+EKims0@7nK_8qfC;~>AF zT23de6?HgLLI+DVusw0mwjPdLjT*TLJVc-7`2TJp%g=4yFZ1C$&&vPzay~F`UO)~1 z(;2z|HR2y`VJ(CDsD*~cwuX2?`JeG1F8B&s8`uN4;44UMTy4zE(N5_XT2`?Tx1f0E zl#Exd(#9hYB-W}s^uxn89vZLVe|rU2xe*KB5;m;gZnt3^Sv#<7+;EsRY#41BqZ%PT zR0#h($_8)*+HF=gm>z40vPf=`l?#l*p}Fch1z!s*9e};o|B?Dd++`18yna~u>VmIA zHBQ`~y>RjOUBwaC1)&?yH^d&oxc%p44d)whJ=$Up3L^d@+~5BZ^grN-G1_`+~9pEG2DOfEaD+GGLe!=7s+|Mt@A7!u#U!(jFHiN-i zngWdGhn>Gd3;Ca=fp_xG`$gw{R6yzi9OeIL{x69DFJ?sB&lbS9Gx3*z|5O|oK;r+( z{}%s;|B3&TAP}wpZ~T8#_5T+CNBmFmPpj<{x&X%WI0E!T@`v$%V*XM7rx0i&W+}i7 z=F)f~Hlgu-vsO?h|$gd9I<%YNIH z|8rmQFMUnl_U1ln%f6FNY!?fov-7hP6^;LQk6h{*Ztogw?dUJIj&$`dfbp}_o72;~ zQ#02tFJ4_(+MipzGIM!vc6N70bU`~avpcgKt43RFR{8^**_lzBp4q8jv)FcJMr;b3 z6`P#hk@n1voLXaMYqG{>wZQ%eN@(kom55DO+f(bf zIg?Z3?9|vqwI^&$O|6el;jnB>OdPDQtS>JNw_T$CC*)5hC6cMfdIv{l7jAYmv)eMv z|9x-%PrZfTUdn&elD(5-FFbEM<*~Wa-GYQb@d6(CZ!lp?mF|4mocl#S)0%L7H&yU6 zo#}Lk@23;Vzy)AkyD|PoQx+|dRYa*f6C+i9rm!mzYjsqR2H_D9&}LDCAGTZB^gD&@ zr01omyUwCO%uz~rvW>#q*_fj#!$BoXL-#132i)Ee_p2sUFBVg?wj6^23jEP0)U-oK z;OLX>3DxHxKDnlcVbvbyVC`Ts;UvR`O%kVor45u$O-_Q%P1xmxe~3pCg61K&G+CfV z{BxAvAHL`D@IS?QC^1Soni}!{SqT5+G4MY-{~}}H2es~x^uu=^$Nv-;HT{oqzJ>9l z#{l(J?H}zwf&X>$FXR6hRe*qGZUm2^S#bdr*9E+_p=d3vOOHWvh{{60TI~}sMn|N< z_KW%i(Z<**HyVo5AZ%{@Pg~;uD$$pxRU7sSeWSdKQ%0JFJ9cr(yF`so>aN}xjtQSb z4|BtK0vaA%0CfapF~SJnrWC%2erwxZ_wc{^5nSaGY|mN=C4KlHI3V`fx!1`(ge{Qoe%!%uQA-U^=@u@LD455WI>dH*`We8<1zN`)T^{`@MN z{wC*tmrwtpDf4wUjq(Th#|41@SrI@Q1piYYC`bWdQ(z=O{*%l91kyJ=(f`!tfAl{G zo>H|5;M;kK_oX z;$u$3YASgvpQCWr!~BB%Iq3f%!+J!xwZU2Qe<@l;O6jv1j|!8o71AHIWq#L}|HqNy zzfZRO`&7$64ikdTf7#?-X(}O~9a7Xu0vAs9nJkWLt?fSy_`0m{0 zXXlrmUtPPswtjncR@QE4+`f+rz2#EH3R$&ux$^y|%P+d1kDm zd!ns-p}(|P8lEqeR?1UzYgb?D@BCAX`ezfBVVk2Nd*gU1*^i*zUCe~|l-sH>zMV7bz@j6CFjfo<@Z_&FF%o8J_spAYz7 zlLA8iXWb#n{~>{gOl~jY7+lf`@K{cj|Ev8~bdTPsV-ESByMhl`^$URcx^Fc8XE4Ek zoxePUdGZ8~u>Dc~=kqe2!HhAd!0zQn;s1*b-*goTVxZ5l_&;i{v(tEVFzxU(Y#jS2 zLNUj^=$K2lT7-DLI(T3 zkoiMC{TVKXj{eSwZ>i;PLYWg4epH04=J)Zwr2zaMSoOBEpY|EjD1A%j>V|4;exv>yr0hlg%eLE9C`hpSghy0&RnEx?+3Udc=e8?|A;AHXtm-A9*HT;$yrolfhl|M&G z#J<>o_{Z!7hJTn#kBR@Av*kn*#BFMfJ;EgbcK*sVG@>k`?#g}nv-kC{a~%Iu<1x(t zjpDuW{2}e1N186=uPiEF0CrdiwAyb^fdA3|lO@sG5Nm0Wvi1xPSsl7I;x(cMYcUDG zD>DbD>MR%=8yLgJ5Vc`2(90_nxuw*P4YMBf^SA(vH2#+-S1sB;u9Gp`i<81L8FSn% z;TWBR48v&CZCtd&_haRr-Oh)nr@(?(OWKb7GQR9`K4|Kpp*|4AZ7|3eJ+zWzHsT@koq#4 z{uA)uB>c}30EP+(@xSSRNCBoOXsG{T7Xb2q_n)l%U(x?d6g|TK==_gv0UYXoXuR8` z@qgq07U%7z)A|49Ox(=)pKRZd|8L?KsQ&+G{xAH`)vIZlp7B4)zciR|l>ZO)KPCaX zSN~58-}L_${|EaSj|&j+zY}9o0M0l5k1hc)fX)>^!vBw2|34De|JVJ$9$9Jq9}CwX z>59Mu)&GJ2sV)C{laEyUgJR~lo%w$mD*oqq>;IW;{jb>;e1Y$~n?A|AJH`CS$jn$z zslU+Hn{VyOw|3`RE@cbtnWp~c_KDts%fq9~h-AtWI}??Ix%nGQOSe{5o?lzLy|MA~ z*4E40+iF`cZHKn~()JPC+J5OodwFZSz6H;>w^WZi+b?5w$=Z+ZcxPw(K22N_ynwq` z5ANK$c4c{arh9PaQvd8gX{9u>)L-77Szg(_`D#zczqfn;G1~mUD{cR$()OR@E&n`J z{JJy$VKMVM`;cZ*Uv*~KnP}rt@IRb`5rqF`bM~`hmObnw&-X%O0|K9hI7UNVfJARb zRsp($Re&V@<^td*pjY{A>qEH!lQr@5M z^U=ot$t~URJ#^qr4GpM(*#8Sq|1lf^9!rYxj7$B@9QvjEBZ0H|JN&rvqk{8GogdP5 zAu%NGATGH854jv*JAdI>qyPCy_#a@V{=bFpjbjaKIYvDKt97X)FRW`y`8;rd4&pZjQ@3* z_Mo4aG~s}&ptAJ&L(O=0xMU*FKju!Z}NzKJtq2 z33TGla0?92{JBf{pD#h?iDpS9Oc#jo%Y0OZ#Jvob9(_CC0LDW!5kNlv*PR88|EvE0 z$EWVc0QP5mh4Fu%ll*1-Y5b3O5HpSMy-ebT>G^8YrU{!KOh zj|+eh$nt;R()?fQg>skF|6wCA@_+Bw^*GxvocsGFCW%_?ak!Mf^W4i2&sPBK1N4!z#e5`7r-iHUhT%Us8bK z|LbUk0R9O7lL%}8Ulaca=~4cmPO+<^#62H{4l(qM z)&G=i|6kc`D4v|>)=i=0kfnT6KIz~A>Jhg@~f8EX}$sLfbjct+%K85W3tF@0r z|AVaJ?rE{afdBLUtNHYM#mrZoIck3X$5_k%o^JVnrkejpsrYqA?yYQcA)o8%9qDiD z?#vWB(oG$irZzv{lFAjmOh=~B-_kjJsh3T`=KD*exFWVcn4Q15xQqzrc?3M08(=>i z{?gXg->n5s5DOyvSMJ`rwzs@6)iuBpK&5YBVW70oKe|1)ytuyia%b~DwR!)uRQ%5= zzWg z1z;5b7eE#Q$OUl31;AMD#s%0+``@+ZL^b_Dzl4zhk#R{5Y(CEJw|_^$fBUfH2#_iK zWAVS{blB#<z!Rt`gmdxZCI7+_C&d3l{x`Z-Iu*z?vr~h7d;~g;b))@U4Nn;$C@cBD zvIPM8ABdVx25=)h54cL~T4MMz(%4wqRbUPqK)B$sCdM|ac1YhM&`Wx(^1om_Pg7xx ztOx-AxB$V8`f7U%M|)A-qhYy>(OKbc@C3WDX1sO^e%nj!tKeu2p2JMIs`IJyMjar7 zL;lyYKZ5!+@QnCS>HQ{%|NouxKQ%(xCg9lk zKYtfMbW(K9n*0Myp?#m)BgCK_|3yc4{&l6o|Hmr${}o|>s{+Ip01E#W{3lI-WMSC? zEbd=*WclF9;eV=I>__{zw1+NaO$H5`z6*Zfekz&337@uLbr~G_>lz z9Ic`KhfB(^gFE6Wlu3%K?sG2H=iSLtY zPf{@aKKTEYobdmzFXjHyUjRFS&;J}N{<**D1gk})YRUZ%C&ge=2W)X z%NE^CKJovv_bx7SU01&UwWO}9Q+1wI)%ED=r&|4z`rUe~-!Dmx-bmd-4`gE-+t|j& z4mLJ6HZcxiLPAIgGlUt!Wx@~=F2j!y5<&=J7%szb|ES+*t##`3QFp8LvOIEHusH0q zYoC42sg}AwYwhn^JD}ZNuJ_hEhPn=pcJ?0c9W)o<(#g};W@c~9&2KC$-dbL=IQ*XG zf83vih1;vEx38`(9h(~K=$+^|IN8%Tb+GSb-|*sz>8TTQHyV|n*Suf%;eyrh!hV0a z_RIbX>A>(mKcH(=Md*0i_`j)c$ipSi_x(zdnv*a*2G0ZhM*is|{j37`l!;a$5MDrM z5c;|N0dW?HS<1EZq5rc+SvClM{EZ;F3h1IPXesBI$p86_)jy-BLhnQsAmx9l|HGd% z$^ZT4!c$B|RO)u6h!BW8Q!0y+4ltB=*bA)2=CCapJc915>jGTPY)7AE{2zqY z*P&#Y>;n8otwcE>YLCzQ9_tR>l#psJ3c&L$5U?olDC+`@!FL$S7bwNg{sNB|^n0A2 z)Xx0hDw{@a;eYUQi^0{#|8_K(w`0<;WFB+A@W0mog#XF^g{2LT83WBi*COU=!nOf6 z{nFEP0cdauAPwk3!oW6O16R=x0CVIP7XN1$pQ=tqqVX30=Z>&6b8hi6((|X=c6WOu zZpw|A(@;M>Oz#`dX^9N<@KWiwgZ#O>nLI$sFb^K=hpG)*PH~F8p;00yzLMO)^ z(D=XjN;J5fC*%t;{r?{4|G|8L-N7EH|5xniH``hvSjz|J3!1SD|5wD208s*TBNs0L z*#5uo`vJ61wlA6=tNc%0pm%(!5k#iI&4TO~d-ype_WScz&_Yk|A&CTBmx&@|6j`jNcf+cKO+7C{zGyB9OC~M18%4B z|7QLF?fic!lrib}Km2dl59Fu$pLTlxubBVyQo|FHXBB|80BAqqe|jK)g8%dY|C{{Z zbOesU|2Py${Ga-tISKnx8-H2lYx^XL5h%%n% zrjB1eJxwmaI_bbmu?w)5`Ty3^(k&91mzFm!o}C?;7^!!UGnOd;&hoUC?KcxRk_&J!kW~O&fP%OHa-pe)DCtQC0orw|RQc$?U5)>#{8@JPDF0JC z6#YLXK+*rqg&s=0Z&re09v`tOHUeg=DR!G>MS#=Oo;y(}!0jvtuqc2>kSq$&`2<#O zX&EWxaWtNh{wMiIC2pn~xXynEwp%4{mwmxl6gcB~L+lVoIVskX;J}tFzJMePWOC#S z?C1{A59!VL9?8Ye^vR!+|DpW^^=TOG+c9wp%xe(HKaedS$X0a;fFG`LJN@#JRIrpD z2-)Bq=?g#$*#h2MfZ6z;3YiS9wg~aU&+tFvVPlTghSgEd@HNsy{2!C?&Ai&!1xRst zn#rwgsLzptKf2r?^?$ff?EVYQN8rdSU_7wBSMqjTsQyUoLD;L=B(&AJ5R5nHg$7}K z`F_OR5d9DR>|cNyl7ByZ%KUt*vRQx>{ES~&zr!Z~?{E20S7pR&*8kjl`XBh8`u{ug zzrjB$oouj{Hs~F9APCkf;rY63LP{0Tdj-h?enU3`cqa(n3#8ybM|S>2_K#~2Z~p5@ z@c%-{wa*Rw69-84|I(CLDi75EY~g>Y|DzQ?iQ%XD zpFlm&|3Lm%70^-Ye=6`Z7-s=_Kk$FF$^YBy{}}(H|EK;By8wUV^?#KAz3u{#EK2}J z^@jQe`3d$bBn$SN7myrr7R)KIYq?4Ry9wd(g2Fd)x(V${?{=E*w7Vqz$xZ1=+HoU! zNL~04{^uL$c%1k@m4D!W;{R;^CHy}G{8xW7SpK53c)J`@z>0^gmKyb_QuT_Jg1~>p z4a>^^qg}nH`VTLSO^6C;dUk!zR6t92_o@U)v`@hQ(h>;~ z)GxZWvUFl-c&OSjRAU1L*+XHty=S6tWZ>Z8<;Z`d=zh{D{^Vf!ANyMW@lfRl-6blr zzEzP;b+1I;7aIS6g8W~D|NHZNGyC>S41dS4f8qr&z`OuxpZwpxDC!A9aRL0q1&~z$ z{0LT~{-IuaMYm6S^awWkAGAZ%`Ga!@oKc^|Tz>dHq2-GsNC=ef8GD!N&?3 zkcj@D?Su|(60%6*G3zl-<_O+zr)YZkXp5O6u;bw8q|;tXAI9|m;vn$8je>hD@Lzs3 z{r{`P|4Y%k)$sF<(m(Z8emzwE-B9IUddnX*qAMl;Ou^}Ky-KlG3oDR+IUlvUq4IxN zbbR=~Ei5;p){b(ut6V=+ZLl@KR8K!DpmS5lE}uFhSpXCTB@19r>i>a%$e%oD)zeP%uJYSZaEMkA4ndh6J{`Q8s24n|N^B*A)Ex{N zm#8RPe63c(BjEIyR~RqkaR-jOE~1*Wq3t>5xyN@h`pGX~Cy~2~(2w||PoC!+Y1qGW zj(9iZ$Wtcw%^E8m5e^Op-V@;;%;(7+2KbYu0XzcQ9oZb)YOs{>Oo`$_{EwE&kwT6T zrVu`05632WpET&)!v8kRj>i8CW(;s!Lj0rrFG~Q*@W#mu=2InSIU03ag8{HHJx1Le z0bs@HcqEQMO;K6lS)%1e(ai}(bZMX|CjEI|112TD+L$x z!If-qH81J9*EMOEeS0~*k`FHB#TU4c6EEOgE;w5VmYv`nxd7?~kPeK&{ARZcBKx`V z+`|86{9pJVc!rtL|Lhq5x4i(!|D`n0swd%nlt1s9{-64v)c;ZPC*c38!aqsAr14%4 za@X{D5ArAeubY37`HKWd`9HVnv)5Sex_-(?%lu9AKeWF_j(`^YVWq(M|Ge)1OR93h z|1?DWulxVf-sFGV|4YUHrvFLe|BQ$E$p`@Z6Zntwf1CRMIR3B2KjD8?0gwZIgZdu= zr0{>)VV_W!>{m80lFdV%LK{7@XDCN8PPNkpJp9O1W;&saPkI`TE0*qg%yu&8?v0&q zVRw&Tuu#wC+w@_Wif^Fk6sG(~)&IX*5&aJ$lP@|;|Jd95tAWbD4z&KPxBN~$Iv;r_ z3r?r&7mJNrSgrcyidz);xA?!~mGi>?ZBe_+f4IWt@pSpNLvH*nt z7Z-2sNf+SG$}%r5+ZF)-x>>-=$rI#CbVt>$VwLP^&8Mz(R@xh_4K|US_wwf>|7In4 zts1^ni(acmw_AgoMgKa{&d~e3Arj?Bum1xMJt)t2JMHh#lgR=9#{cF5;0081pCuhQ z#44_bkAMrnr?zWAl*_X(z+06FsCyI++~oh2nPqYMq5}q2BF^)S-Q*PrD=kvs3~9Q`j@p0Rul^igGIfq7ZHpdq@Z7Wqz?mP>dw;0_r7bEFgg=PTt4f&aPi zojS3ya&}*4|4YTfDYjBQDUh=_>TyP^FP_E~uE1ULW8Um)!T ztmV`N$OQ|;_MPB1m*l0wzl;a_nTJbQLC}QX^UsgwKON)J_&@c3jHMry|6SpK!u}Hf zNB^Iy{}22V{72q@P3!!?{}@!yTEN|+)c@oo+8F$Qt0cjHBK9{}8Yspifq(M?NDW{W zfc)P@s9uqt#RB*rm*FfPL30bRB>qoGf58*uEimx{DA%(q|8t4O{L{xc1y~mag#VdJ z_5X-};D5FW_@?UrpgJc0e>Mt^`u@qlKNa{Veeb02pOE8L+tSJ49gTU?mL8-1Tc_QA zquqKQaMSc%(rvlpwilr%{Vp;-lxHUM_h=v`;t3?~&R|ym=haps%+cGG@UxEM&wE?{ zrLXeOedQl^m)@v_tC4rSAYsYYpi+hZqgu6CuSV4>YXL#IRk8rWy6}H%qf}`xRwxkE zS8WV;bRX**q#P)_0AHP+S)W_jfd7}5_SzDFRXw_?${+lo%O}sAKXH0yaxl{_SV`bu*!4*zf@eI9B+gOna7xag_`%jinfjZ0c zeeV(Se7D2D1pk}-Z!Q2wwg78s$>9R@H!A{_)dj#WU@&V6tO9sh=j(du?MHI~jQ>eP zrvA@f(EnJ?f8NwrE3{;DAV)UWnlJFZ4YjX`~ zSB|mLN$dZJBcL!$zo9ZQa{+9MCN()3h=s91^f*v&WUkDtQFIxJAtfSOPEeh@FqI;= zXn`05X)_78V0{s(3~UT$>$IB~pTyx4>`%h-jI_ZVnTnc49O3`inr?zVWZVh=+k4qd z*}K!GAG6t-K0>T4;>qn(eRBDtc;(os(fXfU#jBzJCnMXwN#TFp?U(PIHb=fa?yz zd#5bfzR3PX`4dq-REvD-kv6^nv3=lQvH+<3Nft2DAT|On3fp5i0=NTLb%S6D{)e0T zA0$8&2whTP4{HI)fgpbx9FyQbE&#!NKppWvp2B_cf9ikcbJX@n>M!PM!mn2Qu}jjC zfAa$F1Htp@%I z`~S9umzDq59{iIf2KxkgzT5dV~nOTv41Ab^fe<16qtg)LFniXKfBQ;z=l`_+$G{%`OpG4d)^G4{IGP^7r^dh>;eUev z6#r-cFI@-_oK83ying?lg#T$6io?Tkpk9Ojbi&N6Ai%o9|B!i80_1BB?kg}}dADynb8I5LI{?eX-qd7-%gx_DvJ;$Ljxe_osikOyww zkueyjg8e*_J{3LO#l9{EH(FZiI6k_jz|2en{8QI}smO8Q8z%Ul_PKC&2>$cF%o$3i zv&8@5e_+`7-|#qrd!Ab-RDB&GLlE#pIx zPm+0Y1gPYR=AWIsOb!J9BLq6{1!M$Y^dx2Af*V}$Wsl%BjA}n*6Mu_qqmTESu*v_4 z{s$^v^R@oJGQW{O$E&_%1jqUx*SVwo|2>WW3;$O{;cxtpya(E+)ITPr{`H6pAcCJ1 z|C9W?5h?$B;s{U3@HGU$1b0qxi&Yuu{eKgkLmwW{s8|s z7nhJfU7wk~c=Ghh=+yMc$VhJ=j#8x%N*YChG-^~dQ7x!c!b-2>jOCq44-|Ay`R`QIY{3B%La(aaG@2HO~O1ngR2V2%7YWUmLQfCK=;tAyVfU`H639*zl| z8}@I)e-q9jWN6&@pRl~pa*K#|xFdMXz2JY4xXpMt*8ju|&sE9%G5;IJHz$uh0#)@G z)hqw2d(w0tl71V^srB&i@V`K~Vn05GHI&a4_|=c#-8W4iQusgir(~U?L*6K!(`JMJ zG5M}E;TJ_ylmBH=fG>pc3IB7++jX?ErL6J)K8cO*5o5_?-%AJhJM~_@bg>+stAyY> zaeLG|z(1M1pgz@mDC6^CDD^xs8dBBAmyhrQkoV&P$Yx%e4sc$DKo`m(b%A7efL1B; zBS*+zw+wiGe-{lDTAU_W;eQ%At~ zUseGEQ4N{$$H*UaH~zQ!Kd69=|LLE~OWDt4ra49g9HxBv_oN$P(-h4vEWd8--$|1J9|N|>hfZr4|Da>S_S+l$&Vlv`?-SdH^E3N1v@2Ob z{c_fU^N)Gngy(IPB9f!o;rESd^iH+-R<(GqRrZ|1_`zvhdN*I``Tg6M*1^7LKj0g9 z5dTv#357WUXA9|DkUvfJ#`YYoGrbD1rIP|R)?@rHoBzW95(AGBH{pLE7J>zgjing_ zHUrr3f7<8~oK{bPk&FlaF*>?Z zQo=~w2TUI?b|sQFx3dRJ$ln_Bz$e)9)W*elYD2E0HpeEm4vTw%@v^z@K0bL z_@`(msL#H=AG4E}?%``kPHi(^2E6Wyr*MM{$p|Lk?*wNl3KWDFSw}G7*JXw0H|*d1 z&yHa7axpsF}fk$LieZ>rxvVJYys9#{%^$b|4JPH@9Coi z&{^UCGt|9VU)*ygfUNS*4HN&IK67^R*!1BMc1b=|ZvpY$Xy zfQeX=G57Jm?0S%G=#I}u`;e+d$e9EHhW{@w)AB%E&$bmPyta9h;l&Z-9Y&Nxt01fnLqu1oWIMLiqVRnNB;vzgT#nr(Es0{ z{(m6YPryG7|JEk{e={QTpMSqf3UK~|TK=up{L7)}e-QRV`vUXLqvi7w!>3cKfCgj- zi`^|bApNPlqyQ8DzY#juP#FpQ$NVqYuVH^^pA-Pnei<3_zwf~I#Q%9_*q&zw%Yk;% z1Y+`6{>KI2Bd~un{;vi9C!DMW_PJbWzX|^`iN*hQPJ}}k{4e7FW(qJF1V@DbUFH8L zFL}+MQR4q&uIIKm@PBpB8}MJsHQr^2>V zpLdWJ7ob03PaojFt>9Nk`;F>ks@6l<2!PmiiT%Ief35#1!GHAsQMI?ycDT_wb+DgZ zz|Pw)V5cvP?C^ga045|(Y&@fYow=W}6W#{D zA#*7G!N!`GEFUBWlW!kvs{3SGYk={%05}oJwdn>QaoXkNN2zk@c_XfCHYVPgw1-@m zw|HGjO@^;miXYTU?^TPhR-~NHO{IN&oNtQu)BlCVpDG5c)#BO2D#f!e?3?`t^t&c4U;PM)|AWxn3*%k@KPj;i|8HUb4Dc`fuXA#3+R~HT zEmFsLBxAT1vzux|Xg@uVwHd%Ga8pi`m!iR(+RvAOU!%C*JX*i;KTi(obA;sS;q|!- zmgfJapCxZ3Dj>=BH77zmCgXoxp||SgJ;?vE-m$L*2@q9w@7AM>8vLiW55a#b_Oovn zYM(Uue-Q;{cmdMiTm_)Wf1(1CB>*Hq90~qoJCXMD*{SjWaK+1?QT{iarv9h$Kf;?B z{%ItJ3qZvFhVZ|m{4ZI+Z<7D3`+ph#C$b+Ji22Fqh2&WXAO(N{=zp-6OOnG2U~?q@ z_qsR^;&dSUNBLvoACvtc_@VMA2LDqC6vGz){)yP5=Lgy;;6H)R2mhrISN>!<05krd zZ1O)|0OFs?eAf8i;GY>Va{+7#0M|f`eSv9Y5&XmdQu))D&40O9%>S+E|DXJ4ZTvs+ zg8!S2kMMu?arj^If1`J5)c=(FKfgRw`DgNfJB!5srvnc~N^dSF1%INpwy?GCQ~y)S z{s69}e9)Q?wEkx+y8yEbSZ6V*|1-2OI*#b&>h$!DS@yl!TlIe|_`go&&-wXl(=*gT zT^gS}*>`xPtGBDQPS{`gKkwrRsGn@a$+ z0bR0*@hOlM0ptYCQz-!QkW9;31Gk`I{12R~sD>VyKV@4dZ{90Q&N3v=W5}Kb>P=Wf zZY?vUT>zQZG){cVC18JIg6ML&CO&BTv(6?@ZVjH32IS9HUpzc*zAO8d_%`?goAG~9 z`{M!>_H7ni;xS|qK|Bk2DkkB}d5d2QdoTWP{Ez+z{=W_Xd!qc8;J+8VrNMu6K5#y? z|D}?@#s6^uI5ii5rGXDMBbZAl2V_|Q!vE|J7KInk{67U2?*7a38~@b!|E!;+TpuI?L)Fkn?DZ-%*jT2)>rBB)~{BQO~*||f7&nQPqub?~99b`YG@rIJd3)Q38 zr$O1z8yHE!3@(ZC=XgHLM>k8kE2{P};OD-g`PZCZ3-}xJ!~e^!T*VWbtHRUTlf(7` z?OM+t><9i?%fOE2a}#+<0pN%ufR8Yd&yMAZ|C0ic#T_s?5RDVf_LOc4#=gMgd0YU= z6gcB&A%ASzmrI6nEi>qUDo>UFYy3a{g8!e0|F3KOU-tj4M_+VN|G)CT`zycdZ~eNH z-E&Aa75*pwkJ?{~e-idbvZ5)g62wNq0{;gT|0y@9W?$0!|25qLxLN=Is@DHU2EV83 z|Hu5lxM2K$;rQvLk%{Anh7Nc3c9g1oY0CeIe?t2*h5!BHfRke%r75pK{2%=f{C~tP z4CM2}`TTf6mIGcXi}Gj2_`fJsn|@R;kw5$7dA@0NP)gqaApTFpKS=`@03lF5=k$cK zFYrOO1`fi&DbNr6Tc&VO@QYr!S`6MV`FBLBwCM#HQ=wHuEdxlu-$ZK~Y9+$yfVYOz zMb9QQzSSahHDu1t1Ri;{0|oIT`tpV2zK8sVj|jrsB|tv=Su@**%vqHvJ;o_gd{jD` zgi9XoHq#jZdky0U9Jg2;+kZMZe1gA6!vDo^rCvH)!-_BL*?j}JS1huivwDIbQ*z1U z@V{Uu@VSNjlT$uY6aHsp(ochaL^vA%7x9nrKh#Qyn{)sg#{c1g14OWacDUEzoKrz@ zwEZgYZ(_Nyy}1uIGU0#vxgFHak+$-`%pjri{Raf9h0Klr4c%kHPiDTa!DHy-(FxLX zEziS>124jT_1V(=uSo$~<7h|z=kC%k7c!N+yP3HFeAMKBS-((MMZX7qU|uK9|9qdu z|CVsgBx!t^ZO@{9-mH;dx0OtZP zV52CMemXA^`-^&&jL~6hUhaZ!^~KTv`78EYy-;-ig#Dp<$?c`Q4*`6Acv%S0 zik-GP0J={?{*Lm0LHK{t%|Z2}3ibfM>;-J5Jr^kx_H~Bw&ryjEZ7sWr>|MxdTl^-1}lcmU>o}-0) zKl&eJ|53H#$^O5@|J6*y?&BJu`~m*ED{Y4=4K4oJKP&tH9@qVUXEzoW$b`MUw~~KX zr0jp1|3@cI^bQUy|Km5^$N$sN8S0tzWte(_P1ySPC@QnBK*63)GY6rkiUic?U>O1jt!N|xSPlps z_rQV7exi1HHVdFgR8;Z9@i4@Hd8O#TT=LoF%GLtpsL^r->FsnasSThzWDd-0i2lB# zb%OdRaBvwusEK`8E52DR+DFM%G|0eZHhyEI8J_#f1L(rUj>qIz&2!bT_`mZ13;2Jw z-@voue>Nk)V9P}RV{C8A6IgUdV~jZn9Ggo3GE!sO!v7WO^q4=>OlWN&TO_rvFd!o$u8mYRz5E z$}U}Cyoi5%k@s8hAI?vbe-kgjgg~5H7Qh?6)&kWO03<+C1&CY#;{V?r2@rooD9yd( z=TAV<3epz;hyMlp(c3G58-%04p@Biy$bsIjIB%%ER(4)4I_t=ILe>Q&_;1mBi~O&7 zl0-~Mp9xLaLmRcvdK9M{0N59_;6L!sN8FJHgKO2E{9*g}sD(Yr z*yZ6-`@{Ln$wP7+&*#P+3I0n1U!af!{zsiGs{lmu!TT+SI%w0J>?D~2rZOUx*b}7? zpAO|R#|zGSxyYa6C;r=N{QpA#pLhXJ!2i_$dAAmR)m8dcf91DB)qg)y{bgU76@d$p ze+&e597O&#(tf>Sr6BNMMFJ%8f09^Z{_if=57j!T{4?6wd$RxV;@HFm#6M@IZ_LfH z`4?q>2#N2d;y){FcEF3@n4e?kuZt&7Esaf~{~7M;MIx2(ziy)(QbX1F-{Sx1{MiBw z{r`mOe~uIiqXlWx|7t~)KbMiySEJP^@N;=`Y=M8U|H;YUT!v=we+%#@?!b2P2m6yw z?hfsHxxyhTg32m@xB!mK>98U|Cp`^6Z1t%Y6%7R8vU&lpvahjj%#{4!2fvf{md1`` zzS*hs$S+|vbtPoWr_BM|bVpm}|IVGJ;{VTi;Wn z#^N8^2B;jeIuLvPw~$Vt^Wfy!@US{Fi_d{;ewM@$s>>~m4d+g<`!UE z_?&(&1bX3l^&_NaKDhwK|J>U6pWA`u$Pj^NK5fqkBpd$&$ruX_I3`;r*Hb>VWe6Fj z_p^eC$^2FR z|3oVP_@9M7njdon%mql60MH11ptV3H1wi?qC4jgJD1QrZ0UrDp;ko+A_1L0rAr0b^fm{Yxf-Tywlt9fV{8{r?{>^GM>3ZIa{9ly-JvRSW!dJ`D`}GL^NBr~u z4cGrTR{OWX$`>7_TjgNRcgOQiC)<0K8x;S6|ABuDFF-gp@8cSDma4tAj-ih3k&d2Y zhlb{c$Ines`G1eo(G~6 zW`7#~muu7fuVA0_B#^(Qv1I`6074*NT>zLL{%`aBh8NJrx&yJ^ut>6SEhtv~s6UKm zqhLMqSq|7xdN)>o;@<;%?ZOxQ|E;+X&y@dRr+kY4t%i@0XA;uq6id7S16PYqb4+^* zI!t6UP;RXM(FnZpKT81cHvDg9=xbEZcrs!66vV~Sln(zliP!i))2_L<+{^q9d3iv0|E@FNS>C>Ji30L*}9Es{%JYP9q#5^c=Ox16`fO0-P z;>$q(NAlUjc{u|AN1WVYCyT-Pys3@EL10CIYvWTHDPDuw7(Rl!030zs>dR(^oy;}( zzx;&!PZbo_?&1F_<^OV11D^!L_h93(`M))Iy&Qc|kABcy`prP~52N)zPS$@vT>VK; z`7QomN8YJ|Lsid?T2~{i)Y#xp~0DKb9i*3cc8OWqvpTV|Hq{Mr^Nr^|ArSH z&gE49H@F|kY`h1%7J!;VP7P`uNL;84q|nHwE+2)ZKI3Tpomj& zEM(K$;I*QE`~K8`y~gv8`@S#u|GUor{KVJMtyPNz#xtrmF{C%nH{efLokq|!X~!u5 zag3T%3+!`Mq$#u@ZleFO_&=hZcEGin0>F467D_jog|r2LTdZtO0^6{PT5N z=#U<9BILeWat8?wQ^C)||GFz~L*oCU(!mJeH%%BDiZ;|w+T5A}j5Ni*Jr$oO{IBzf zkDy~1CdTt%TxjnuUOMPKBpR2-nhT#_JJhD8&>%{!OPrh5s;GcCC z26K0tR`{PygoXbL@3&RH?qK)E{7WzG&m!g8GcJJf|I3x=RyCp)!FA&QdCC7p|4(8s zZ2yba=vUR~pDNMM%MmWX*Ts;sKgRzY6a7DuA=wMKssCRi|M$Db|LFhW*afdJLJ-dp zsSGXvVR^%T8lZS$oDx*$2(+*G&fT(en>~C{{^(9#g#AUg6KJfSUA>x$9pwGYh6lks zG{Am>`EWk&fJN*9Z`AwrOKiU&M}XhNISi2eTsMoSzv7~i z?g8?Ts0}$;GhBeAEiQn5nhfIVKN5|8czbFMXAd8pVy0x9a6l_4ju6b(Lz3ptU_L zW2F7!0^kTx@RP_tmF-WDj8gM|d35r^iBs%&b%V11^9!==?_N^=r!`LS|Hi_S?Ef`8 zbMe@T#lz!MeM8j$sk&j6;-6YWN%sF0{;xKO|7-p)+kQ<4?rC2nK-h8J{A1#q+Z zc3&d@>Ig_iKzA7Quv0)(6oHVs0LX-Z|Au-2Y$7lghU{GMGN}Q@`%(iQmx`aa@hoca zNLyq`k8=9Z7kR<|e+T@JX864g6^U@lr319jzd8NjLWe?rt;qBt!)j7!3i36=jv z-f#TRJjqM9F$7RhHGc^FwEkTe9FjrnnN zbd^CKgPWS+HSjzf_3{|_nFn9MjCa8YLA}FxW?-c#*3ou)NeHZy3{SP%LX1zSDOC0a8lN(m$jG?~q zHb<~u%IsKjdyDoSgYZp zz*`D@a@1$^2OMyv(YeNqD=_N2fCc&!wt20RKX zKgyV^veiYvLIcNd*|_PgA}6+G+T=*n8RqkWW=31g3)7U~SC*Dy;{biE@(;0PBLj zi2{dC41|W3xkMp6UII`TK*H>}05(jc@!9qkOv33b4RA-}fB2a(^qBq!u4WkHh1*l6 z{BKJDvRDwu|K%1&^)jBv=w$OUoy}fEy#o^np}{!G9C~ z+;fACf^Gq<^?!u_*#($ifarhpm*LsKTlKy3q5s@g1ZDGIYQ&v!o&H<~PBs#z5Rkz( zaegE;WYn^GuC%p~j;s+l!ugQ@d9_uAf7qUw|BRbG;Zie)h`m~~A@O`Z!V6$iU6vTs z9UyKmT&-nx@D9j9W&rRH*rP|3=Bw z!M2!>ht3=sT{&~|!u*-b$4_hIALY-&I%R*BHS>3Q4`u$Q z`QNI5ZY;_Yz_l|otH(}EkBs$I+bO2h5tQ135;Z?rK&g3BTZ#HVM;wPDpCtdTM&8-T zCjxw_#BV(RV(6U@g7cC0K`o#X&iAUtijzkI1npN`53Uv$K=Y(|DJu(K5dsuDeoUAcc|POlXMc%G_9 z$HL%L7#t6SsUSGYQ5$j$L$M&32(fS|2uX|Jk{A5{t>OJcZ}g1x|NIAMQ@oEF`PeN?PQ~1XP5dNp4(E*VF@zY^`259_0L$Wh&1^OR~Grw1F{j$A$D8FO;|L+$5 zA%Elld({7Ljn>NimJ6>rvIW4)Zt$iq0pJ3_`6T~B{@-ub{7jTTR{TFH{z(S$1(JWA z;A|#XE5z{6|B)5Xga2^{Z~@K*@@MfmH0A&3r0eu%Gkw`OPL6V31olYc$$r%%>@7^L zW+8WC_-Di1N|0L&WP2DJsHop8o=+aG6w+zd*s&Ri_DjD6{G~ysNO$7&+stG> z!vI3-Kz=%sk5|&KJv0DvqxwE4bI8eXG^#hw@6KfoD&*5i1IVWbvqo1g+m+39X%7v% zFge;tIc7JF3wxa`H^O?=xUimS;i{+MfBq$)|9`C#oqoap&D@8_jsL&W8c_fL{WgjJ z3;!Q&{B5T3*AoqdK>xe1^8I@7ZpmF3oj7%BezJFHq|rOkd1$P&_egusK&3HIuAewG zxODW`rN!CnD~s2rXNh$a@uuMC#?sz}fAa$FtgevwaBGoWz!q*y&s{iiW^R14zt-pq zTf1eiWZ5yf!R`q`xvkt_F=Zm}%y@;fMej!0zgr1ju7=pn3e4}rtC!&adPu4K4=Y7B zqGew#Xdgr;1$ZDTLfRdE!0F@W0t@74odor1Y^QzkCYs9x>5<#p43jm8OpeT7i}OqI zpC~F7ynHraaeerIv|PdwNIta&V{idj4q#CLSBOeMoxXta@ksF)LQWb6%waegHv&E) ziQp&$nqz3E?MxI7g=mdfC}DeosN*TDJ&3Re&frIsIl|g~ejH-lt_zuyPx;fR=0_wx z)4*D#8j=)5k~U>^y{M!|*DD?gLmZzM{Qpoa|JDXPJN~~?3O;PK_Q(7$m4D0&Q2v)w z_5vfGkEBPv0BgiO^xFW&a21zGf&ejXRnY^wP%ia9;s1&nAPrY{s4O3pg}^x?pVFuEM(8B-h9U$?4kU@Tc1> zOmM2GFY$lgT6*-3JOG}H1^){~z%68&m!lNfKA_%KpCn?2q$az5GR^+@E*% znEs~){lBw6!{#iMWu{i=xl(Yc5U>P56(F(z-VQ_sM1-Hc0KP{$a1c-q2zP+I08)cL z(4D`4f7S~K{saG);D0iIvvKiHss9-TbNT$GQuxKp@YhS@ul2Rh=kw>EZX1qg^N>fx+q>O>57h+mZhErgi3I%zYzso8#?5`PBS{oHz~`^}#9`ssy*(C`H&4|6Gj! z&%EIOC+7ch@M>%HUR(5|9^wB#PBi}GOyh5-8-E;a`}JV;hmG*VvUm3AvHAJ4rv^uk zbq}35G<2+YV7&X#k$UHFwVj0Dwdv_=XBTg+on4=uXSd&trKOt!|4Y=sOyc2tuCcUy zdvWPDwU5LbT3Vl(zjA73acXL)(a{@KdrLJ^st=aix{Fn^Dmv>u-KFY+m%kEu_bLIo z*dKR9pR|`gXcS+sh4)$!kU;)E@&Bu`iIr5sXSZKaA0ZI1U3LBbqO1Zy-NgT;6Jmb^ z+Y#Wmqdrq@AGu?X@V{08BJ-E$u~0v26NOwM)8b?^c{f+!X<`21QVCyxh8crxv4Hi0 zE-&oz!@3_DD#ONQKU5P=rU5pW#psNRepph97PMxU0oiB(%q~{MHrs4K0JJ|4-BpA1 zK&npxf5QLCR7qo(%kcv6Ys74&BPinO92C=rzPtH8twDt6!YalK{{PnS{-HN|M*J_k zP87p;>*YS-f7IsiypX)XyzT-loBW3A0zmssM?f0!sf@R)WEeVs+ysuTExB4tPGaEH z|5pO%Xpql}0QlPEf1=r-GK1s5xN`V?@DFLj`CQW8k~4A^K3C&f)G`1wBu{523@;u5 zw7j4B;eT-`Qv9#*FZW^?bE=qM@{#qXIpKfx0&ISc#Q9D0Uv1%286XeS^i3Gdg{*Nf zQhga&O282S>6QQG?hL>MNFJM!i4n%<;kgh`fsynxQuO}-zv_R`Q^Nd#@;@#B@5NxA zfwxxvm;0jsf&cjl!vE|fuKd4;>i_V*wXE}LTN&vK%AE6+;DR4q%}FLNE&!^23i}`g z!UZ7IZ-y@b`QsP7?Tbu^v|p6}Y#bmvf06Tx?8gf(v`X+Fwa=lZFU{0CH8uPzDp74_*L; zF~>D0*RUUdf$;?DG1M}64RVPZ^v-GQS(rniPJs#afqWC>5L%Bz@y7Og3f05m61`W3 zPq;mybQ%pseLk`=oU)fk8^>&>CP8~SCA)_sZHzW=&9KB|{QV}=;uNUaFfPQAC)z;$ z&y6Ro|HBXT1^;hJ0er%K_QFrOziT{ra|NqBy<4==~e;cg+xDkC?_SR0G zTv}P19T`8}J3Ku&Iz4dYXm9^Wd(TL{b8dL_^6bpU`IXxj&fSD{$@$%@n*Z3xf&3Si zZqF~uab@++h4Z&BUtFJGx_WwM`S|hSMps`{8)$7CsCD$$I{PY(-b#CK_t4>b$DPo9 zry6|G8U5m5`RBdmpY*hT*;)Fat%$tsHq{l1{)e^jN-1OqtB)>G?j9`QjQ*AnN2#*S^%LGck!C2i&Z9$#;y3NAb#UI7P(RS#58FZlnV zSpKaIcozK6pREhB>rMDZwb+|;T3$kJBJdAtS_&{ckC6*Z@E@d4m_KP)Cj-o4{ufO< zkesVY@~?!o6dgC1htEE8CeTt_mG zqyXr{0LG~31;rW3ih!%}f2L(T1isx8{@0ZSZY@p$W{+x9sqY}XttHR|M{WtlPhSDn}eK-iBL@TR5% zyhFfWWBz0ZQ}Bm0VIuzm{~G)!@{i{K0l^%yE0oC0zUtW!lh@wBGfp&!UMyobGM2;nQJL*^E`R}Pnb zIskYx<9UuUMu%|~r`1dg?}eH7t?d>%#VCi17ocLEn)PJq;de+pz&d4S#-xnkf7Eh- z-NsorK?3y9zjqJ%xxC>2C+2_lkh)6#FY$l$|H}Wrga0QR|9PhIe`nhNe6;OQ)w-&s|;^pPKF)o;y4?e|Y3X-_Uqx??ijo!tlh^`T1KH&)&Xt;pW2Pt%U_J zc&}Ca-(1}w=0Cr9YjOG3xpTKJuHCtE>E6bbyX%*4uARHKumJxbu6OpA>KqTZ_Y8OS z4Idmj(l^pOGBegY^jX#aMOXB11Fio)RQcVJ%D?tkf7Vm}w4+27R&st(0ex7D(Em^; z=LhxDfa_WcfVu!#s%}D$Ff)W{xxk3S_T6hXzj2hYn|0P6)52nwT-YI(9=!NzN?v~dRxx0VJTT z2-0c&)_B~b0e><{n*Y;NCH*^&Og>FFRQ@E7t)1+HM!hC8@oRiv+LHUgQLN;J<3T_! z@I!wezR3Um?(#oBT>fmWMY0_>>66_#C!_q2p2zr~MwIZs2KM1n9DxM>lZMFtZH<6s z0#yMS|5pW!g}~GN&uO<}JfUp0H~>yR0(gLyqXguehIc^QJc2F!-^Dx{EjKU*v*}?l z&y(hV1^D}10Ne+Q{KxTsaXmKqpYc3Q+HJ2E+jU&Ggh1HxnTMOhE?@a>`Y!G!-+f0g2YSsCF%Sq;&t{C)6$;hlQ< zQ{w+_f%^a1eS3@l`|<_WKCUSL!|jr^n+-1IB$*emN7%zc0Gv-o0E!@t24>{H?grQL zqWrn2k$-{zswDh^_j7p%wa-`cV}$*20mQ@w==Pzua_dq{t(cX ze<3U?@qdj~0FlbyXPqLd5eW^(zal=B*gmy@9<%@6};9!l!Kw6!=2#nHElIDCZm1^G{tdKE{5@yCGb!5qt3h ztj$p*c^NUr^X}zLODkr1I(m$+619(5RgDJy!2wAWBKUu+^#%Xuw_yAepFVp0AO1&G za<3ea|NCA|>VN)rsP^Zn#{Zpe{1^N`)%ZVy)n9Z*->-QW&rF}aaCK$k=={Lg;_$@6 z$mr>T!&BXdCL3KVV-web-pl9jUcGR8Y55M(@8!K}>~zf9$J*Mh^=r59uHSz7*6mmC z+fu%8|6Aqoy^1UWs0$$ZwtzQbb^_@J z-jTzfFIgQR8oz@VkcR(l5&k5epD27}23st6@R0!;{LfYk{y(Wb`VZ2Uil z|6<5q-YcO`_^UJLu>UVolO+BRc?#7Z$k3T!f6_?c-<$#)W}UQ)BDt~nKMV|zhzo$x zfJssTa{+0V0Ak*5%HU=20VQ_z9aM57r%YwCA6Oc0F^UGZP5;xD$pO+_sQfQU0o@AK zfRH_aMEId@H4z1&N75sWq~Atbe4bnIBnWcol%;{@gbz$w5rxNe$%C%A~_$5EIk z@?Q*Ad~e2aZq!O&EscMAa_EDJz7Hl2eK3BA2427?$A`X{9cCE1py!SM;n;y(j)1oO z`&4YUf1Ld5l32PL5qPs0%Qf(h4qt@&q{rs6p?&67xDisp2B1CKkW#gXwd{lin;eG`rD)rraVrKP)<*IvGH>CVdP zo~`z8YX4go&fmOt`OdxdJFnclbLYmL+v|6)U%ks(z?swQV?JWM=q4FP(RQ__b{7aCY|w(L`o#jlRSw0;uFUkLv`MI3ux0pZr ze|{FUIl}+45+U(_c{W2k(f<_U`2UmfKl!uCeqpqe)1<+t>2@;s=^MM;?osY&`XA;0 zQXKy$AYT&c{u*mrL2!|jUiAXV2*43AyQ+0QiTNY=M=2zNe+vF{RV#vj$@ZPiJ8uqk zeziFE@$n&KKSKW4n3}8t9AZ)6y|II+gPtoDkcrs)aykCx1GmJ$L2e*C39~mFNslzt zO=uCAmn2*%s?(X3(}dy8O-POs!`C@urM>3QLil9SY7D=no>kMk8|PanKB4*q=B<&O z#(dtSdiss<(??znD{9c*+H!1;q!F7A+Z=!zro1B(h;QsNBp5@7@_2rd&m)GuHzkGY?k@_r1(lLH0+*+)pcof2z9wTz7v_bAMj-eqQ%}(FlIk z9sRzq^ylH~UyjxPbGrS%W;*}xbjM#$v{QfSU;8WHhyUx*E3MH6`k#7}|Ai03535BI z0a{veBm$&e0KgT_B$$j8XG=v;Sq^wo!iV;EaScrQPi}9L)qA*Dqe>ejH<*27mkX<_ zJHS^&ZZgAU@|gc+;C}^LNFMqH zYawABDaU3*rH2jFLR0f64QduI#D1#&x^kh`!_ z0MFBt7^ihfJ&<9F?s6}qeYr|~6XAJzNXhNx2!}H*ml*$ZAeL9pYT5o za{-uvU!A-%kFAr);s|RBOrrcRuOKCrl>asUFS!zU4Lm9j!@J`GknF&YEpOG!=>Ji| zNdE65>wgTtjkF(yx1S#PA2dVyNf@)`^g&k{|MS+LH&XoHbOGRb(g8%JU;wKkbhB9eIb9+d*2&9`1RuWM@RdW{QF|^H~#0Ss{lVd``oF3jQ@$` zL$2*i2O$&Esa^mY zd-)&H4DaJ{SXAGnII$o{1MjBZ!M9C#K7P(7tK$n}X=qQ{loYVGmLQ9FAxX9)p&_5z zZs1Dw43k1+&31B{>XEQNrxXgn1^7nzpI@Xe_}>-+9zFgK|6h&#J0+F?!gp%X54wxL z8G`@Y$o~aICH@cpcSPT>691n)e{p?z{P@Dq#KPFr{Mf|o;gRFreUlvrS0<-6R+sNx zUAuSf((PsCf2bQn2xN^!4rDaGvLb5)n0=Z)CfXs!*j0SGx*{W!QyTQdr_GOSl4YjV zoAT-QyYQ*>>r|WbCsWx+_s^hrA(w$No}2HoBH0bnLz)8v^zkV2dGp-aJ2$V~S-*Ve z(%DxoEWNrg{rZWKw?=y3?QQ>Fqx4DH{i0NW|9@F^e_nHcT=)K=E%=9aQhiIm?koSE zZI{REe>>6k-?N?nJ>UIb)17~V|3};YZJ_#t?h@sINYK7n^xr4{H;(^HeE3o^_@r9A zQ;PN-$R;iT8*erIzzS@V6b!Mm3ILq53Se5G7~{3(=fiygQwQCjBb*NVTbp^};D0DQ z3_=TIKRo!KWCXal0Bv3XZ;!N=ry3O^|FUba06a9V4L_7Qi(9ZCPe+V>kTKE?xVyCV z?#BGd5t$9MF$pQj7!CV~2IFbtfRMRd&ih5jLy}a^dzFG;^8&t4+yrw#k^}_11*gdP zDfo#ip)!7P9JmVwf9P-b#3XZBy$QSk9EW6)!el?mRh{~>mjBBxU=LjneWCyVj_UvA zCmZvBK-lWI?-6M?{wL;-3t%OEXfz2Q&XhLM{wDU{*N~M05&zQ(|Fa3N;53ZdMf^W2 zu#H|9LKeV|*_F!s{cvF^LHfYN2Kc=ul@Er?P+(sZ^HkdHcFQNyQ$ES+;%lwfJ(3!xU)`v zKAT_4xMy3ul@=FU&bTX%x6DqLu0z=WN;!IGxcjT6u`lOF(D}!a{{;J!#`vM83TT8| zJU=QR_#gd0l78Gq{tH+*ov6Eo-_yZ%+Ug+aQRQlII{|!L0QSR@Fn$XQSg{T#iQaFS zic?9jz2E3GORh@aSMa_WqEC~&vArFWegk+}|58H;hc!>Bi?y>s_3=j<0bq5^jQ>8i ztjYf=HCa%j$I<_2B>#R>@&8h@2Idw?%$_=ZEdIY1`L|2B0O32;=m%Y;-wam&INtW3 zr=$eXpC;RWGgL$W|0Vo?V*32r^`)_6^FyP{MKw1aCu^if}eLUpL^x{ zrQ1s@w`b;WEiB#AG+2?uV1nXO66YrUl*ECV=NE5M9>+d~pYa7GXI4k@sSdlvfH^Ip z!>4CgZZF3DQdEc87%v5O(Az`cT5pFo*K#i@xw&{#=cK`1?l3c@_;m&@zk^Ym)Mh}j zFWJh>ptIp4$jHSzBr@AmHScn3b%i-^E-z8clLzK$d4-KR@}24Aqvego<(ucvZmgYO zzkGS)#*K~Jw>RE+ZT;Q%Hr{>j=Buw>y?Xt^$y4XLJFm3YZ+Fx_YLvdJ1z%UaUsejg ztGj>djs7?+J1_j#Y{&nb@A~ig&cB^%|KoV$*8|nByGoR1dbI@q2k+Iw%T500m+d9l zKPddFUZM^EC*^<$`-7v9Fmwvgg8kG8()fIK3%YOe|1N~T%STP^kOluCXA5+LxP-M) z{!{RCyYfFB`L}iQh{Ht3af6wk_6ugBRSHeNw{)t-4*hW@?NQFxB!t88=bBA0&c)nb_o+Q z6*(6roCU|lS)iR9Z4zd{mC$QFyOtw9MHy57LXWilk{BcXe9k-K`)`+n%TVI3_JaTa z4)~wozwv*5pZraPr3v60|Fa69*sr1fo%kPjz)Z-$$^WA%FNEXa|N3^)-c7 z0@tJfgWMX;77T`XL1i#l8D2hu$+=3`3nbX6;0-FvY|76Pif0}!;c|`t3)OSVRe%Ht zgbIi_Jg2DZnbuIBQ#Ip%aY>Z_Gk~{!#3>Dpu8Xfh%}?_S>~{7T68u!7(;m1tD_(=8 z0Enn5&Hre1w7e(NGKMzKZ=MSyFU_d#eT6(rY<#QU2U)RNffuK=AYZ`O)u9N!b5G1pi~(BL9gC@LZ~Z;D74>3_6Yk z(lx4Y;eAVb-TlZ&1|%y5wmLwM{kP>`T`SqLoa)im>~LM_;g}dVO)TPnW$M)Wt#ivU z)R7#wSis)am@X{Z5rrX60WJWiwAB}|h8)=@Pdhi;^fXO2(My|4i7oBPi3`wz{-6D$ z9~$p^&|gC?RQ-=n{Qm|2(|McDirqXiq;SfEGt4+288+rE_<$Tm-Fe&rkwX zSQ>=pD3lIh!`^e7{d`2FAcoN(U-*_1yRiDoinR4A8Uo=;=@MqAqR!lceG0nU49s~K z8eit4#XGYLca~Pzao`pYV=!+s=;V@>EX2N`6^e9Pw&jxf7X;>)mTxloEQR`)A$i)y z_BX-%xy6myg^l@T88b^;PHE83v)!0kyfw3Mn@<;4ZcQ)VIx~N3Zt2!MH(Fj_KD)kp zj$Gh7SFYUNSif;|p|5tTuVEgYt@(UTkKdhIQLvaE2k@G9X zKeLnoQU;cNforZ$aNafrNb~{|niChese?;_4Dwr|gf` z`PokU+k5gk5eUKL@IMz0mP*V3X2aMxGQGOH8R2nwyIK(H#sF#FOB!Igkh%24r>>88 zz~vHz55#3Kpl782Sq^>T|F{4|;~nLH<9XHphzbb)x0pXp z!H$uC>C^~(GJxb?uo+MX{!zBWsBmwhmS+h7Nj!eZZxSg~SENNvS9!#-co=u-hRs`&$zMMR>ZgVCQ9piue4tsZ9AW*_L-M+Xa zLhlR%=^4Yf!I7!tG4u;j{+FEJg#Y<^!7$^0Ml!x9EA!*&OlbW7lQ!yq?!f=)`1uxo zHkMB4n$x6hBh#nW-ZEzUfZcnN%acYjKHWomM@tX30ha%}3;!e0hx0$58~O32<3CuP z_-yv@hbQ|!n(qJT%)mz{`@eUz@8c7LsC~Ye8QCQNMD3pd|Mtf7rvhRJ1WEvn74q;o zoUB@R0dnK*gv_l+kJ+dtTg>E2nH)!Jlu^)`WpkBmu9|V!UZF-Ko8y=m=|a{iW7KGf zH7~bL8^ioen^-Fge`?l~ZYMo7(pR-+ob*RrVyBEr53^1?@+36u$oNXui61ZP$f@+m zk+;S#AbI2|`JWxA;h!)A|2ums)W74)J{JF9ihRHl{-?;~_d81nPKf{i^?2iNr-c81 zGf@3$NAy|Qy>Rs8^4hhf@u`KO;ngGKi$kMl4jn$$**D(kUKp9YhWcgg+|3K;HwcCU z&usn+S{vkR!`d9p1pd_w)NODwKK-fTI>W4y9>Z1EDaS=Ic%Du(IytqY^vo^X0N1sX z#`+w4@-A9~82$}T;rwP}eO3o(=gqmL^|>X9^F#hJMox9`f;QxoS7v@W&N9`)`t02L z%nu>XHItajkI-5^$$&tPR^ZJoL#+q=JM@h7w%3hUiq}Q z_8)x_-}!$^@ukLJPBs2GR{z(&%9ou*QZrsH2kS-u?W)!Pxo;12a{+$ZR-O+0|HY=j zTKu1wKjC!r=K|6}7-~5nqV(`TxNb*lBp$(*)1+VcU$Nh50{`Ym+GD|g!r+AcQQ&Y# z)JK3c%76D^%760(2>ugzH&H!qiwa{*HWT7dAg5{f0^;)$LXVF^_+}c|56}*^`C`Q9 z8AF_(*B4XFu87wlW5Y#C$_7?BJ2D?EkYR|j zyvTlB2ASMO!ptJ_QD+1o2iKY3RvBpUBz%O?#_&*j)I$+;*GKABXy-{7Y2$e$@osc} z_C=yCSt)CTnb#Q)zU{@+&KQU3!oKZvDs2L$-@_hWy;*#EPQ9iMr7 zC;X58e+T_f^LN*(?_ogxr>BO{?V#%aiANu&%;D?h`_>sik%{<@jz`vy@ zQ31_}3W(f+-G5bn3!h$MgU(VkA9zEKLrTcFn^&Vz;9rPZo+GDIo_E~yPI&IT?=JZH zS(jp+ayHi(AxLW$slwl*?i4 z1xzhdDQB8vcVkX--ML(sHW-%l&`H}aY#yCHM=h7@&pQK-GbFcmSSw*9IZAwQqyRik z{Qq8Sgp^&lH}iu3A20rY8U8N@w@Pe76S3RWuLr8g&V~O^=>A_rm7g@i&yD}jU7H`D zni(2i8XlWDGIFwS=y=DW$+oWf(TS@I^S9Q{-MMg)IQhoRLX6g6w(@qurfCglq zAiCkb^x#LB<3OW1GQONsC48Gxj>#!EQnm-F;&nY`)kA!Wxy9?V3pZvLIMPqUY~30&^Xq5kdD^SzRxZxW zp6KoyD%WwEjvefu9U7S*nOc}QzBGSsYIWnigN;9Qder>+>xl;XtG}FV{9(N9SACT) zI!o~X%hdlb`fu*U|NL^p1z0bIKWQrux*p|#*cw;`KRHbVD1p=Me&DmmsAd5ey+4rl z%{$mu6m$#or%zd2u;4$UdQ|ba05}35eY)oV_LzUtgZPJ)0x*84sl!M4FB!YKRTk0v zIL@!|Z~Wg-_?IKJ?*&NoF(dr#M_nlpr@)K><`=|XKurEZ<9rnJf0OxRn2(nLOr^7V z6qw&K@{v!W{VgU^qP(}s|5j4F`ImJ2pRgDDpKr|{`NsL5|FHOO6#u6J^W{=V{m+>H z$)y#l6a+Q+M0rvqPAGM00QDTKj!_lU8b7wdr^sdqefMVNuW%24qtX8>26F_k zEzyzDjBrpz9;|_2a z$RF7}N5b{QLYahqW|j#BycNH7a^|GRcwYL|1>gyRf6OjXR+oG6c;ZAz+OOz;@H^;~ z2X4v2|GY1$#}=Xo?3tDx2Jl{t8OY_>8Wk4+nxe#_J&8Rw-<8Gx|9|%0{I{{|O4RKe zmqZ@Uhom@&B1MVfB#9y^YT}@|q$(9^MosVl+pvwa;RFd{!-fqTF&rca+(-~42+}{K zaS_~yM&mUajYgx<4-bv|x4mzzMINz6t74b!cU!>1&OZC>v(G-PRQX%uW{|*s925K+ zTEf&yFliMg<9}IScFULY`TBGD`OIhY|KrSTBi}9FpZ)Dm+rN!d?^O9e`DpzA=wGdi z;n0nw`$>24!?mhL^^Yp~jPv|i4F2Qut#464`^}%Ml=g!`ybO>33;8XNpaKvAL@4{a z={Se_y_Jr?ZIZH1;;r=5d)dspDff2Nxf1bTkNI!8{u{3EB<8ym_3ej!TLIr@z_$^c zTnkKg0+XvM?Z9L+FxeW(qF)XLtNCSQ#hGlVJkqH;lbj$>oSc~VO)z1}nXLO&98L|J zta5f3iwtF=NYb&CDw?45&{$1T8I*Cd5`2=LK(2vh6Og<>p9}@k7OajaE%lBp^}w5B z84zBwgN-wX3}u^{n4rqmOC&YFIu+-4`>X%3P#j)Qy8J^b_WyJKf4=yi7-7bM<$dLU zP?IyK{-ZJdUz@rAy`KAjnz{e0p8m~@3;$nP?;c#bv(xDGYmMD{b8E4&R;hOi3oE(& zPGjZv;r@fGdj8kJ#e3T@Hq3lcJ#} z-#a*@vM9Hh_UJ9J97>mIVu2sF!_Gy|GEe8e0=&+&`SzAWTonTF@=_HxgNU0z2Jaqd zXLoNnBB;NA+K26JCas8Q0osvEv;YyEBb&r=z|V4W>E_=4)xGV4UIq>ORJQ7R07qijh<+ z#{cC+9Ad}#f0oF9v;Z`Ps1uT~ALV~bu$5B(F&ARTPL8487#@SaF}zs&H46aj_iXz{ z{x);GWt>ZEM;(h7iexo{NXdT;{>P2~Pa%Kv1&F8M%)mdcDG=u)_Wv;zfIbt1|NY^i z{r{Z*zc(R3-z@+0mzV!$i3;G#5dg2H+yzQxjqLwGKK6VN8Vc1G<1w=fiqNs#UVtC7TUlEe*zEjJtBD~!V2r@A2LC(`Q8U71AY3fukh9Xb%vV+iq#snV0I~namH$C;-is-o$=*^J|B>OPZj5&s{2Q4A|E$a_WPj>43hFcpFr>riHiD1AifwmQo-Z@3Hen;ADI+oXDt}~;nVH^kMi>cpQC1f=Oy1r z@B+{mU%-<_9vQbwo9Xq>((zs~KH2s#zM3~;gQKS|O!_bQ9{m=r|G(e_Kc5+$|Ml2^ zn6S*hR?PhJuxUpAM~nG9PW8_{=pmE19?z3myh1 z2Z6~!(AN!4?uLEa5nn&->xF&Yu&)#FwH#?zAC`8&XTq(9oD?=$ci?}SY|?amNP{-u zTM5WH!uY&6G7O>5$dcn*Qv{ep^-(I0&gp5w96ccCi;a#I<9q=VD@kOz z2^{0Arv{Ho73mqlr{#%8xeO=>CQ_kPvzw)UDJPNnsB}cu5AMzjEcfTW@TXg#R1)*`1Y@Tl@R>uU;ku2uPM_`N8h}?LEL-qGh%Zl)%vo zMgcP@2xte`;Te%go5Q<&_f`xzHuoxQsQf_9jo)_bh_bb18v?1_bc zuy+T6^O?zrcn9|H@Cfhf@80R}F@9&TFO#?T?(7_J>kdu<9_jDNG{#YgE+606-QQfO zbMkL1U+T=&*DFii+VY^XaaYIS8o^h&e_PJ}eL3}MKKZv9 z_Z9fxjl7+rUh7l%Uy0)FDLE7%qUVB{3V_+45E`;^i2?%4{gV#n`ez&epGy}o<3GxU z)x)W3@DEh~%$olXl>mkRp=cgt1%&t?HjD=WIERWMc9?%U6tid_?GK}Ua2a6y_rQLf z`Mo}OQ<@jx+5Dvw3XhmyG6+;;nuYza842upCiz?B&oF4``Bi{N_}_GabN>IU4)#s63e0Pt_;c|rKt%t;oIcGv-NCE8zFpBVgMegys}K%EYZ z3W3AJh?2ZiY45jwn@m6^by ztJwA#kuwDp)Hw@DG$1|xfY+hsunlya!lVdK@rw~J#0c<@+QK6G8_Fj$8LGGX|Hl6~ zV$A1aN*4R%N3h?PnbiOJVK(u!`2TTc9znhl_z0~}<9{9>v6+qW=A;Zztm0jrcZ0!t=mCv=83XiXt!?p%4E<_`pBtE}DYlYmbTP z0*o*RyvNlA+c(60;A`Og^81!l(1OPWM*#c}HTZ^Skl882cmzyl^aNq6VhZZ> zS4>p^9DD-v(yDOFPAD*Sz%CgX9$3IkDRXN4k6L|!WvT!3_4)ri{>Re-o{RslxsmH` z_^um$GZp)|5XayQcK&B)nj>h5MfpoP{mY#D0sP-tKiIo6s5kpn?EfoU^~QR+zM3yJ z^2MD-^Y-rEgUc7GkOM{E?eDopG9N^ZFe8+unzhKxIQMlWf*KPA@@+dY=CX_+0X9%-f16z zCm`p3Z})DGyF2&j^kv#TXy5CD^oUH@XY$?du1Ih1PH*ova|Zi&XuG>y-s#FT2onD|LR)gWm37_tIPW^zSp_ ze=Ljtk6U06*Tkkoww&awO_FG!G&h5!vyfVl)>rI^1(^-E+6fj!PJ$e&Q3(fEQ-0(TLK*Hsk& z<{y&3Vm5VwSc(Uk$pqtn^8y&t@)i~cxw&=f0zger{vW5A*DBQ6Y`FI(0490^z&UD86_t037Y&XE*SnG2>!Qd zD-P0P#QWg#X8x#JwlQ zZyW!=TF6k?NBI9?s(=6C%SY~>^|GZWf)NGO)F`0HcOM1h{rhYQT*t0^IUc{~hVDgt zH^RPaQ9p7$>c1EU-6dTAIOe|?^?O>tdce2l_|}L5Qrb610NAfi0g3h}upeVSmV6Wh ziT{_Fe^C=mUtmqL1Gr=kt}fUfe*q&a4#97}MMoV0o)^FoH37le&*=lS{5?WzAk?43 z$Ryfdd;!Bgv_C?6ij<-+_;4AVQvFB;5Y!)uSo=p$!TB6#JT7&mzQ^|jg8vUYN-atF3%MD#Q$J;KfZ{G`}X$!ZMe6;3-R9G-o1@FFwoBS z&Yhjz+sO6~Eqrb*gY+L@mfzWvyF0s#+|fDG+1b0L6SU8`h%7r2Q8y5PpVx^gepjsS z1KGeW-aFWpo!Fp^?A{QYyzJ`sfPL=Y>hIs~?=gN;rtRHCdb_uJd$)Rw?A_pz-p+}*iuZEu&m(%;&-v9o{c@aS@H(4H$V zO&3}-^PTznMtNzgv^c1=F7^((+m~LQPX0O*{>#GDzc(`fLGVv2%OAua7E&MN+_zIP z95h$lFekNP|9=|)^B0G|3!Dq~(^UNPOaj6u7l42tXdjC=6*Zyjd^8qyf)f|yIDqF; z0Un=>Qh*f+~RsGl2I{w?Nb z2>%hO$&Yp*%>Nkwd(6+>QD2Pw2LH5Tw>No;>D~zc8ExDVAHgvANAv}&ASCTqr~v9& zz`5Y@Kk-X^?C_~`{{P;@{ERCLFNpv73rpbiPp6ahz$5%GAwX6I2n5AaZP+Pru52vS zIx_WZ-!a;!Z+340mB|3@L-4OwdN7+?%x5wL3l$q1t5d*K41sMzVun#VBQF4xd4!eO z11-70mb*WpZ-qs6PT=5P0V8BFfv`vT26>C^#U!txwA| z@B{ca{wDxw==BiYL5%Q>^Ip^tP={L@$YlI4N|4;ugkOCH-Ut0sv;+Me$W5>>lCRKp zSOG5p(>(sS_$#~{C}rj)!9QZ#BRdikX!^p$VKnkaTKHe`e>sTdr3uAj;Qy@r|6J^S zc0J;GXP%M&J@}{d(hnv=7kuxec1cJ-;VkB!@}(izkvFssQ)D9KaBVf zqayu~AMz&=pcn9UH64I{Hz*@A9wa+JVcsuB{*4e>z@jW5ouFs|cmzaQ2>MCv_4t2X z-3mkj5g=rOE`XRGfiHm2IKqCwetl%1Vw?g#2_jl>1o%Y-AZ~{vhOz%^ToA2^ z;Jpq4{B|}O9R|%cKwsu-l#$nRD{c*?7I4Qu7{GaFSm&X6(QepQ#=YPBaXSe^u z|5sv>lNkITeLWR>e>(n$QtF@U87fcy$7=3hm$QGS{!BLdlX&Q8b)&y~(yPG#jqTbB zfh(Q)S}Rv-<_f!w*0ue;TbD21xqJj*gS$8TJ2wUU_iibEgX=d3J7D&$ZGmtGX@T%t zgY8?eI>I;rueg4zuOm8#K^_^6>m!W!S!zd?q9d0=<~Lz@-XiP6?QFmRevkdY=LYi- zyU@E3`}Q^ZV0jll2iJG5_qVSr!(ZEyTi1Gn>)nC&x3BiLuXeTVZC~HohV!p=MXu0p zZC~BmxgxTSpcGu~?OfgLU)da7m0N==o7-2qgRAR3ggAyhaPemNN-{{LjG z{^#R=O+*EVM>!h!y%eX@$hlx<@Xv-L^@Ie~&qQKjKZyWeHwtKE{Ew8PlJkpl(MTpJ zt?8RMW8{C3>Ojt9kQkmN)lCfksnw71A9S}+pHt(%RrpbxKXrbD|1IbDVg4ThzeoPo zIfZ{y1cd*A@G)E`CI=A?f2RFkD*s4Hf0=1OOUkgxDEJ@# zALf+drufEIoqpxt{H6YRT~OaY&YdNE`sMoUq7M6*!-m&3pK{ZYgy6N-X^ z@*&P2`3t;9xGU2v z3TU(b+0HT$_?QHDd z&rJOy9sW~k3jOO}8(Fe2{=(tEbIG4(T~22vdhjR~zMqKlH|5#<&tC-oMsP0JXBlD+ zV{#(BmIERem=nPke_G2!9 zQ{%tp9XNjK|3`kDJdgj0e^20_ZM-=CSNngW!6|v8_@4yvKb+xpnguKsLFGIzjZp_^ zEg5!5{>uLhiaA~5QXp+9eW`3eMCFkIA8ck3F~Y0yzY1dm;Tq>dtUmlNBYJ9p*#B*^ z!nWrKFt35R0O0wWVEOPIVHW4rEP_uTeun>nc~Bg&6ab=hSe7lx9&{Um@hY1CE3ZZi zU;_MK(H)Xw%Okh|)X^Y2WG7zBFTgF9;s>egFTlIte3TBx(M_OwCiCMk$5S+zU>c38 z3GyOHp80&Qhz2IHe^MCWe^fAu$kI=+mD!%|NBLi?8xgvI_4~u@>G{8g{a?ZVW3W9W zf5Y|fy!G?~jR<ucqDYoXpQRvNh?(p_p@8Vqh6>|Z}PywMxn*y!JYmb-oFL)tw$+qe2a_|8qS zx^L4$=`i_qu$&QyeM{~#jXoI9phVy>jy53^gy)&ht?PZ(9DwvUx`Uf)rI${BaJ}EZ z*6qRb*SdonTfDJ6!RjNC6kxeKBKAi* zb{ud%5^W|Eq$g9e$O;@qoDiq2i8di?hHG)RlS(pynn)ZOCe{9*w4lr%r2u#nPz}!c z|NDde-cDX9|NmtAA^w;6e=YT?2LVV%0P*MGD`+aAJDPzj=xYLjYxoM|n5H3fv7C== z>`&l-Wo&pD*scmB6S1RbZ>bg_fiwogFf%h%Jl+PIA$-VJXprv*E!}_A|Kb%iM4v#3n%U8(fjFbxoA%D+{K_)UA8|oSwMv8#*-|9uYpl{0@;+H%WK-=;|2^kh3p> z7r^OAe>vj67x!Ov{a0iDYcc0A?B5Ie_d@<%75oLeA>YnOBcKoV!|>Sup?z=M zvvda2vLvcG?_yMMB|ebPY&CmY)*!qp=SIUN|AkQm6B@R;?% zwRNoP5*`D4Us>C}y0OE!a5$8{A!d06^*$?HSwjX_H~Ng9Z1hi{^d76pt&@$x<*mU9 z0XDo2AYU6?2G_al46ZN^ldlh8^-BP}-nvYGwSO!O51IGj^Glr`#C}mgy??acJ6e;$ zBa?OyhCgccj#j&eo!;SU?;@A0{e!m5KWz67+Fj`*t6K*x`f}@_+1qdT_FCP&raU5Z zn%(_Y|Ki5ZPG_T%EiO#umZnRMnaXmpx>6`_R#tkQot5U^jdbFpWa!Ic@*m2Xe_G6N z^6#HYsh`g#-^!33AH5cf^2c-|9wt)mEBW94rVs_hUn?qt9t@8L78M{73=yS+BLFis z>VTIBAYeZs4?06bi6T7bg;L-9Y% zmkFUT9evSIHL!7R3I9Jy1yJ}8v5l2vg4cz7WG--KF)oY%E)N_Tfyp3=4+uUHPFq+{ zPf-VzWqDBdGtxmuQA5W6)}HgfN7~=d&Wq*$ti=C}+y5!=NvH|@4}2Q`W78h;zk)Gr ztP%Kv@gfTRVhQ*7Ux5}_9nax~ep!V1q~Fq->6T;2DW{> zMBlMJ2+XVSMp;mN1b*Rv2`C!zzwV6{cnj|uKJpO%%WCKptiZBB@s*y%;s|CdMS{{qZUmB+CE88rShN`KMgPu2od z5qRXgp3VON#%(9b`C}=z;4Lth2%)Dy|Dj5!nqw6M*xBsU^nQ4{|`ojzJPE^dlVMEK+=SRz`y@c zbp$4>njr1Ajts(bQW!1(2!ER@qalAc;3qnW^96WTvxbL>!-NJA2vpU4VG@MtQz=M_ zh5Gp$?W3|5O2JalS9Z{sH3~?L{Pf2Igvb}xa62*nd;HJe6b8v4AVvuH0V^yxvd+8; z`a*i*!lLp2*N*?;|7`AL;5`2S?D7A?{|Wav79K=GN72aLcoftGI0-@j$9m?U7c&(9 z`EqXRXX)shZlG6N*;qeXTWGZAs-053JzsCmmK)jG#&mJBzIbtS>tuKLWSfJmx|iF% z6Cz$Z$l%KA;7WUNrL&_^F}QUIr~q8Hx`OVsEf{|D zNG9};S{sM0&BJE*P{zB5D_a*=x)+;JefOXx?f%Nf!Af_(xwYTiJZNm}H@bT&yDOV} zD_m~wOJ{v=MLN5U_1&e-y~fra4{mIC8=Z2xP)g1$X6Kg+<)zumGKE0Pjg3}+xw(5a zo&0Gc#1RaCET{jWlKC^rS1I*jKJj|mB|GC%G_oBbOe=DB{^uV$RDj#@*l)6lP9#hs zfF2F(BN1RFnWV-hxxfLR04=*PYuW4l;WwsAOowArB-M^3 z{~Fi}`D?8HspVfU4gReq7D(cA#w_*{wNbVi%E%GqJz*eiTJ~G&Vm!`!Ss#&Dqfz9)bv*c|b8i3tZ^-{Y%_bV8!u;R@z)9s#E%h(VY#~6L zu1owmj174ze2!b-hpcY+%3X*TgEo{t;$97v9|5!QznR&U@Iie{ocx z_&?`@Ic2|7@;L(k`gRl|&RmoDAGQCZ+UTE!FRd%EPuZRpS;{N_6Od{4|7Y>PS<%(- z?$Pzb7|+ox&lWSOa2hMc2KW zh~JKfFNXccTIA=0qyzlF>^dhA=Qtwmm8iC80b%DbBw7GG56O2!PB-KS{_n?~+pcpf zF0nutLH!_X?_DhQemU?oGfj*y-v;KO|&>jVFupb=yyB>w-ERO05>ex3M!r(d78kR{)#}pz+7=-|gt#29c8{AIms^{c+MAbFH;-3ow@$$FPWMFmoAmY4 z2Dca?oJP34vw3-K>vCu7@+z&&m%a$lzDY+Plo7Gm%TgQ1t6P`aTSu++BPO(2YKxKM z_SVG~Fu!HODN1h`s#LTV`r_m zR%!0(iSWVB?_zt$s1925 zA}|~G0WYGX7I;R3#M=y5vD=EtYiF-p&tHUQhPXvORSED7zudWd^+W4dZ{$C&J2$dR7`MnD9D&XXAgXzeH#gUx!D=ekJ4o zx}OsV&?Cg_aaR6+%<})7{C`G(|C-ZI`QNtZ!M}Ax1u#(`huZ(2%Kyg_R}lGlv-as$ z{gmg;Vn4?1a!W5i)DD*)u9h+8f6yv3LZ7=H{||4q%O9=EEhaEiCuqC6@KLAotE2Y) z=_H{?XLOW5&hyVTa@mbx_J7q4Tv0RoE`%R(?zztGm~$LrWi8xnP1vm+d z7hqfkprj8afZj;@AH@B4UFU9GW@6{Rq;f1e#ZXWO;Qx~-=>vlCw3j0OqloZ7DuXHu z;t`MtBGC~f3mE?26Vq4`RW!h2>7?usOd}pD8tn{4Z3NXBMiN*>WaNF2D}hyw$(BH8^ap zA2!z@ZNcgG#!*|S`?$4!+#2?en!x!wax6Lm3@^8iQ5IV3ms;oxOtTTVedB0l{iwNq z)LJ`gGA?X?+};qp7sB5F>YZMFwO4P`VMIFBc6Vi?+vzsTjk)AZF+RPJDpaziat@hql^X5kLA~BRjz`~&hklxi z|FSss+hQ8|d^Y*xT;hHzb`?WPB(fF`UHU8dU%dbvko`p_k#GW{0`O0_<^ofzA9^Pk z#%hE+Ms)7(Hf zrnlG+Dgdbg&8b8!5jWeusTe~S0PjUT887~M&j0@{`Tt$qbNv5T&;JGZK}}Qu0?ttZ zNCY7GU+T|j$x07XfnMNNDP7IcimxMbmJucx|3kb6*mTeZU}ocgCIin)SieW%F;FqU zZ%EG|hJ6bw;t|BN|ML`gP3SY<3h%J^vbqLvSP(n||F2-A3q`LeMQZwvP8 z;vWA)>^xQDA1Hf^#sbl28XsblO$DIOYJ4m|3m8wwOY#NyL3DXae+rbEvib$=*D(HP z5#xW;n5sU9)u4I^-|~Mw{vWC^sz9+%_K&B7#o>Q`8H@iR{$K3>Qvc^sBl#l;&E>=7 zZ5)Vsh_}p0(%JZ2l#JB1^#IfgMae^cq)MA0yw>ZMEk#<47{3f?vES+fIWEu zClUV@ld%6J?7tkA%c}$kkqaCYq?ZX%Ka)`wjx;(5WdSz?1w}bl81w!LM*g6W5p)EL z4x+ULWVC=T67r)UVDn$(uweh>s%Q=VmLpY0Q2|Wgedmer+`5( z7l4k~{8d4q!?QAtgULpMI*^;3fd3y%NuiSG{pG#+#IZa`#O@|y3FZHk+Q9$Y!vERr z|NL(J+ePy4S}3#^iBJgiUdp8e(65S9f0$4IVJ`jaqQsxPos8Ux20F#XW@Eomtk-g- z#eBJuFE4?w>3nHwx=}0-8tZ$V?tZ5yn7zD8yWi*>NV|s3oX&xEE;cs~S320vv5p=7h_(T@Z(hVE-;g;}96D^sv->Nu&Vf##f3UJDRIjh#E%L5qy%anAs+i@M z*D=c@docWB2iaZfV2Z~$zq5p_(r$CP$lxlnji{9#R_B0Y`u+N9mwbo%D%N}Y@H~Am zeyh5=scgShZ*}XfUaj4&u6Ai_?e$uFv)bOMwl-=^Yj0Fq>y_44Svs5L*2ZFIqqSZy zFBTHhMR&TG%9k_6N@ji`Q(T@~T3p($mzq~1!I$HqceC-&3Q2zXUlvmz7m`2B#&0EK zmtq9}L{>w%X~h1|ADs7p{;y4rW1z-V&WQl10Fx7v z_Uj`HK*duBp#9VS1pJTjKa>wk#x626{yWcS{Es=v%n;1zf7mhI^y1ibKFi{U5Jf;kj2JnO|M`B#{EYAF#o7P)i}`vgP8|k9%{g-x{wJ13`JcENNjsIp zr=<9#SnnZEfEIX0z+Dg&f)(?)j+jZ`2DyuXxa0g!f1LlB0I`FzjI%gGhqPHDh!pQ- zvWZ9Ca5aQ%(eDEDYCOj<4&F0U$^rSr&nv2uCk4%QuAId z&zi8Z0d{3ln4CT@GX5v`cGyu}0H|OBOkS5{WX?QR@M-!Z@HvZmm#vbvNJzmoqcE%@PD<BE1LpGFj$}*vJUmmZ%UdWJr@GZCi z4qf-nMCi+u|1H-!2s_&$XQ2FlJ?h+6{`XV>yZ~Mx5Gnw2q%$QKI4G9?x2Bv|5)Qcl z_Y=4b6#lg+5C(th0+|5(a*lAAlLEw;kB)HD_&*}90Tb>rnaZM=_gle9_@8my1dI?P zWP%p}0sbjNggStxfSsSPAF>8e0ceK>f>Z@q4T&DXUpIuP9zuXH?2{u5(pN|Pk1t?B zivpnqSWpm?NBmC`v5!>WI&VSIAlxV*59TK)8Ucs$xnH0E?~M5WzccyUx1Z7e4;BKG zd(m(!6zWAH$Fa!WDM10MV(J&gsrP5%uTyhA8fX=2jiv47*(xUM+Dy5gTc}UZ zl~aZJ#56T+dRWfen>(#dpt`@v9t@l zBcnm-*vxI_%Oh=;k_VRr>9?0w2TSrU1PlBC$X{B;GEXbkc|S+5vcYehbvEwKnP3w2!&I)>^N%HmYqHe!VIr&*f&dwN`0$sw?Z2cBj&oex=zd zo3vKTD@5|F4lkSSaypkGzv{Kh7sVD@=V-nEKg_1pi-8L=R(;UO2*kU--lNEBGHBK~x4k8u*J$ zJmm9>3IJ4u!xjYuV29*HfJq7OQSpHPR3lI=z#@gH&qWEy5$5 zN9Msl)p8g)HU0zYQGs)4oV^is&ji5UgMT~-;5V3UQ8%=d<;V6uQUSyfKt@0-MIewQ z6ys9>(WQJSB4>)pv0|ndn7ScafSn@{9I5~yI)X``Jv62grC1EL0{AEMmjla)=OK=W zz2Rj*f#NN!Z%N0@m&!l>Fgs)$h~_LLs5L5X0#CD$EaOK@1D(hJk5l*er~g9v|K+5M zscq`*Ha09n8q|1W$FKbs0*S^$^OuTZO!v+}=FQA;?t&SvpELwYgPYs8Lqz;$@uWZ3VN(E=>C$7?@Uy}+h@tn(Q`4ESxDET|&WI+f1{%Rled zFGdK+51qwC_I2hQxh_d|KTSAysoozR+W$fNqlj}S>f9Pr0qA=w05L*W!Xo4alMB!d zh=bt0w5R~DCY{%k&YhTZHS9-sz^yPQ&h-e9MG^vJ0FOFwhr&TP1=KHqF97cVJ_6K- zixCojsV_Q7oDWU`9>FOhK7iZ-Gy=^2gae@nNa3Im z)r914oE#P=(LOR6VUVl=ke*~=1~K)UwV!EN__BV z=kfo~%m4Qh?rt;;SZ+na2eI(YL==PbM}_1U#i?J-LZQhYO~)T3!q>uq#!Pv+x>?Ux zFj?aQs87#h!7e59M6*TFh)0w zT4i_!WsdZjTyJgbU73kBezCm+-fLghsp;gQah^*x!VA)ixxQNk=^^>%R<*TNYi(6p z-ExaQB)?gK-kTfM=0>Fn&#wXVNV!S*eJXI$(np3!UjpX2)$Ejn;XCD(j*>it-yvLQ zzR@nNG)v7D?XO%dR9t}=6s_$*Ju{$w_4Qu&y})~R4$gzySZYj zfXt2bK~KT#@2x*> z`M3T#|9^j&e|#q|kpDS&Wl9RjKg9nRsF^SD4|7h9_@DZd&^R;;DudY)h@u@>ol7f& zQ}GLO8}tIG48pn~>$rfg05i8BaOf8+Jsl3c0@0ZaX@mF1|EyE=NrHg9m-(tP@Cpp_ zi1;07B@7<6msgNYz#~@wAKHiiS)WaE*Z3b`HrST^t1e@kVP*L)eG%_sahaxX0q;#r zO`x2Jb%g)rjdGXAW&W_BsJx543I8jrvjOfdN`~+78ev2EOawCN&FA*mAD`+8AKj2%x~d9+d=7Y9s%UP;!J}5I0ZPNjH85se+D@(i~u17 zdI$0~s(%91x6mNG29hSAoNxS3YaJ%n9sXvC*1)qXs2H4j%+UiX%K_B+iDuBbpOoVg z{%RErs)7IUAe{66^YTB13ot1!2ZQV3(0(j@B_4Y<9sgM_LD)ZqfIciF-_E$VIUp<) zAlj!^-l*lurF5~HEmbpf1hLO2W(sb0ffQhh*erLpmR8qFuyk|1)Sv~qyXE#~xd~uz zm780jcv(iIBe5}%d9y2`Bis^VXMVd|ZVIENxPx;cDQ|G zfwutgtwz>H}Dgws$P2dbAvVlIHK@iQ9vkL6#heG(Xh-JUt* z{9-59SA@_V;!X%NI|_){-ea;8R2YJA%=S(=J)llF6IS}ki2$U%no2T(&M*o{RSfvw z?EmA~@Ab{{PfO%K50c$L@IPN6#(&j5c$^;%=fzx5b0HRs2>?4ss=lnm;+uyhT3+4Y;rlf$p@xLVhV)Gxx|7S_GAq!wa z>i35R3pm;4U%IpUUfC`#$}pw1UChX1L412Yq!KRqGj z3<*Q^S`}v$sBew{eVwToJgD$5z|T|0|8V<5{O=e3FWcg>zPti>ZulME;-vt1-$I97 z(V|E26{cmNkC-|D`HTHud=S#oH?Zd}OwOKJnG+DG-pRM1`-owGsA9-!i~4IA`7@0U zzX=m8NQn+#gfLC`|D{Pg0w4zePf8#bwV2SwSQ%Y|NFf#{LjmJIA3_e@8SVcgs1^qS z{ZYWrDP4~yeEZBl{>I^cY2_yGo z{s*pv0PTl`|6k_#u&AgBOh5%d&>C*V{8wTE_T>4J-b;c2cku%76j}gc$-<-wGY5|V z9m0eR_$4EN6N)Jls)q%%9XWc8vj)uCU(rLs$mE6pQ4LHBphXKX7XY+x!lkMJ;sU_t z-_$IFR)~%WBXhoqR?xY3&i~(N&cfN_fB5gtP86^VbVA|nXykG{`XJ^0Xgcx!O!A|A z@@F&2*HZ3HH+&chRI|l$d99o+%}*7u{4b=7#nfyep3BG5rEC#q?ku)D_10>!fvgpm z*H8iG=rlS-P6}>p%s19e=Co6ik&V*InzRkk7gTtvGuL3gOlBt9#C&6IZdp2v&&g8K z7MIs~i;RHu@9~*K^gHwDo#k z+QsR5eRi=vzf?#U(xIfd0NhN@&E?`V`D9@sS1ZhQ>Tas<`1hmX8;RKK>GGd+dl0}KB-|9{``{ww$MZSsFoX%pr|;A&I=LeMP*6>5d*=U`M7 zIwp2m^aa3ROw%wkBQg`~{m3;y2Lm|myl=QdfgD@EpDbOp23A)7m&kt@82E?t(E?P( z5abq0*Gqc^sLTW&6uh4h6@bVbFdP0)T;R+s;{S=jcc5!f8PZnxmq;Oixv3Ab!}*c0 zRdf*9t&XrbU!ku!#d51hMDUM@dI4yeBTfhLddRm(BEy7#M#6HG|7F<#E3*d{lwUwK zAofECX9Oxk%MtB>kFuAtXTFr23#u5!h0!{&f34@jst+>!0@) zjp~);7lhl_jOrJR)jwGm+85Ezr=5z(dik?89i*k-DSx^q!ZgM|W%B5f5xbPmS_R|( zr|XqZH!FX*(s^Sx^DP_)b`pM1%iS;gi{98 zBj6*Oc-irbou4xPSoo$CbcRGGQ<(@>toLg7PHaNU&&(+&=i_Ibjjn!LovWTswBemviy*(?7Fi3v|Db(bRoU1!qTbZp1!>cUk zrN4~KGEy5}=4*@j+R{vQIbU7OSLiS1tMy!EX{NR)!eF&dOFvhuvWMM`WauU8of5mDxJ9KNX6nLdi@tlZ)l1-P}w(KbNh{ z6k642V#D$CXJIPXo0-JxnfU!F_aqV9jzu@9*dGcn1OilQx}R{V8TfUaWjs!?T#g1l z;%MNPCjZ9|COE#f9#5221&9YiRu0I_{{Bhv0^lZKFfi#*S|4*dxC^&~*5K~hjsFNF@_v_J7O&HM9R|OT{36csUaJaWeLm zf6CWl5gc|$9P#{pEt~Uy%*MV={>R{Nk^i*tKbfj10z{#}=Nj~`DuCL_g~>-Epe|f3 z+zSb#7eK`lQKLDzit!_uAKnrl0VoUq3mZ%Dj8w=6Xn}Ut;TEUd7S*~AdrM#K|3<$G z-tr(`0HJP)CK4KF4*X9D5ZDZpvv^w|TuO|piUEZCSRWMw+GpG%h)^`p3+zx`w#~AH z<$&cZNXsQyF0}_`8$M1B<**?;T8O3OmhwLvBiF!$;zc~gS1ibbqJ|*sZKwgM86OpZ z^*fF;3;`Ox&cO!(|C{|E{#X0I{1KSffCc$39z;<>6QQN{Tfmxkm&4%lDWCy)k# zy1x^#w}0g($0s;1`N;pJ#Yp(t)Dxmoslam>4t=&+{i44J=L6@;{{s1+5fp@lpA)-` z>J{mQ>Nm=Q{p-?!`DvxUu6@1y`RFc+z|XnMeA5oN%ObR@6rct0ieK+FelegBP>5~( z)&Ipj+kW^4K+rz?Zv}!#4JH?0BOuX194OXwBu4-hz#@Do z5X3oRO%eEC&KmQW-%|n9^6y{bK~{d4|IszdzDfB1ZqhxE|FO`WZ=^jd|6}b>y2O^0 zogn7rZa9po`9_iuAoquv1c|^TpWaT!P~m$4XFfesEH>vdC2an)kiWoxE*Hy8N78fY zLY<>*OG}LfLSHJ&g-T<#${7K`wg@K$0Ov)7zK%<$uH$q#N}vX@56={kTSL+L^yNXX zFEd$_mRHQy$VaAKDpZ%jej)#Ay--;!RF;JEmEk$004Xpox1>KKQq5N+i#abs2T-qo z_cIWF1$3{>NGtR%CHl*Nz43e*s#jUC&UASJ;Lnv8vJ2&`0Kat@EM*rEE*G+;g-ofO zuayebjGIjaC`uVmhlv8pBGd6)AzRMn8|84k?e}kn!x!a`z@;KC#(z48Zfp>x@+UD` zp^_6IOp9#6uO!2-{3na5Bd7p>mrd+OBe($m$4e8bV0e)zptyw7SUDi-{Cg^Z82^b( z2!yfK6Rs}?{J1bY_HE$YB7C0FTG)^Ba~N!f|1Eq-NnVWeaJX3Jr6AAHjBoKjE6F%P z{=6ch_4A40G1Y?y7n6xbO2`}yq7Ws5hzG^U&r&>#3xL=0U^x-r&ZW@~bV2bfcwzs{ zH1d~@74Xrx9nby`mP@LF#zzJH;)96!rG^h9r6^iVd}Ail&1Y89Nzq3n3xGhQ;hGfr z#96?HPh9YGh`>o{nnVVR|54>4dd~m$K|}7Yc@&M9XjF*pwfs>R0)JVuqJQe74Sdr;&T{hkhk!S^!ZHu7g+xB0`Q~J zqL1*1@&Ae=lU3u8_#X-`dHj#&f-eL9=ZlD0Vw3jaWC|1iLoCn}_VLkbw*7Tw_)Q^v zZl4 zoFAnG{@+VEZ^k7Gh(T%50^t0Z$QyC800iJKMSQ?N?f~rnnEeUzv5q|Al7oy3fMdeU z{y$6;AZ1v}0gYmT?Cdb=1??#RqXNME@V}J?;+U}Yfb>xcs0(UwK9u?=!UuI=Rm%in z$wv#|7PMbe2LV0;#QMbiqcWf)82@`M{7=20bN>HEn16hcGx9%)rNAWoUveA{pj-<_ z_F~biSTj=Y8)^5gOdJa3@_^)1$C*v!@`c81rZhv$50c2uxYOBaIulOLQq7WbptJSG zVoi>QVV2;@2jq0;voUdY^H-M)mzeu^vCD3jRpIKPQ z$R!wG8WRRbBMXdMe=f67N|)v{rJ}a;>C#+kejdq|i@AkVG#z)`L?E6DPo<-oOeB-V z1&}Faa?7Q#+w%K5;n0p7xtu`la&INw%Zb>Y8ztrk?W`UM&HD+{3Zb&y_!{&2(xjpWd8F*C`F8 zDF7NZB>azn*5nAo|AEOsBa=ETW~o+0+>t2=Ofd4d)+2U%mP<4K6Q#pCrt++&5*Fsf zHlTX!|DwOdVy$#?HIw2)iqR;VQ7+<|@t>u5UCbXOZ5ba#B_2O6W@#;ah?V7_frl%9 z4qX7}_W$ptFXP+ce@cK7`44JJw7JIr(C1KE$df8GkS*v7A!F{wEH8oyT08UFg0;Xu z|4{~OL2E!A`elxp%GD8IW^);bnRyDNhpt-zxioSHjMX0QRD44#H@0-3*H|6sh1Id< zQMG%KU;ufAQMbH>_p%50AAzpne?|arc7<92oHGcV!_4>vEN+MQj`P2|03=*zC7?)8 zSiV>eWs__}cOvj_T7dAs7$ha~AHrudYTg$|1K%Zjjc$)VU&POXi-E5b{@0oOs%&p9 z-~j&wHrP^@n~>7@pNKdzCar!czrSo=R1cZVg9sIe_!iFp^6dYX|2qnpml&RB3;4~1 zCqnplHHahN*?acxIv3#oN%;Sjr||zyAn-;p`}>p5r(0Fvp8y`jD4iB`|4bSF^Q|(3 z|HTGK5A9cf(G_n0rFH0ctH0`1e~EOF+An%~sdJe53!V0hP3>$|WgW0zo~qb7zd2}p zx!<4=(0=H}y8s9Qazl5+Pj&&o|7Zae1$|rvAQ3>k01@YqT!0An|G-Z(%Kz#JcrE6< z3gO4a=KpG3&J=zS^S`B?OT_<&B^Lm7fDoYjF1f(e5oH7ZS0qP(Ly(34vHug^Lw!+P z2Dku74Hg%GRt-f#Fc$zy0dp|^uWJ4;*}?KShDC`4i{E$-m$|6@i#9DFZ*ra}G)$Bj`<2;mXdSrz;LdH%1Q+xtz)+4-NpxrF`9;RA*LL*afj zLQKf5DeTWulJ4%5d+bJg;qV&&u?V}C$j#+S6#{1R zwc2#4Ms~39w$=#EE!3o6l9EBWg+*ya^wRnOd5Lkvo3lu9Q67{LZKvn#mJTuk$qUc3 zGOuHCgyXa-*?C&IxGTXTh}xqPx%;Bu;1%#`w((o`hv z3jZgjf{9clor+}AvFvneK9yN4hTLTzNiiY*2#(#@wIuO`?om9p6N|1z!Yjcr{9p73 z+TR@iqX>{V|8g?+t88LA5d51TT(|&ZM5CwxhUFIX1OH?BN7A8inUq_Fe-7<};ep|= zH2yQkBLAUrh!u4+FtY`|`i&0&oU*suIz<4lNtm31`!REH(!0H&c5hxe1XC1H{^E*NQ z@U{i#2&ikOtrYfg9MJ;Q*PvE+S(d0CUS#%vw25WEEDKSyCB`l4UW#)n|MM)Se&PQy z;gWT@MOAPftovADNanm+cCG^O+ZO@trtcX4o3g`KFmuTNI-hBnE_upI7|CX&94PJs z2mWUT9yk7P=_g?Sms89%kwN?~@&_UOkB-9*`EUgOM-5?n#{chUr2hX)Y7ajn|9`gu zKVPX|STK_FbajARlgj^?q@Ec6GYACUoXh^<%IfD`N`PX_7v}%GD~OJ5Ur--`;YWmT zec^nA{C@S9eeTv6|7EYH{4ZL-ujq?9P~)!kZ3R&ibTwOt#br*f_UG%JpVSLo$2q~g z`J6}RJkNZ|0>&9ca1KHH*!C?2z+3=OJ^W9I4&#rj0E|$ahm2s(1ttXO?PTEN>A;WD zfgh{9NqA4xdEFh#8?K}Syy^Pij`N5_^5Y12J0ZaTD&mS3@E}TpfFEUnWijtl@Spe} zss@>ZK$3;ESSZX-AdsCMCY`9ClZZJo0O0qkgJSk4_y@r|fM0+x9xC}z*ppU6gG66I z3jplleB%5u^`jslbW9+Bv<7IOIbeS;Of@0j24|B6u^4bRfph1a|Gzo-M~QeY{>Quw zSQ1_WTW%7t$6t^H#%GS5a<3&~`)-t=lvNIW2!ztvYCct*j%IVQELuP|ngRY(!9*^e zoz2Y66B091r8=m}TsZ@23w0yq>|8lZ3!l@1-*eKLo*#Cyb5*^|r9LvZkg@s0R?f-F z>N>+U;dz-O^e)_PQlv1EFnmVfoIag-pnW8OxA75bE*Y9~{jqp}(_j1^!Dqy>3Vc(l4IcOm5nN@V^2r#0&WwGeh0pb?Zt#ViLe~0{!{a@m*hWyWG;%FE- z3rKI|6Q0EXUu{Bv5`O!iH2B?n2RwuSBOUnv+4g_<|BYh$k5}4XY}LL{BR^zLU$I=Q z^dru%f$_f*n6GOGu$Pfv_iJAcYJWG7R%Dx2jQ=*y$gc;rU(w35wAC;5Df&GA%U05}WS3IP!UL~T#V9~A&05#V7J zAixNB@dBU%!2h@jKFS8(&p3DiewGcqn+(AJcmYrWfPaMk8}NTZ?D^0;mj8DWqyP;0 zAMd~`%KxMfUyBV>0MG(%5kaH@Lh!$o3DR5uu>XW4VuZYanArKL{D~Ie6$lmn*Z6<< zpAw)e8s}sD&m|`d4EaAO5k8~zC9yw8+612q|A({baxOVb?rIupjb$_8 z6n5-nAdz*`v*|*C(m*-FUBphVGAj_CQc=^o@j3Q(u)LI-Td(Ung1S zk$>?_1^8jg{XCNh`5aULB7blJP}`>%izR|#P70>%4{$5|uh351JGTsO&jSBoGm~L* zG>eCf|FJRS|0w)k&F`WJNIm~x1eF2srlgO5A^>kG04Nb;_J2zO5HEn#{E=uM5>YL92HXWZ0nx_=&;@z{c(@wmrUSHFrmexh zXaSlgY|pYXbKw7`Bc^+^${{JkO^yY7yd`UVE7rEzc>!G66UfKV10}K z5&m}qC?g1DPP9)!Pv9~y11s~aZco02WohkF-!4=S+uH`rGH=Fz2|yaD0Cbo(&i@Mb zqF=BGUnK8!WM#IBk)OBNkAN1SYJ#K>bOWRS2>+9(ApGx0RU(i7=@2HAll)~-UQDwf z{s-ZwhX2_iCSMX5KX}OgKji;s7|$O^eRBtVx9#J*V&oC*_b!$HCn#q`HR;Fg|3v=x zoxq#L%->(@m<^v6;8$b5SmptBgZy9h>yZ54Z6o!++o{9(^uN^pZ+2?G-KqUf+u!cg ze`B&;WBfO?+jWyKcj~{Tzf=45c3rdsxuuKfvdsCXJL{jc=QsS$H@GRpvP2zVRkU-0OSrIhn@G5r~tzMq5`A?kUuJb!9VSrG3TwA z|A(A7<~l_3WAGqq#O{XgzyXS_lTOGRR}+zhXQDIAhG{T3cxA+n+iaS z3b2atpO%6`3jbT2N*4C_!ju~#UWnrc9Qglhp8tDZ|9?Do=#2J%_@ByMFn^x^A_PLP zCGgpeMu1PqA0ZvM$LV0f5a%b(!T+gJHc?1x;0lK8RA?#@Opwr;j-`Z_)5St|PT(0@ zR)j933fReou?4DUg}SF^709*Eh+Gznw8G(Phc`r*OP(?VJEnMHa%6T&R%SJ6RqRD$ zZQlCAF!sLeKUSEhkpN;3IMqy{%ODt*=p2GN zhxroo6OOK@lKc*QxZg)0P>_5Cv;fo!9;8K0;F1bIrhTws@`VWyT1h0x7hr-{`JZLE zd?Nod=e+*U|Ec!>+o{B@RN@id;BOxqJ@G$g|0n*3XcJ5Rr4{(pXm)@WLo~7IKrc)V zb{a6#TE`F?pe3}P3CstlnTDyKaCgD}k#GsjbIZeT0C?3AhXN*XNS+QDjOiX}V1yqn zWgT9GNq=N8=WYPrE@Hb78W-a^{6BSJ0xhEzkb0y%MU72Roa6;bzm-5BF`L8Z?32|H zTbV&#jkFwPay9HThh>?p9aWg z4x)p!d=Mc(06*tLt+73lARGr_8mg1U|HLj#a`G}iJLHcS2IYv&^P@2tm4x59p$W)j zeLjuFn+=QF9&ro+7ATa0e)!zx4U)ZcRTgp(E;Z5^7s37gb|TFX@9?4|HEGW_sEX) z8R7B|Ow&OwhtKLdJc1VR+ua&7|8&&&=a<)SOu1br@W@}lZ}yTbUP*L~Orh0R#VL@E=bEpikjYya1>G%p`RG<7tfl!vF7Q9dQASvH&Cz053qykB&g) zf4H5(p6@38LQU9w^2@ql(@_ZqAcphbeqM(ETVgJ9S$BJ=S82pdrpFCpP z>oE=#kh1@PI<(JI1P&pzp4In@jnO40+!`~7@6DQ&{i~p3jp#*r1E4WvJ?>hFK~2e zat?@{!t5WOnhK@Ff*rsl>L$Y}AbAEXPZwtrGc)m-TmmuVp2;O=X2h0Gt$Q0pK@dte zAXgLm5Y9&Io<`ji@kj)O6&sU%+D#VL+?O%j$~US zb~+T^kwJm^nM^#Nc69=S+DXUcQZe2-x#5K2crK$Irln)kD%N4zG(~=NJeQ6#m`l;J zR3e{pb5SQ2@e!I%HF*k3#@#?X9!@3Qd@`2L`-8Zo=l#LuV7L>BbYo~<(M}}Vg8zfz ziW8a#K`CS%{O0-JQ~)^SXKDAxsrXBJE*OkTMNpWZbAhR`Pl&q|y_CA0aY&eA_{g)3 z|Cad+v7;t{yBO^W*D?4feSn~Sa9!YE`Cn}BK_Pz=BY6hlVP3bE8iwDKPD@}9CUkD0 zHLx)&{>hPKhVc+Mm4bL9f%n`xDrPowDJ=8E-q`DGn%8x<0R<&$29z(D%gn z|2+Qx`=$cCQ2RgO(+{Se!2cNkjsLMo1F>K;_J1fElRH2QEsu+VG^nfv?FHt!t15s1 zt?>US;TJC{~S!fBkR<)a3pMrJ#Q-iGXehRK@oHWE=b}cuYFV|L6kn|DC7U|DTA>j{vq^ ze)sTis)yKEaj*WTgZkee&@R&X!@=Sor9YHE?l1o7a5QpQ|Kq-h z-4%5Jy+AL2zx)5O_vWE?Tvxt-x21c|sk!Q$b0^8NE!moNHP7=rp&3os60)(4ZR}uU z8yklho0x=&ErAy{`k=}{Ql_w9(kYd zT2*!Ho;ygkB-`CB*cAKh+O=y}-FwgdthK*u?N#(-DC~%LAKUiMwud)&2#LMBO902a z-OJsSEd8kc(fRDHwgYAf;8YgU$PxgYk1fD)stFLD%_!Ot?6gGzvjl(&2>$2zq$T+w z?3YkqQ2$2Jd11J7z1um3uqVS7fc7Q+FN?(jJl*fGp%?}NTYZWD2^r3{l^FH{!Xc@E z2nViq1R#{)T+un0O8f)=@3E}`aQ;&P`zePq)rq5l^AYv~`P&n1E?n|Ixjm5pSv#k& z5h!j5(9R`v!{-n7!~aw$TVvt{hBl*jx~Kp7jPyVF|FA(ei8e)6-Rd+ zRTd}@Cr4HMsI;7Asm_jO3#^QfW)!che^nnJ$tVk~s6w~NkLmabW+;v^e_Rs(Qq$c*vl4Tg7MFtKn#8g>j}6>v(0k;XKQ)?VQl;#jq~3pib=M1JBwxY z_wzsn{y)>rzci+mIL`p@=pEFlkh+sQ;{V@-F_L7!Kkj|xKvb$?8HY$Zq*GG< zFlY8J<1UyFj8hG+r*pG{TLAoMekiId-4Oc!^dA5J*W-V1Xg2>p{Ez>?J0$%-{(sN> zg#! zo(#{p`tfgb3p#_f@V`5{fo&KbLbW!-bpKEk)$dlgg{VBh!K*0*wV%^|>LKrXs-2aai2LCf{ z-W5-TnS)qpBXeuFndimff)D`oKm7Ns;(uq>%)r&S_u=Onus=(0|IOAK>PdQqsZwde zKXu@LXx`AVB11t6|2O}qc=CT#{r~gC`mdQ9$K^fW+;6Ua2mbxe`Ky6^@c!F}R{bsI z@TzYgrf!A$kyYP0qT}D%Q6G=4{_fFL-#KjU*7-a1r~UA%?;ctG?ZXrXS*gNkHte?# zule5biJw2d;~avAPH?KYGXZqX=dJ2W1bVJlkgF3PJE%|ofAs$YO8^f49H;U>AI1MH z0U!Y)^XHcq1h0+v!!Ed&q_ zOi(cVk7FP4kMutT4i}vZwly&P@40}u9P&s1e}WYO5{e?u(av9r_GNPcghTlMo&OIg zm8jxqfcX{1`8#865a#@km>-=#alX#{HUCq=^Dq97+V1vu|3Alj{C}q${sZHDEdM|J zk7F6-Ih?5|V6y#?@IOt7Jl&c}e1a(D;dX05C+-ywjN}U>TruqmB6j()322SFqxsl4 z4*C9~1^M_m(ZTrwl|o^{Rnc}=ZW@q2$ScO`bkN6U-CD{hW5pWVfD1(L<8&kMkZ3!^bKiR?XPg_g0h zQI)Y_JC>D{j|+y2h23O0ZD&b4O*>iEDe|D3@%FHxvirN~0@$9+7>fw93!;fQS(l_6 zlXP8@tTFKq;{U}#G)6|gFoOR{-Cup%O!>;a&BilD2K>J>-aDAHDZqR(g0k-|?rYTk zRJ;yyv!NA;I|u*6=e1WHHYO83b_7ua?)>M7XWk*4q&>LtNt;u--zUl6Edm7sn{J;? zxLAFF?H#%O7Je5<8*>2F4JkUAmD?t$hBl22FYgM!0dHLKenbN_|0~*5g)E@(KS0Y4R_N`)a!Jrz z0%l+stff-JGqewB1MR@GTL*s1pju!I?&5W);yr<33OG$83ko|!^fU6Gn~NTOi^g%` zz3cE9M`?MGzUU}p!Bzn@6Tp~R!PWTBxl6_W!L~4DWNyb$a5cbBPcram+uW8eq(1{L z3py(T%>>svQ{qP8I$^oMIW2Txk0D(2oLZ)}4;=We0&1@yVY0YABxw)u&uGja6NWj^ znafFA%_JfAjh$e75HK7DClx5EET@^_`p=b8Tn{L>Uc1N@Kphb_Riq{)k8UED{_e?W(lN1Vo=#$3l1hO9(U?8urWo zPqu&fAH096Pt>r`9jOoI0C?~IfBgK^pX{l}vkiBuLqcgL;YU2JKagF?=>Y44=dH=~zD< zwYtEbLat7NbB7b!b*e734=47KV_C-fbW6q%^Y>Jty*`fhV=f=>=L?ItpR|*>os=d( zz^fw=pj%{^2BYjUL%E;i-WXz0LQej5ae^VRGM4jyaX|9VXskt;c7*=_F7ZF#-o7>P zSI2v7G&YYQME2XVv_HsoSONgu;aC7$j6I79;AD;hp8^a6=WO6b;!qd=U`^mc7xTOF zAL_2)f57u!uL!`*P%$zj_5xc)3Vo#atq?1q;%za1S=xgCB#2k`$QsmR=Il^_r@e;! zrj^lb9Bn9bctCf~55PaT?k0fPKGmz>S$Giok0s;?m5ISe-&S}*tZ)YR=W_)3DO6@I zHZrHA1q!n$kpKUMes;F8nBM%yTG#LG|NE&K3wN9U-TzPiPx8#*Y9`v0GNxl2{$GOd z4?ng!w*gUzUE=eOfELj80<{{NirXRW<|*1?V0_o$tFH_2(HeCv7jlHp0coT%zEU9m zw-o{Kn^gcD<5VDa3Zbt+bZ=>PHjvjo6VN7)|3CYrPxF&I`B(hkisrXI=j7j<9<#MlE?YF8j)&!ukqy5z)*INB68_(x#D9Hg z^>LQq%aaeHe-~B<=8sSO-BBI?{)EauoV24={^689*5w~hP5k4T ziGMgYz5dR?Zf3iUK zkG&wL{o~!v^Z5TQ|0hTC{}2N3M7Q%qPr`nod)zt0RbAbMR>J@Br2Yr@K7M{@eqRFcI%Yzd|Ht${ zuK(vqvH0J6{C_9O-xmTtF8`0>mO&?JNhV>=^*EnX6%dp~0YoWCQ%1%A3!ML5yUtO9 zmUfz32*3z%UX+e;L+H`b#F$)UPOf_uV{)rdFALjXsgAeY-13g!-cuJE&vVMJva{x0 zug0Xs1~(M%xjZRh{|?(sk0V=Ut`{+CC)JBuui(nS^j6AQo+0Ne}WqHmE2&>36> zaEH8<@RosxAX%^$@eim>;aZ9-|G{rL|4Y@qwD%V6JLvx}R{<=k4xzqe-G}Ot)0nGY zCIE(jh58bYppQ&oMX!rQhi;e{5VbSE)BYg;Tf~7h`NscLSgIIAbcgfCu@|J+Aebbq z7bx|RJp$x^zwy6IlH7tA9IE1fHwOlc0$&l}66gQt@A3cVX(O=P=YRgP=J5ZE|1JL~ z{O@WLz6~S<(wdg2FXCVqHUM^(I>tt#i%{^0Cd z&i;_&IVx7XCv_@Z?&a)SF4ur3Cw{=uM*iT%-G8-j`G(frT>>~+kU_EgD2YHx&WWBs zEhRv10vt&c0ze=DrooKw@jrP% zWdd-7|49KvGA(_bpFQ#ApVy$ zStx>t4qz`}{_0l-?Ny>;sPG*NSZ#|hmk0e+M}=kb^O+MMOo6(+GQ#RW-wG8PN33qFt_~%@bBdAy?d4Vas6once6-5t z?x5}-c;|L!b!(}(y`KzMRPRX7$6;mNB!r=f^pnJH3~y{-!@NU2$BOn#^6$odYaF(U zl6mqHs``|Q=8%qNVG*@r_x~sDEX8!<1@ao<|FF;heg4-Ii18p2Abtw+{HtIKIR8J; z2}fEA(;(H=g{dp8Q=JcYz^#E+;c{0l=;c{fq{SlgTjse^02%)yts!s!j3$8dzb^sc zlSh^Vcq4;x_~!_g*J*#8|4nt{v9~kBzncK&KUX)*2d1HX`)RfyP5W^|l2R-J3hd1y z#fqX!sIm--hUcG;4(e3uLW?yj;8#|FN@H8z2J^e~zdi#0mhGpM?o+p-00BN3jsXUDrXV?Es@_$SIe_sOdEI^n87QrfE zKYZ)HXlwz<8A`8_*eeh?NDP_R?VvK;OEJ&6tN?eR)9$C}$r1qY3R#{1Hu`6)cL>D*&BtV{b6uG9l-Vw1H?ZRw=x)i zo6?DEdhY7?sqzSP-Y$5o;yg%lQ3__!<9CQTeEJ7Z;Q-xYsOWn<-`#0@BcZYhAdRXg^g))7HGd>8@ z?#yrF|DVqPkUui}=a(+{#pBz*ePjac|F)z!xcVo)cLMU4Hy$tjB=DaWtCui;cI}TY zP}cqE+}a%b+r6g-PUqc<-Cq%XZmw3*1@@nADxpaQ5p;9*8ceN zh7VsmaCW4-xqWw+0N4xcd^c0RP3;91?z1T10~QEC9fiO1KT80peFm%mj|*n|0G0q= z7!F@wU|E31f-j7O*Ya{B0J43;|5yYl{NaDL{iP1@i}~UIY6}48f8u|Ed1OL#BR8pb z0V4{)b^&1RfP(&?g7^m$fK)$t^0!5e{Ga6i2~82C4TALti~fhQ(ft4H7p4#Z6ZzL^ zKkEwJ$P20zpo$l^%s-a@)ARq_%l|nb-M=FrZ{7d@Zz1|dxv~KHT78X zytGeIA9uZ-ts41g?K)*;u)f{kIP(t-2)8`i&7SFJi&L!vAkoMFParS_j9=9=6TpN( zNZ)G(j`M$=1_y;afW5riUD@y9`A5M2pr!qRw|&FkrvKkLf5s!kPP@edsNw+6wU_`{ zB@BxupPh^Oh#%erMY#0Ut!DlT^`|uYurBzaKgxU;=gV*Ja9&qxR;OaP?s}d{jZnq# zgYe(HaJIUsgM7&V^C2|;zqkMIO_}`D?C^>CpFb-%0k8sY!v90mv&RE8%Uk9cTpFIfCd|6MM~D?#}rM z65|8U=w~2*GUQiT&*u!_qxmo?wJnMGBwBr;*rh1$?_#chU3riQi_vB9e{K)UWdvt;bT(BQk zzg+RB%)Kv<{W{?MN9QMgbRM2p5!hc^`{Rq$*Xwv`-Mg39y;~otTw4F`<@FpnrL*-Z zx>-+M&Z&#*F&R|HI-qh93qs}hukHWl;gxG!og;TM3VnQaLGqk!$e=uZMq&jLgu&=DR0559f)AN>#TkMm#ppG->wp66dFK;i$~ll-*V1q{zW7kcpTPD=s0 z7yo}`2D@zmK+Nx?|39*XKg9n|Ekp#s|Lp(8Dgd$~mKAXRQ^^0r^1({W|4;g?+evA{ z7wo=@8nn0&iVq z?svaM4djmE`sioN{v?#|DfX6_X3`o(Jt`crVpO4#deqg?>gu2tyGnr%)}gN0DZ9kl z{c`W4L9iD_ig-Bb6wy#jasE$%f2{%(X*h~K)f;LJBeG(BpH+U=VnT!eFlJE(0Y)dNz+(+>^_{43rMi-CIFiJ>8k*40$>rK{0EY8*KZgd zUe#ChE*XTXv_F>izrp7fiqE_R;25vqSquS0JJRix?s^Op^hhcuD5`0Enfa3qoz5G9)ug$+OG(H*s^N*?K zf9f~U|5W_%8_r7qZ<~Ly7VwDl|3YrO_vE)+OnyrXwuP=&v&WXU*>YF8C9w=8&cOlh zyL4d-0KlN~8a8?NN)Y12rj*wP!3yJKWx@<`*Gpx@?T85Gj#v{V>RV-FH2IOvl#N6U z+umN75@0rr%|6BW&x!DbtGS%}G8xQ-JzoW@j93jY32_8prfMc=#z?xFrnzD&m^yfc3#uoZ?8{ zQ7j;=DohM`4Dxs9KY3bM1Yu;nF?=Dr|DPWR@XtG83pVjT+9c)^hr6Q?Q8Xd07qlUZ z^go{eCzvPy{}ftoe4Hfz8tH34yjs|{&=&&g%8cs*bDy2@lAAVGW7gz9FvG@E)GDUH z{Qo?->3=4@QuzPT*gW~r=x?6gg*e~6{Z9UfbAEUR?8iSp@nd)6Uxehv=c0NqSJ%IL zW&KaCuK&rS8>W=UHvE(pj*nTpDjVsb#;aB3(GBl%);nMh*f{S#w*JRgHoSXz!@E~D z{Pg;!kKR6X2G3_ZV4eexuRHgVC;%k_y<&+#(ero~AlaMMcEG*_Ff9wHmIJgGutfnM zX9-{=cx@c`4~PPIy04AEZscqMCRe{oFF2F4paB0X_!sgg$v=gq2Q~tR|Ig<9l8gUI z1SsKjWOncWdEK5 zNC`iQ3$C{Pr4Ib#|EC=jfFqm#Vgj%VApH;58vpZ^a*zL;^nUyGkH!D^|6zOl|6nJc zXO99v2*g4F&S&H(h|KY{l?P3lH=>cYv z1+mu^{A`W-XeF|C`gl+4(4*11+2!i*;BD<9aE(b%D`NFV(FBO4btbpEeN>M9nM#>{ zfAEy$nsAsz_<8$C&{9o(;oz-Xb{Cnf{4!T@> z@0NLlobV@)wwo|lO}3cffRA1J{Hn06Fw{LO$v3Iq!M*|NrI|3$$HNDjY=0~D#vt&2 z&`H*$_xAt&e3<~BjQ{;F2=WIrZtDLBI8*okw=4aRIsc{qX#>WJP6g@VXUG|jMe2`! zezg_A9>%87hrE~TtS297ho3pt6-MwDj&-3YdGdD~Fs}?kgzospVPhmf+=A@=Xz91a zrJ^2D{ifLM0!7PmM|d7Q#v#u~ERlsnZS?av;rB!#mvd`wimAf*Fg){zqA3rzwRtXx zjuA0|&Emyo<-iZLa}|Y;sCVe*2Iu8$4I@V2j?~RS z5dUL0VL`Bp!LkB@63{-9Y;*pH9APu$Ks*lq|2|*HLqqug32i25ng_(p!T#cuv8Gxoe2jkDjqwg{RgfYodM z=kc5Qe|RnYMBdr)@Ow{eQ{1iz@=JLSnjFJ^T=DYW!{?XR{p1P&5AAP+l=T1W8=@RZ~WP{O+UM)3N1gQMP-wf`lBj8eZ20^CDz9-`N{Q-l%HPPh@~K7!R&>9 z`pV(IIkkFXsB`!(E&-H@KrIRw$xRsr%aPj`Mh*o3?|}a!Ee*i%Nb=DCufVY%wYS#$|C0QlY(Igv z2EzpK0{rulcEmqy4S>U+oS*C(K-reGSw4{e-}wJ_wluxx{~s_JJ^}wD@`wMC06lD; zXW$>_{}PFRLPY6ftyWk3-_OIo0#`|5DR!0kCvHWO01*)YjVHtMw>$KfC_V+(Yd$Y4 zc+RVCy1c5_mF8I^aO>Zo@fyq7T9+aS*HOh=CfjR7=d3Y&HrXP2mlR+0?)9>TMk}19 zg}T~pd_US9b*;CnQWbU9wG0^dvakp3M?;Z{q5$B3Os6!#jN+|!Qux1%-;9ms38phG z*2AITpC1bRPhL>>{}ZNt75)F+*Z;U1gug7x^=|eAxu4Qvv@`*_iwg;Xkn~LSwMQ7M z0L0-o;h*F`!%>3TfAjdrghk`BaNyZ;U~;58?yhg@f57$WCV&S8JO9^K02RoeYZt~G zwewe#^L;s>c?orm4C+_p?RpP~Q3sW`VY z>R$ezPfY^!3HYBsoaP@9A7{+}Pankpl;NQ^TL6;>;C`h7E=>Sd0jR^wIJ7qk>;w2# zCV;g7#yHH~&+nLR{4Z!Xq!+9@@KdQx0Aar&w<=cXv(g|LMCU@Z?tt|6kW%tLa+e)2 zzeUutUk~@ilTUy!K6ZTd#{Y=wWil{N=YQr&YEAtA49=~Y=MEC`Ykk4||M0(A0bIge zOMekN56AD51a9gYn==PaBb6{B31vtg+7@SJU{}H#) zb2 z8vjp&WH$k(!2itre>bT9V~shB_0iyeo}y{d%*Ou*Vv>KR?_S&VlSeoG;0)S+$@`_r zH-*oSO979VA5Xr#_t!QF@2_wA+2gRi=>6wUZ2I|)jX(Fw6PteVlv;j4dD4!Ytv>#o zmM1s;{HaYp<4C(5fAKh%Z~8gMCpOwpW-^qw*!at5xBl}tkDnUpq5?vwGxyi*ZJf0w z03`x#69s&{N(AalPDp@gL;~cr-wXkug#ZDT3{IrlJ^+9x`6v1xOaMXvp6G^Gv%uqg zA2&TeFn^)wJl>P|2j<7akN^KN1*4$Q<;Zxzc)(sgtK^@)FBtxR&hCra*aD;gB?N#L zf_)^{2l!2~gRkDDJL3OHPJZAYut(D4%72ysNb)KFe`gZ@YZHJ?0p$O;cmbshGXCFg zDMMFuTJHb9$NvKie>)#5 zfvaS&GzGiFKT#`U34pZ#p*YePj?U*CYf!hnuF6woBYOvE11ued{Z`S!fj64loJJqb zhw=+mx3rmcqp>$b8JXKnr`GK*Q`)^}4ejN}PC0^q95{MsXZt+B!{5s~j{59NWt?AG z3P}3Bxa}ZgAr$`;`HTPG^goIMj#)b4H4;EA{!j4!6+?)uod0Eu@w4FgU{H$x?JNwFa7vIMZiHU(o9096l}HWB$K6_2Ig!3rQR5Rt!d`1+Cl zrg1s{=}BZRl#cS>$-l|}@Hrpt>uLptuOZc}8|J)S)zXQKtC3$}- z7Ah;X|NqwczqApI+i}qoYs_8%uzD>YnxYz-j`vjy*Y*Nk>xJC}ywo45bUN{B<^R0L z|DU5JfKSH%wF!Xpe>VTW^FR6j;eR%q75^La*I7W}e}EicHvo)_8`TOz6ytvd?Si9@ z`hH{&Ttqm)IXqtjzgGZkseqjMXX1ZG1EJ~WIYMD|H1k^&6d-Pg|G7H_TYy_|BWw-& zZwX2~#wYI)zVJW!GvWV(QAD4b|Ka(4G7vO*7@G&7$aQaV2Q~jwahR8SCybo8nF%nD z_Qv5K0S^_}UQYN}s86ghg#`u1WF{C&=<~UXSAqVA+&+jd7EZvOva@_)krwqpWwCJGz=$C_X!`=j7s6z&V)|Cr65ruhG-60*~> zD}rY7`F|`v{~UCl`gF7Me-iJF>lcmQ4cuO(=C3ECqXzX|`;YyM{mfMh|G zXM5pGqrnTK!52o^=PQ8!@$k$4Z-O86{3P}v1*qKqSOgR^0g(D%D}oE=|Cf3vmf&Cf zPufr06aex^{3CTx)aH1EK;Yc{|H|02EoyTK7droA0_-;a$H@=>Q%LwvWRT$B`5(bg zM|Oc1cm7kyUV#5OZlJ#{0k8_dS^y>k{J(+x|M&F&cQggit@HoWBi)Tj++zdBPKeY8 z=3Ly734joYBmB9j9kM$BDcy>pAPIUYC~G-C<^1ntkUtJ>Vp=f@0A?q7`Q8oA4WUa` zcb|EU;^t;AMK~2{o6}yddvSO*f)Wi;uN$k*Gq8GfiJ#IqCE=H#PS}DIyH%il6~lPi zCqEL`ppU7J9_HW`H463B*f@gyWj9|bup=$jUIt2s-83v<{s8@t^#2M0uqA-R`9H(| zPr7O2e>VBT+y7w4_`iex3jUY#U;J+}txwbc?_Z2E0lL{!y_9_(N77vh;Yu{M!HTZvQ6vdyCxu zq}i+nasNNSuhK9RYJ0&9fihB{PVza0|Jij*aPFylN*cGVe&xv*uB%vnA%k||dYG`G zFt?*UudRT(b5LGYU7i5^6fShg=PG{>7$$B<*nqN4Xs6_HwlUA1>7@|k-2a&F`Tswk z{(m=_`Pq4eBR@)xv-$sl|0}%$&3VoLbqXLVm;(>B#jZHyAx~V~iw&W1hP!jyQJ*6k zp{?2m7n!}>_zvCxKNVI4@XVurkpsMP`)d`T_WxUoP82(6hXCCrZT_bth6!2sXCGet z{K9Mtw%u+OkQ+XyKQXW%H+YOg-aYx`0+s&14O&Eqk+?gz#=}n?#wKK#Q#=KQoqRDU zc0_XWGZWu&fS#BYd%|d^1Uy&}(jH?Ga7VD8Li&9f0?G?2{^t}k;gbE4jF)AibVVia zi6R>5gMJDAN7%#2Vt8H1+=#)9PNot3 ^i^GGlYylo*q4U2*0ptI#WkuqDGmGd( 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