How to Make Vulkan Triangle 

Make a new Project

Update Build Gradle (Module :app)





Add Validation Layer

This tells you why it didn’t work. 

Update the Code

CMakeLists.txt

cmake_minimum_required(VERSION 3.22.1)


project("triangle")


# Create the library

add_library(${CMAKE_PROJECT_NAME} SHARED

        native-lib.cpp

)


# Link standard Android libraries and Vulkan

target_link_libraries(${CMAKE_PROJECT_NAME}

        android

        log

        vulkan

)


Native-lib.cpp


#include <jni.h>

#include <android/native_window_jni.h>

#include <android/log.h>

#include <android/asset_manager.h>

#include <android/asset_manager_jni.h>


// ERROR FIX: This must be defined before including vulkan.h to enable Android-specific structs

#define VK_USE_PLATFORM_ANDROID_KHR

#include <vulkan/vulkan.h>


#include <vector>

#include <array>

#include <stdexcept>

#include <cstring>

#include <optional>

#include <set>

#include <algorithm>

#include <mutex>


#define LOG_TAG "VulkanTriangle"

#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)


// Global mutex to prevent race conditions

std::mutex appMutex;


// --- Validation & Debugging ---


const std::vector<const char*> validationLayers = {

        "VK_LAYER_KHRONOS_validation"

};


#ifdef NDEBUG

const bool enableValidationLayers = false;

#else

const bool enableValidationLayers = true;

#endif


// --- Vertex Data Structure (FIX: Matches Shader Inputs) ---

struct Vertex {

    float pos[2];

    float color[3];


    static VkVertexInputBindingDescription getBindingDescription() {

        VkVertexInputBindingDescription bindingDescription{};

        bindingDescription.binding = 0;

        bindingDescription.stride = sizeof(Vertex);

        bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

        return bindingDescription;

    }


    static std::array<VkVertexInputAttributeDescription, 2> getAttributeDescriptions() {

        std::array<VkVertexInputAttributeDescription, 2> attributeDescriptions{};


        // Location 0: Position

        attributeDescriptions[0].binding = 0;

        attributeDescriptions[0].location = 0;

        attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT;

        attributeDescriptions[0].offset = offsetof(Vertex, pos);


        // Location 1: Color

        attributeDescriptions[1].binding = 0;

        attributeDescriptions[1].location = 1;

        attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT;

        attributeDescriptions[1].offset = offsetof(Vertex, color);


        return attributeDescriptions;

    }

};


const std::vector<Vertex> vertices = {

        {{0.0f, -0.5f}, {1.0f, 0.0f, 0.0f}},

        {{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}},

        {{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}

};


// Proxy function to create the debug messenger

VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) {

    auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");

    if (func != nullptr) {

        return func(instance, pCreateInfo, pAllocator, pDebugMessenger);

    } else {

        return VK_ERROR_EXTENSION_NOT_PRESENT;

    }

}


// Proxy function to destroy the debug messenger

void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) {

    auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");

    if (func != nullptr) {

        func(instance, debugMessenger, pAllocator);

    }

}


static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(

        VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,

        VkDebugUtilsMessageTypeFlagsEXT messageType,

        const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,

        void* pUserData) {


    int priority = ANDROID_LOG_DEBUG;

    if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) {

        priority = ANDROID_LOG_ERROR;

    } else if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {

        priority = ANDROID_LOG_WARN;

    } else if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT) {

        priority = ANDROID_LOG_INFO;

    }


    __android_log_print(priority, "VulkanValidation", "%s", pCallbackData->pMessage);


    return VK_FALSE;

}


// --- Vulkan State ---

struct VulkanState {

    VkInstance instance;

    VkDebugUtilsMessengerEXT debugMessenger;

    VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;

    VkDevice device;

    VkQueue graphicsQueue;

    VkSurfaceKHR surface;

    VkSwapchainKHR swapchain;

    VkFormat swapchainImageFormat;

    VkExtent2D swapchainExtent;

    std::vector<VkImage> swapchainImages;

    std::vector<VkImageView> swapchainImageViews;

    VkRenderPass renderPass;

    VkPipelineLayout pipelineLayout;

    VkPipeline graphicsPipeline;

    std::vector<VkFramebuffer> swapchainFramebuffers;

    VkCommandPool commandPool;

