How to Make Vulkan Triangle
Make a new Project
Native C++
API 33 (to work on my chromebook)
Kotlin DSL
C++ 17
Update Build Gradle (Module :app)
Add Pugins id("org.jetbrains.kotlin.plugin.compose") version "2.3.0"
Add ndkVersion = "27.0.12077973"
For 16kb pages:
externalNativeBuild {
cmake {
cppFlags += "-std=c++17"
cppFlags += "-Wl,-z,max-page-size=16384"
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
Delete Kotlin Options
kotlinOptions {
jvmTarget = "11"
}
Update Build Features:
buildFeatures {
compose = true
prefab = true
viewBinding = true
}
Add compiler options
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8)
}
}
Replace Dependencies
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation("androidx.activity:activity-compose:1.12.2")
implementation(platform("androidx.compose:compose-bom:2026.01.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.games:games-activity:4.0.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2026.01.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
Add Validation Layer
This tells you why it didn’t work.
Go to git hub and download the Android Validation Layer binaries. I went to https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases
Make a new directory named src/main/jniLibs or just make it in the file manager.
Copy the four folders you downloaded into it.
The zip file has 4 cpu types in it; copy the four folders into the new folder.
Now make a new folder named assets, src/main/assets
This is where the shaders go.
I got the shader from https://github.com/KhronosGroup/Vulkan-Samples/tree/main/shaders/hello_triangle/glsl
There you need the 2 spv files; copy these into the assets folder you just made.
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, ©Region);
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.