這篇的目標很單純:把一個可以跑起來的 Vulkan 專案骨架搭好,並且你能看懂每一步在幹嘛。
心法口訣:先能顯示器開張(視窗/交換鏈),再把顯卡叫醒(Instance/Device/Queue),最後排好畫面流程(Swapchain 及命令)。
glslc
編譯器。VkSurfaceKHR
。VulkanHello/
├─ CMakeLists.txt
├─ src/
│ └─ main.cpp
└─ shaders/
├─ triangle.vert
└─ triangle.frag
main.cpp
:放初始化骨架(本篇提供極簡版)。shaders
:放 GLSL 檔,之後用 glslc
轉 SPIR-V。先求「能跑」,之後再把每個步驟抽成類別/模組。
[建立視窗] GLFW → VkSurfaceKHR
│
[VkInstance](+ Validation Layers + Debug Messenger)
│
[選擇實體顯卡 VkPhysicalDevice](支援圖形 & 呈現 Queue)
│
[建立 VkDevice + 取出 VkQueue(s)]
│
[Swapchain](格式、尺寸、影像數)
│
[Image Views](讓影像能被當附件使用)
│
[Command Pool/Buffer](錄命令)
│
[同步物件](Semaphore/Fence)
│
主迴圈:Acquire → Record/Submit → Present
cmake_minimum_required(VERSION 3.16)
project(VulkanHello CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(Vulkan REQUIRED)
include(FetchContent)
# Define GLFW dependency
FetchContent_Declare(
glfw
GIT_REPOSITORY https://github.com/glfw/glfw.git
GIT_TAG latest
)
# Make GLFW available
FetchContent_MakeAvailable(glfw)
add_executable(VulkanHello src/main.cpp)
target_link_libraries(VulkanHello PRIVATE Vulkan::Vulkan glfw)
main.cpp
(骨架順序清楚、可逐步填)說明:
- 我把每個步驟包成小函式,先只做關鍵;
- 程式可編到「建立 Instance / Device / Swapchain」;
- 真正畫三角形要加 Render Pass / Pipeline,這留給你 Day 12。
- 註解會明講為什麼與什麼時候要回頭補。
// src/main.cpp
#include <vulkan/vulkan.h>
#include <GLFW/glfw3.h>
#include <vector>
#include <cstring>
#include <stdexcept>
#include <iostream>
#define VK_CHECK(x) do{ VkResult r=(x); if(r!=VK_SUCCESS) throw std::runtime_error("VkResult != VK_SUCCESS"); }while(0)
struct App {
// --- 視窗/核心 ---
GLFWwindow* window = nullptr;
VkInstance instance = VK_NULL_HANDLE;
VkSurfaceKHR surface = VK_NULL_HANDLE;
VkPhysicalDevice phys = VK_NULL_HANDLE;
VkDevice device = VK_NULL_HANDLE;
uint32_t qGraphics= 0, qPresent=0;
VkQueue graphicsQ= VK_NULL_HANDLE, presentQ=VK_NULL_HANDLE;
// --- 交換鏈 ---
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
VkFormat swapFormat{};
VkExtent2D swapExtent{};
std::vector<VkImage> swapImages;
std::vector<VkImageView> swapViews;
// (Day 12 再加:RenderPass/Pipeline/CommandPool/CmdBuf/Semaphores/Fences)
// ----------------- 入口 -----------------
void run(){
initWindow();
createInstance();
createSurface();
pickPhysicalDevice();
createDeviceAndQueues();
createSwapchainAndViews();
mainLoop(); // 目前只是空轉顯示視窗
cleanup();
}
// ------------- 視窗/Instance -------------
void initWindow(){
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); // 不要 OpenGL
window = glfwCreateWindow(1280, 720, "Vulkan Hello", nullptr, nullptr);
}
void createInstance(){
// 啟用驗證層(Debug build)
const char* layers[] = { "VK_LAYER_KHRONOS_validation" };
// GLFW 要求的 Surface 擴充
uint32_t extCount=0; const char** glfwExt = glfwGetRequiredInstanceExtensions(&extCount);
VkApplicationInfo app{ VK_STRUCTURE_TYPE_APPLICATION_INFO };
app.pApplicationName = "VulkanHello";
app.applicationVersion = VK_MAKE_VERSION(1,0,0);
app.pEngineName = "NoEngine";
app.engineVersion = VK_MAKE_VERSION(1,0,0);
app.apiVersion = VK_API_VERSION_1_2;
VkInstanceCreateInfo ci{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO };
ci.pApplicationInfo = &app;
ci.enabledExtensionCount = extCount;
ci.ppEnabledExtensionNames= glfwExt;
#ifndef NDEBUG
ci.enabledLayerCount = 1;
ci.ppEnabledLayerNames = layers;
#endif
VK_CHECK(vkCreateInstance(&ci, nullptr, &instance));
}
void createSurface(){
// 由 GLFW 依平台幫你建 VkSurfaceKHR
VK_CHECK( (VkResult)glfwCreateWindowSurface(instance, window, nullptr, &surface) );
}
// ------------- 實體顯卡 & Queue -------------
void pickPhysicalDevice(){
uint32_t n=0; vkEnumeratePhysicalDevices(instance, &n, nullptr);
if(n==0) throw std::runtime_error("No Vulkan device");
std::vector<VkPhysicalDevice> devs(n);
vkEnumeratePhysicalDevices(instance, &n, devs.data());
// 簡單挑第一張支援圖形+呈現的卡
for(auto d: devs){
uint32_t qn=0; vkGetPhysicalDeviceQueueFamilyProperties(d, &qn, nullptr);
std::vector<VkQueueFamilyProperties> qfs(qn);
vkGetPhysicalDeviceQueueFamilyProperties(d, &qn, qfs.data());
int g=-1,p=-1;
for(uint32_t i=0;i<qn;i++){
if(qfs[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) g=i;
VkBool32 present=false;
vkGetPhysicalDeviceSurfaceSupportKHR(d, i, surface, &present);
if(present) p=i;
}
if(g!=-1 && p!=-1){ phys=d; qGraphics=g; qPresent=p; break; }
}
if(!phys) throw std::runtime_error("No suitable GPU");
}
void createDeviceAndQueues(){
float prio=1.0f;
std::vector<VkDeviceQueueCreateInfo> qCIs;
auto pushUniqueQueue = [&](uint32_t family){
for(auto& x:qCIs) if(x.queueFamilyIndex==family) return;
VkDeviceQueueCreateInfo q{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO };
q.queueFamilyIndex = family; q.queueCount=1; q.pQueuePriorities=&prio;
qCIs.push_back(q);
};
pushUniqueQueue(qGraphics);
pushUniqueQueue(qPresent);
const char* devExts[] = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
VkDeviceCreateInfo ci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO };
ci.queueCreateInfoCount = (uint32_t)qCIs.size();
ci.pQueueCreateInfos = qCIs.data();
ci.enabledExtensionCount = 1;
ci.ppEnabledExtensionNames = devExts;
VK_CHECK(vkCreateDevice(phys, &ci, nullptr, &device));
vkGetDeviceQueue(device, qGraphics, 0, &graphicsQ);
vkGetDeviceQueue(device, qPresent , 0, &presentQ );
}
// ------------- 交換鏈(格式/尺寸/影像/檢視)-------------
void createSwapchainAndViews(){
// 查詢能力
VkSurfaceCapabilitiesKHR caps; vkGetPhysicalDeviceSurfaceCapabilitiesKHR(phys, surface, &caps);
uint32_t nFmt=0; vkGetPhysicalDeviceSurfaceFormatsKHR(phys, surface, &nFmt, nullptr);
std::vector<VkSurfaceFormatKHR> fmts(nFmt);
vkGetPhysicalDeviceSurfaceFormatsKHR(phys, surface, &nFmt, fmts.data());
uint32_t nMode=0; vkGetPhysicalDeviceSurfacePresentModesKHR(phys, surface, &nMode, nullptr);
std::vector<VkPresentModeKHR> modes(nMode);
vkGetPhysicalDeviceSurfacePresentModesKHR(phys, surface, &nMode, modes.data());
// 選常見格式(若只有 VK_FORMAT_UNDEFINED 代表任意)
VkSurfaceFormatKHR chosenFmt = fmts[0];
for(auto f: fmts){
if(f.format==VK_FORMAT_B8G8R8A8_UNORM && f.colorSpace==VK_COLOR_SPACE_SRGB_NONLINEAR_KHR){ chosenFmt=f; break; }
}
swapFormat = chosenFmt.format;
// 視窗大小
int w,h; glfwGetFramebufferSize(window, &w, &h);
swapExtent = { (uint32_t)w, (uint32_t)h };
if(caps.currentExtent.width != UINT32_MAX) swapExtent = caps.currentExtent;
// 影像數(至少 min+1,最多 max;0 表示沒上限)
uint32_t imageCount = caps.minImageCount + 1;
if(caps.maxImageCount>0 && imageCount>caps.maxImageCount) imageCount=caps.maxImageCount;
// 建 swapchain
VkSwapchainCreateInfoKHR ci{ VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR };
ci.surface = surface;
ci.minImageCount = imageCount;
ci.imageFormat = swapFormat;
ci.imageColorSpace = chosenFmt.colorSpace;
ci.imageExtent = swapExtent;
ci.imageArrayLayers = 1;
ci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
// 佈局:若圖形與呈現不同 Queue,要設定共享模式
uint32_t qIdx[2] = { qGraphics, qPresent };
if(qGraphics != qPresent){
ci.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
ci.queueFamilyIndexCount = 2;
ci.pQueueFamilyIndices = qIdx;
}else{
ci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
}
ci.preTransform = caps.currentTransform;
ci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
// 呈現模式:乖乖用 FIFO(VSync),相容最好
ci.presentMode = VK_PRESENT_MODE_FIFO_KHR;
ci.clipped = VK_TRUE;
VK_CHECK(vkCreateSwapchainKHR(device, &ci, nullptr, &swapchain));
// 取出影像 & 建 ImageView
uint32_t nImg=0; vkGetSwapchainImagesKHR(device, swapchain, &nImg, nullptr);
swapImages.resize(nImg);
vkGetSwapchainImagesKHR(device, swapchain, &nImg, swapImages.data());
swapViews.resize(nImg);
for(uint32_t i=0;i<nImg;i++){
VkImageViewCreateInfo vi{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
vi.image = swapImages[i];
vi.viewType = VK_IMAGE_VIEW_TYPE_2D;
vi.format = swapFormat;
vi.components = { VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY };
vi.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
vi.subresourceRange.levelCount=1; vi.subresourceRange.layerCount=1;
VK_CHECK(vkCreateImageView(device, &vi, nullptr, &swapViews[i]));
}
}
// ---------------- 主迴圈 / 清理 ----------------
void mainLoop(){
while(!glfwWindowShouldClose(window)){
glfwPollEvents();
// Day 12:在這裡 Acquire → Record(清畫面/畫三角形)→ Submit → Present
}
vkDeviceWaitIdle(device);
}
void cleanup(){
for(auto v:swapViews) vkDestroyImageView(device, v, nullptr);
if(swapchain) vkDestroySwapchainKHR(device, swapchain, nullptr);
if(device) vkDestroyDevice(device, nullptr);
if(surface) vkDestroySurfaceKHR(instance, surface, nullptr);
if(instance) vkDestroyInstance(instance, nullptr);
if(window){ glfwDestroyWindow(window); glfwTerminate(); }
}
};
int main(){
try{ App().run(); }
catch(const std::exception& e){ std::cerr << e.what() << "\n"; return 1; }
return 0;
}
成果:空白的視窗
這支程式完成了:視窗 → Instance → Surface → 挑顯卡/Queue → Device → Swapchain/ImageView。
下一步(Day 12):加 Command Pool/Buffer、Render Pass(或 Dynamic Rendering)、Pipeline、同步物件,就能清畫面或畫三角形了。
幫你把 Vulkan 錯誤直接印在 Console。只在 Debug 開就好。
VK_EXT_debug_utils
,並建立 VkDebugUtilsMessengerEXT
。(為了保「程式碼精簡」,上面的 sample 省略;實務上你建立 Instance 時就把它接上。)
VK_ERROR_LAYER_NOT_PRESENT
VK_LAYER_KHRONOS_validation
但沒裝 SDK。→ 裝 LunarG SDK 或 Debug 模式才開層。VK_ERROR_EXTENSION_NOT_PRESENT
glfwGetRequiredInstanceExtensions
)。Swapchain 格式/尺寸不合
caps.currentExtent
與表面格式,不要自己硬填。呈現黑畫面(Day 12)
執行緒卡住
vkDeviceWaitIdle
前要等所有提交完成,或 Fence 沒有等待/重設。Vulkan 初始化的本質:
把視窗弄好 → 把顯卡初始化(Instance/Device/Queue)→ 把交換鏈與影像檢視備好 → (下一步)錄命令並提交。
一次只加一個階段,配合 Validation Layers,你會很穩地走到能畫第一張圖的那一步。