    std::vector<VkCommandBuffer> commandBuffers;


    // Sync objects

    std::vector<VkSemaphore> imageAvailableSemaphores;

    std::vector<VkSemaphore> renderFinishedSemaphores;

    std::vector<VkFence> inFlightFences;


    // Buffer objects

    VkBuffer vertexBuffer;

    VkDeviceMemory vertexBufferMemory;


    // Helper to hold reference to AssetManager

    AAssetManager* assetManager = nullptr;


    int MAX_FRAMES_IN_FLIGHT = 2;

    uint32_t currentFrame = 0;

    bool initialized = false;

};


VulkanState vk;


// --- Helper Functions ---


void checkVk(VkResult result, const char* msg) {

    if (result != VK_SUCCESS) {

        LOGE("Vulkan Error: %s code: %d", msg, result);

        throw std::runtime_error(msg);

    }

}


// Function to load shader binary from Assets

std::vector<uint32_t> loadShaderFile(const char* fileName) {

    if (!vk.assetManager) {

        LOGE("AssetManager is not initialized!");

        throw std::runtime_error("AssetManager not initialized");

    }


    AAsset* file = AAssetManager_open(vk.assetManager, fileName, AASSET_MODE_BUFFER);

    if (!file) {

        LOGE("Failed to open shader file from assets: %s", fileName);

        throw std::runtime_error("Failed to open shader file");

    }


    size_t length = AAsset_getLength(file);

    std::vector<uint32_t> buffer(length / sizeof(uint32_t));


    AAsset_read(file, buffer.data(), length);

    AAsset_close(file);


    LOGI("Loaded shader: %s (Size: %zu bytes)", fileName, length);

    return buffer;

}


VkShaderModule createShaderModule(const std::vector<uint32_t>& code) {

    VkShaderModuleCreateInfo createInfo{};

    createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;

    createInfo.codeSize = code.size() * sizeof(uint32_t);

    createInfo.pCode = code.data();


    VkShaderModule shaderModule;

    checkVk(vkCreateShaderModule(vk.device, &createInfo, nullptr, &shaderModule), "Failed to create shader module");

    return shaderModule;

}


// Helper to find memory type

uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) {

    VkPhysicalDeviceMemoryProperties memProperties;

    vkGetPhysicalDeviceMemoryProperties(vk.physicalDevice, &memProperties);


    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!");

}


// Helper to create a buffer

void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) {

    VkBufferCreateInfo bufferInfo{};

    bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;

    bufferInfo.size = size;

    bufferInfo.usage = usage;

    bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;


    checkVk(vkCreateBuffer(vk.device, &bufferInfo, nullptr, &buffer), "Failed to create buffer");


    VkMemoryRequirements memRequirements;

    vkGetBufferMemoryRequirements(vk.device, buffer, &memRequirements);


    VkMemoryAllocateInfo allocInfo{};

    allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;

    allocInfo.allocationSize = memRequirements.size;

    allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);


    checkVk(vkAllocateMemory(vk.device, &allocInfo, nullptr, &bufferMemory), "Failed to allocate buffer memory");


    vkBindBufferMemory(vk.device, buffer, bufferMemory, 0);

}


// 1. Create Instance

void createInstance() {

    VkApplicationInfo appInfo{};

    appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;

    appInfo.pApplicationName = "Hello Triangle";

    appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);

    appInfo.pEngineName = "No Engine";

    appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);

    appInfo.apiVersion = VK_API_VERSION_1_1;


    std::vector<const char*> extensions = {

            "VK_KHR_surface",

            "VK_KHR_android_surface"

    };


    if (enableValidationLayers) {

        extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);

    }


    VkInstanceCreateInfo createInfo{};

    createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;

    createInfo.pApplicationInfo = &appInfo;

    createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());

    createInfo.ppEnabledExtensionNames = extensions.data();


    VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};

    if (enableValidationLayers) {

        createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());

        createInfo.ppEnabledLayerNames = validationLayers.data();


        debugCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;

        debugCreateInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;

        debugCreateInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;

        debugCreateInfo.pfnUserCallback = debugCallback;

        createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &debugCreateInfo;

    } else {

        createInfo.enabledLayerCount = 0;

        createInfo.pNext = nullptr;

    }


    checkVk(vkCreateInstance(&createInfo, nullptr, &vk.instance), "Failed to create instance");


    if (enableValidationLayers) {

        checkVk(CreateDebugUtilsMessengerEXT(vk.instance, &debugCreateInfo, nullptr, &vk.debugMessenger), "Failed to setup debug messenger");

    }

}


// 2. Pick Physical Device & Create Logical Device

void createDevice() {

    uint32_t deviceCount = 0;

    vkEnumeratePhysicalDevices(vk.instance, &deviceCount, nullptr);

    if (deviceCount == 0) throw std::runtime_error("No Vulkan devices found");


    std::vector<VkPhysicalDevice> devices(deviceCount);

    vkEnumeratePhysicalDevices(vk.instance, &deviceCount, devices.data());


    vk.physicalDevice = devices[0];


    float queuePriority = 1.0f;

    VkDeviceQueueCreateInfo queueCreateInfo{};

    queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;

    queueCreateInfo.queueFamilyIndex = 0;

    queueCreateInfo.queueCount = 1;

    queueCreateInfo.pQueuePriorities = &queuePriority;


    std::vector<const char*> deviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };


    VkDeviceCreateInfo createInfo{};

    createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

    createInfo.pQueueCreateInfos = &queueCreateInfo;

    createInfo.queueCreateInfoCount = 1;

    createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());

    createInfo.ppEnabledExtensionNames = deviceExtensions.data();


    if (enableValidationLayers) {

        createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());

        createInfo.ppEnabledLayerNames = validationLayers.data();

    } else {

        createInfo.enabledLayerCount = 0;

    }


    checkVk(vkCreateDevice(vk.physicalDevice, &createInfo, nullptr, &vk.device), "Failed to create logical device");

    vkGetDeviceQueue(vk.device, 0, 0, &vk.graphicsQueue);

}


// 3. Swapchain

void createSwapchain(uint32_t width, uint32_t height) {

    VkSurfaceCapabilitiesKHR capabilities;

    vkGetPhysicalDeviceSurfaceCapabilitiesKHR(vk.physicalDevice, vk.surface, &capabilities);


    VkExtent2D extent = capabilities.currentExtent;

    if (capabilities.currentExtent.width == UINT32_MAX) {

        extent.width = std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width);

        extent.height = std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height);

    }

    vk.swapchainExtent = extent;


    uint32_t formatCount;

    vkGetPhysicalDeviceSurfaceFormatsKHR(vk.physicalDevice, vk.surface, &formatCount, nullptr);

    std::vector<VkSurfaceFormatKHR> formats(formatCount);

    vkGetPhysicalDeviceSurfaceFormatsKHR(vk.physicalDevice, vk.surface, &formatCount, formats.data());


    vk.swapchainImageFormat = formats[0].format;

    VkColorSpaceKHR colorSpace = formats[0].colorSpace;

    for (const auto& availableFormat : formats) {

        if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {

            vk.swapchainImageFormat = availableFormat.format;

            colorSpace = availableFormat.colorSpace;

            break;

        }

    }


    VkSwapchainCreateInfoKHR createInfo{};

    createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;

    createInfo.surface = vk.surface;

    createInfo.minImageCount = capabilities.minImageCount + 1;

    if (capabilities.maxImageCount > 0 && createInfo.minImageCount > capabilities.maxImageCount) {

        createInfo.minImageCount = capabilities.maxImageCount;

    }

    createInfo.imageFormat = vk.swapchainImageFormat;

    createInfo.imageColorSpace = colorSpace;

    createInfo.imageExtent = vk.swapchainExtent;

    createInfo.imageArrayLayers = 1;

    createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;

    createInfo.preTransform = capabilities.currentTransform;

    createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR;

    createInfo.presentMode = VK_PRESENT_MODE_FIFO_KHR;

    createInfo.clipped = VK_TRUE;

    createInfo.oldSwapchain = VK_NULL_HANDLE;


    checkVk(vkCreateSwapchainKHR(vk.device, &createInfo, nullptr, &vk.swapchain), "Failed to create swapchain");


    uint32_t imageCount;

    vkGetSwapchainImagesKHR(vk.device, vk.swapchain, &imageCount, nullptr);

    vk.swapchainImages.resize(imageCount);

    vkGetSwapchainImagesKHR(vk.device, vk.swapchain, &imageCount, vk.swapchainImages.data());


    vk.swapchainImageViews.resize(imageCount);

    for (size_t i = 0; i < imageCount; i++) {

        VkImageViewCreateInfo viewInfo{};

        viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;

        viewInfo.image = vk.swapchainImages[i];

        viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;

        viewInfo.format = vk.swapchainImageFormat;

        viewInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;

        viewInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;

        viewInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;

        viewInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;

        viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;

        viewInfo.subresourceRange.baseMipLevel = 0;

        viewInfo.subresourceRange.levelCount = 1;

        viewInfo.subresourceRange.baseArrayLayer = 0;

        viewInfo.subresourceRange.layerCount = 1;


        checkVk(vkCreateImageView(vk.device, &viewInfo, nullptr, &vk.swapchainImageViews[i]), "Failed to create image view");

    }

}


// 4. Render Pass

void createRenderPass() {

    VkAttachmentDescription colorAttachment{};

    colorAttachment.format = vk.swapchainImageFormat;

    colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;

    colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;

    colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;

    colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;

    colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;

    colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;

    colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;


    VkAttachmentReference colorAttachmentRef{};

    colorAttachmentRef.attachment = 0;

    colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;


    VkSubpassDescription subpass{};

    subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;

    subpass.colorAttachmentCount = 1;

    subpass.pColorAttachments = &colorAttachmentRef;


    VkRenderPassCreateInfo renderPassInfo{};

    renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;

    renderPassInfo.attachmentCount = 1;

    renderPassInfo.pAttachments = &colorAttachment;

    renderPassInfo.subpassCount = 1;

    renderPassInfo.pSubpasses = &subpass;


    checkVk(vkCreateRenderPass(vk.device, &renderPassInfo, nullptr, &vk.renderPass), "Failed to create render pass");

}


// 5. Graphics Pipeline

void createGraphicsPipeline() {

    auto vertShaderCode = loadShaderFile("triangle.vert.spv");

    auto fragShaderCode = loadShaderFile("triangle.frag.spv");


    VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);

    VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);


    VkPipelineShaderStageCreateInfo vertShaderStageInfo{};

    vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;

    vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;

    vertShaderStageInfo.module = vertShaderModule;

    vertShaderStageInfo.pName = "main";


    VkPipelineShaderStageCreateInfo fragShaderStageInfo{};

    fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;

    fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;

    fragShaderStageInfo.module = fragShaderModule;

    fragShaderStageInfo.pName = "main";


    VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo};


    // FIX: Define Vertex Inputs

    VkPipelineVertexInputStateCreateInfo vertexInputInfo{};

    vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;


    auto bindingDescription = Vertex::getBindingDescription();

    auto attributeDescriptions = Vertex::getAttributeDescriptions();


    vertexInputInfo.vertexBindingDescriptionCount = 1;

    vertexInputInfo.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributeDescriptions.size());

    vertexInputInfo.pVertexBindingDescriptions = &bindingDescription;

    vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data();


    VkPipelineInputAssemblyStateCreateInfo inputAssembly{};

    inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;

    inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;

    inputAssembly.primitiveRestartEnable = VK_FALSE;


    VkViewport viewport{};

    viewport.x = 0.0f;

    viewport.y = 0.0f;

    viewport.width = (float) vk.swapchainExtent.width;

    viewport.height = (float) vk.swapchainExtent.height;

    viewport.minDepth = 0.0f;

    viewport.maxDepth = 1.0f;


    VkRect2D scissor{};

    scissor.offset = {0, 0};

    scissor.extent = vk.swapchainExtent;


    VkPipelineViewportStateCreateInfo viewportState{};

    viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;

    viewportState.viewportCount = 1;

    viewportState.pViewports = &viewport;

    viewportState.scissorCount = 1;

    viewportState.pScissors = &scissor;


    VkPipelineRasterizationStateCreateInfo rasterizer{};

    rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;

    rasterizer.depthClampEnable = VK_FALSE;

    rasterizer.rasterizerDiscardEnable = VK_FALSE;

    rasterizer.polygonMode = VK_POLYGON_MODE_FILL;

    rasterizer.lineWidth = 1.0f;

    rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;

    rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;


    VkPipelineMultisampleStateCreateInfo multisampling{};

    multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;

    multisampling.sampleShadingEnable = VK_FALSE;

    multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;


    VkPipelineColorBlendAttachmentState colorBlendAttachment{};

    colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;

    colorBlendAttachment.blendEnable = VK_FALSE;


    VkPipelineColorBlendStateCreateInfo colorBlending{};

    colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;

    colorBlending.logicOpEnable = VK_FALSE;

    colorBlending.attachmentCount = 1;

    colorBlending.pAttachments = &colorBlendAttachment;


    VkPipelineLayoutCreateInfo pipelineLayoutInfo{};

    pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;


    checkVk(vkCreatePipelineLayout(vk.device, &pipelineLayoutInfo, nullptr, &vk.pipelineLayout), "Failed to create pipeline layout");


    VkGraphicsPipelineCreateInfo pipelineInfo{};

    pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;

    pipelineInfo.stageCount = 2;

    pipelineInfo.pStages = shaderStages;

    pipelineInfo.pVertexInputState = &vertexInputInfo;

    pipelineInfo.pInputAssemblyState = &inputAssembly;

    pipelineInfo.pViewportState = &viewportState;

    pipelineInfo.pRasterizationState = &rasterizer;

    pipelineInfo.pMultisampleState = &multisampling;

    pipelineInfo.pColorBlendState = &colorBlending;

    pipelineInfo.layout = vk.pipelineLayout;

    pipelineInfo.renderPass = vk.renderPass;

    pipelineInfo.subpass = 0;


    checkVk(vkCreateGraphicsPipelines(vk.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &vk.graphicsPipeline), "Failed to create graphics pipeline");


    vkDestroyShaderModule(vk.device, fragShaderModule, nullptr);

    vkDestroyShaderModule(vk.device, vertShaderModule, nullptr);

}


// 6. Framebuffers

void createFramebuffers() {

    vk.swapchainFramebuffers.resize(vk.swapchainImageViews.size());

    for (size_t i = 0; i < vk.swapchainImageViews.size(); i++) {

        VkImageView attachments[] = {vk.swapchainImageViews[i]};


        VkFramebufferCreateInfo framebufferInfo{};

        framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;

        framebufferInfo.renderPass = vk.renderPass;

        framebufferInfo.attachmentCount = 1;

        framebufferInfo.pAttachments = attachments;

        framebufferInfo.width = vk.swapchainExtent.width;

        framebufferInfo.height = vk.swapchainExtent.height;

        framebufferInfo.layers = 1;


        checkVk(vkCreateFramebuffer(vk.device, &framebufferInfo, nullptr, &vk.swapchainFramebuffers[i]), "Failed to create framebuffer");

    }

}


// NEW: Create Vertex Buffer

void createVertexBuffer() {

    VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();


    VkBuffer stagingBuffer;

    VkDeviceMemory stagingBufferMemory;

    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);


    void* data;

    vkMapMemory(vk.device, stagingBufferMemory, 0, bufferSize, 0, &data);

    memcpy(data, vertices.data(), (size_t) bufferSize);

    vkUnmapMemory(vk.device, stagingBufferMemory);


    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vk.vertexBuffer, vk.vertexBufferMemory);


    // Immediate submit copy (simplified for brevity)

    VkCommandBufferAllocateInfo allocInfo{};

    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;

    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;

    allocInfo.commandPool = vk.commandPool;

    allocInfo.commandBufferCount = 1;


    VkCommandBuffer commandBuffer;

    vkAllocateCommandBuffers(vk.device, &allocInfo, &commandBuffer);


    VkCommandBufferBeginInfo beginInfo{};

    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;

    beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

    vkBeginCommandBuffer(commandBuffer, &beginInfo);


    VkBufferCopy copyRegion{};

    copyRegion.size = bufferSize;

    vkCmdCopyBuffer(commandBuffer, stagingBuffer, vk.vertexBuffer, 1, &copyRegion);


    vkEndCommandBuffer(commandBuffer);


    VkSubmitInfo submitInfo{};

    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;

    submitInfo.commandBufferCount = 1;

    submitInfo.pCommandBuffers = &commandBuffer;


    vkQueueSubmit(vk.graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);

    vkQueueWaitIdle(vk.graphicsQueue);


    vkFreeCommandBuffers(vk.device, vk.commandPool, 1, &commandBuffer);

    vkDestroyBuffer(vk.device, stagingBuffer, nullptr);

    vkFreeMemory(vk.device, stagingBufferMemory, nullptr);

}



// 7. Command Pools & Buffers

void createCommandPool() {

    VkCommandPoolCreateInfo poolInfo{};

    poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;

    poolInfo.queueFamilyIndex = 0;

    poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;

    checkVk(vkCreateCommandPool(vk.device, &poolInfo, nullptr, &vk.commandPool), "Failed to create command pool");


    vk.commandBuffers.resize(vk.MAX_FRAMES_IN_FLIGHT);

    VkCommandBufferAllocateInfo allocInfo{};

    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;

    allocInfo.commandPool = vk.commandPool;

    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;

    allocInfo.commandBufferCount = (uint32_t) vk.commandBuffers.size();


    checkVk(vkAllocateCommandBuffers(vk.device, &allocInfo, vk.commandBuffers.data()), "Failed to alloc cmd buffers");

}


void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) {

    VkCommandBufferBeginInfo beginInfo{};

    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;


    checkVk(vkBeginCommandBuffer(commandBuffer, &beginInfo), "Failed to begin recording");


    VkRenderPassBeginInfo renderPassInfo{};

    renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;

    renderPassInfo.renderPass = vk.renderPass;

    renderPassInfo.framebuffer = vk.swapchainFramebuffers[imageIndex];

    renderPassInfo.renderArea.offset = {0, 0};

    renderPassInfo.renderArea.extent = vk.swapchainExtent;


    VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}};

    renderPassInfo.clearValueCount = 1;

    renderPassInfo.pClearValues = &clearColor;


    vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);

    vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, vk.graphicsPipeline);


    // FIX: Bind vertex buffer

    VkBuffer vertexBuffers[] = {vk.vertexBuffer};

    VkDeviceSize offsets[] = {0};

    vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);


    vkCmdDraw(commandBuffer, static_cast<uint32_t>(vertices.size()), 1, 0, 0);

    vkCmdEndRenderPass(commandBuffer);


    checkVk(vkEndCommandBuffer(commandBuffer), "Failed to record");

}


// 8. Sync Objects

void createSyncObjects() {

    vk.imageAvailableSemaphores.resize(vk.MAX_FRAMES_IN_FLIGHT);

    vk.renderFinishedSemaphores.resize(vk.MAX_FRAMES_IN_FLIGHT);

    vk.inFlightFences.resize(vk.MAX_FRAMES_IN_FLIGHT);


    VkSemaphoreCreateInfo semaphoreInfo{};

    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;


    VkFenceCreateInfo fenceInfo{};

    fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;

    fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;


    for (size_t i = 0; i < vk.MAX_FRAMES_IN_FLIGHT; i++) {

        vkCreateSemaphore(vk.device, &semaphoreInfo, nullptr, &vk.imageAvailableSemaphores[i]);

        vkCreateSemaphore(vk.device, &semaphoreInfo, nullptr, &vk.renderFinishedSemaphores[i]);

        vkCreateFence(vk.device, &fenceInfo, nullptr, &vk.inFlightFences[i]);

    }

}


// --- JNI Exported Functions ---


extern "C" JNIEXPORT void JNICALL

Java_com_cixiidae_triangle_MainActivity_initVulkan(JNIEnv* env, jobject thiz, jobject surface) {

    std::lock_guard<std::mutex> lock(appMutex);

    if (vk.initialized) return;


    jclass activityClass = env->GetObjectClass(thiz);

    jmethodID getAssetsMethod = env->GetMethodID(activityClass, "getAssets", "()Landroid/content/res/AssetManager;");

    if (getAssetsMethod == nullptr) {

        LOGE("Could not find getAssets method!");

        return;

    }

    jobject assetManagerObj = env->CallObjectMethod(thiz, getAssetsMethod);

    vk.assetManager = AAssetManager_fromJava(env, assetManagerObj);


    if (!vk.assetManager) {

        LOGE("Could not get AssetManager");

        return;

    }


    createInstance();


    ANativeWindow* window = ANativeWindow_fromSurface(env, surface);

    VkAndroidSurfaceCreateInfoKHR createInfo{};

    createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;

    createInfo.window = window;

    checkVk(vkCreateAndroidSurfaceKHR(vk.instance, &createInfo, nullptr, &vk.surface), "Failed to create window surface");


    createDevice();

    int width = ANativeWindow_getWidth(window);

    int height = ANativeWindow_getHeight(window);

    createSwapchain(width, height);

    createRenderPass();

    createCommandPool(); // Create Command Pool before buffer

    createVertexBuffer(); // NOW: Create Buffer before Pipeline is fine, but needs command pool

    createGraphicsPipeline();

    createFramebuffers();

    createSyncObjects();


    vk.initialized = true;

    LOGI("Vulkan Initialized");

}


extern "C" JNIEXPORT void JNICALL

Java_com_cixiidae_triangle_MainActivity_renderFrame(JNIEnv*, jobject) {

    std::lock_guard<std::mutex> lock(appMutex);

    if (!vk.initialized) return;


    vkWaitForFences(vk.device, 1, &vk.inFlightFences[vk.currentFrame], VK_TRUE, UINT64_MAX);


    uint32_t imageIndex;

    VkResult result = vkAcquireNextImageKHR(vk.device, vk.swapchain, UINT64_MAX, vk.imageAvailableSemaphores[vk.currentFrame], VK_NULL_HANDLE, &imageIndex);


    if (result == VK_ERROR_OUT_OF_DATE_KHR) {

        return;

    } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {

        LOGE("Failed to acquire swapchain image");

        return;

    }


    vkResetFences(vk.device, 1, &vk.inFlightFences[vk.currentFrame]);


    vkResetCommandBuffer(vk.commandBuffers[vk.currentFrame], 0);

    recordCommandBuffer(vk.commandBuffers[vk.currentFrame], imageIndex);


    VkSubmitInfo submitInfo{};

    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;


    VkSemaphore waitSemaphores[] = {vk.imageAvailableSemaphores[vk.currentFrame]};

    VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};

    submitInfo.waitSemaphoreCount = 1;

    submitInfo.pWaitSemaphores = waitSemaphores;

    submitInfo.pWaitDstStageMask = waitStages;

    submitInfo.commandBufferCount = 1;

    submitInfo.pCommandBuffers = &vk.commandBuffers[vk.currentFrame];


    VkSemaphore signalSemaphores[] = {vk.renderFinishedSemaphores[vk.currentFrame]};

    submitInfo.signalSemaphoreCount = 1;

    submitInfo.pSignalSemaphores = signalSemaphores;


    checkVk(vkQueueSubmit(vk.graphicsQueue, 1, &submitInfo, vk.inFlightFences[vk.currentFrame]), "Failed to submit draw");


    VkPresentInfoKHR presentInfo{};

    presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;

    presentInfo.waitSemaphoreCount = 1;

    presentInfo.pWaitSemaphores = signalSemaphores;

    presentInfo.swapchainCount = 1;

    presentInfo.pSwapchains = &vk.swapchain;

    presentInfo.pImageIndices = &imageIndex;


    vkQueuePresentKHR(vk.graphicsQueue, &presentInfo);


    vk.currentFrame = (vk.currentFrame + 1) % vk.MAX_FRAMES_IN_FLIGHT;

}


extern "C" JNIEXPORT void JNICALL

Java_com_cixiidae_triangle_MainActivity_resizeVulkan(JNIEnv*, jobject, jint width, jint height) {

    std::lock_guard<std::mutex> lock(appMutex);

    if (!vk.initialized) return;

    vkDeviceWaitIdle(vk.device);


    for (auto fb : vk.swapchainFramebuffers) vkDestroyFramebuffer(vk.device, fb, nullptr);

    for (auto iv : vk.swapchainImageViews) vkDestroyImageView(vk.device, iv, nullptr);

    vkDestroySwapchainKHR(vk.device, vk.swapchain, nullptr);


    createSwapchain(width, height);

    createFramebuffers();

}


extern "C" JNIEXPORT void JNICALL

Java_com_cixiidae_triangle_MainActivity_cleanupVulkan(JNIEnv*, jobject) {

    std::lock_guard<std::mutex> lock(appMutex);

    if (!vk.initialized) return;

    vkDeviceWaitIdle(vk.device);


    if (enableValidationLayers) {

        DestroyDebugUtilsMessengerEXT(vk.instance, vk.debugMessenger, nullptr);

    }


    // Cleanup Buffer

    vkDestroyBuffer(vk.device, vk.vertexBuffer, nullptr);

    vkFreeMemory(vk.device, vk.vertexBufferMemory, nullptr);


    vkDestroyCommandPool(vk.device, vk.commandPool, nullptr);

    for (auto fb : vk.swapchainFramebuffers) vkDestroyFramebuffer(vk.device, fb, nullptr);

    vkDestroyPipeline(vk.device, vk.graphicsPipeline, nullptr);

    vkDestroyPipelineLayout(vk.device, vk.pipelineLayout, nullptr);

    vkDestroyRenderPass(vk.device, vk.renderPass, nullptr);

    for (auto iv : vk.swapchainImageViews) vkDestroyImageView(vk.device, iv, nullptr);

    vkDestroySwapchainKHR(vk.device, vk.swapchain, nullptr);

    for (size_t i = 0; i < vk.MAX_FRAMES_IN_FLIGHT; i++) {

        vkDestroySemaphore(vk.device, vk.renderFinishedSemaphores[i], nullptr);

        vkDestroySemaphore(vk.device, vk.imageAvailableSemaphores[i], nullptr);

        vkDestroyFence(vk.device, vk.inFlightFences[i], nullptr);

    }

    vkDestroyDevice(vk.device, nullptr);

    vkDestroySurfaceKHR(vk.instance, vk.surface, nullptr);

    vkDestroyInstance(vk.instance, nullptr);


    vk.initialized = false;

    LOGI("Vulkan Cleaned up");

}


MainActivity.kt

package com.cixiidae.triangle


import android.os.Bundle

import android.view.Surface

import android.view.SurfaceHolder

import android.view.SurfaceView

import androidx.activity.ComponentActivity

import androidx.activity.compose.setContent

import androidx.compose.runtime.Composable

import androidx.compose.ui.viewinterop.AndroidView


class MainActivity : ComponentActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContent {

            // Use Jetpack Compose to host the native SurfaceView

            VulkanScreen(activity = this)

        }

    }


    // Native methods

    external fun initVulkan(surface: Surface)

    external fun resizeVulkan(width: Int, height: Int)

    external fun renderFrame()

    external fun cleanupVulkan()


    companion object {

        init {

            System.loadLibrary("triangle")

        }

    }

}


@Composable

fun VulkanScreen(activity: MainActivity) {

    AndroidView(

        factory = { context ->

            SurfaceView(context).apply {

                holder.addCallback(object : SurfaceHolder.Callback {

                    override fun surfaceCreated(holder: SurfaceHolder) {

                        // Initialize Vulkan

                        activity.initVulkan(holder.surface)


                        // Start render loop in a separate thread

                        Thread {

                            while (holder.surface.isValid) {

                                activity.renderFrame()

                            }

                        }.start()

                    }


                    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {

                        activity.resizeVulkan(width, height)

                    }


                    override fun surfaceDestroyed(holder: SurfaceHolder) {

                        activity.cleanupVulkan()

                    }

                })

            }

        }

    )

}

Credits and Licensing

This tutorial, "How to Make Vulkan Triangle,"  utilizes several industry-standard components and specific configurations to ensure compatibility with modern Android environments:



Vulkan Validation Layers: The Android validation layer binaries used in this project were sourced from the official KhronosGroup GitHub releases. These layers are essential for debugging and identifying why specific Vulkan calls may fail.

+1



Shaders: The vertex and fragment shader source files were obtained from the KhronosGroup Vulkan-Samples repository.



Development Environment: * The project is built using Native C++ with the C++ 17 standard.

+2


It utilizes NDK version 27.0.12077973.


The configuration is set to API 33 to ensure compatibility with devices such as Chromebooks.



Modern Android Support: * This guide includes specific compiler flags (-Wl,-z,max-page-size=16384) to support 16kb page sizes.

+1


The user interface is managed through Jetpack Compose, which hosts the native SurfaceView.

+1



Implementation Notes: The included code features specific fixes for Android-specific structures, such as the mandatory definition of VK_USE_PLATFORM_ANDROID_KHR before including the Vulkan headers.


AI Disclosure: This tutorial and the associated code were developed with the assistance of artificial intelligence.


THE SOFTWARE AND INFORMATION ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.