iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
Software Development

渲染與GPU編程系列 第 12

Day 11|Vulkan 專案骨架與初始化(零基礎友善版|程式碼精簡)

  • 分享至 

  • xImage
  •  

這篇的目標很單純:把一個可以跑起來的 Vulkan 專案骨架搭好,並且你能看懂每一步在幹嘛。
心法口訣:先能顯示器開張(視窗/交換鏈),再把顯卡叫醒(Instance/Device/Queue),最後排好畫面流程(Swapchain 及命令)


0)你需要準備什麼?(一次說清)

  • Vulkan SDK(LunarG):提供標頭檔、工具、驗證層(Validation Layers)、glslc 編譯器。
  • 視窗庫(建議 GLFW):幫你建立視窗 & 建 VkSurfaceKHR
  • (macOS/iOS)MoltenVK:把 Vulkan 呼叫轉到 Metal。專案仍用 Vulkan API。
  • 除錯工具:開 Validation Layers + RenderDoc(之後你會愛上它)。

1)資料夾骨架(最小可讀)

VulkanHello/
├─ CMakeLists.txt
├─ src/
│  └─ main.cpp
└─ shaders/
   ├─ triangle.vert
   └─ triangle.frag
  • main.cpp:放初始化骨架(本篇提供極簡版)。
  • shaders:放 GLSL 檔,之後用 glslcSPIR-V
  • CMake:幫你找 Vulkan/GLFW 並編譯。

先求「能跑」,之後再把每個步驟抽成類別/模組。


2)初始化全流程地圖(先有大圖,細節才不迷路)

[建立視窗]  GLFW → VkSurfaceKHR
      │
[VkInstance](+ Validation Layers + Debug Messenger)
      │
[選擇實體顯卡 VkPhysicalDevice](支援圖形 & 呈現 Queue)
      │
[建立 VkDevice + 取出 VkQueue(s)]
      │
[Swapchain](格式、尺寸、影像數)
      │
[Image Views](讓影像能被當附件使用)
      │
[Command Pool/Buffer](錄命令)
      │
[同步物件](Semaphore/Fence)
      │
主迴圈:Acquire → Record/Submit → Present

3)三個「不會崩潰」原則(超重要)

  1. 先開 Validation Layers:錯就讓它大喊,不要悶著錯。
  2. 每個 Vulkan 物件建立/銷毀成對:誰 create 誰 destroy,程式關掉要乾淨。
  3. 每一步只做最小事:先跑起 視窗 + Instance + Device + Swapchain 就很棒;之後再加 Render Pass / Pipeline / Depth。

4)最小 CMake(簡潔)

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)


5)超精簡 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;
}

成果:空白的視窗
window

這支程式完成了:視窗 → Instance → Surface → 挑顯卡/Queue → Device → Swapchain/ImageView
下一步(Day 12):加 Command Pool/Buffer、Render Pass(或 Dynamic Rendering)、Pipeline、同步物件,就能清畫面或畫三角形了。


6)Debug Messenger(建議立刻開)

幫你把 Vulkan 錯誤直接印在 Console。只在 Debug 開就好。

  • Instance 時掛上 VK_EXT_debug_utils,並建立 VkDebugUtilsMessengerEXT
  • 每當你 barrier/同步/生命週期寫錯,它會直接吐人話(真的好用)。

(為了保「程式碼精簡」,上面的 sample 省略;實務上你建立 Instance 時就把它接上。)


7)你一定會遇到的「第一波錯誤」與快速修法

  1. VK_ERROR_LAYER_NOT_PRESENT

    • 你開了 VK_LAYER_KHRONOS_validation 但沒裝 SDK。→ 裝 LunarG SDK 或 Debug 模式才開層。
  2. VK_ERROR_EXTENSION_NOT_PRESENT

    • 忘了把 GLFW 要的 Instance 擴充帶進來(glfwGetRequiredInstanceExtensions)。
  3. Swapchain 格式/尺寸不合

    • caps.currentExtent 與表面格式,不要自己硬填
  4. 呈現黑畫面(Day 12)

    • 常見是影像 Layout/Barrier 不正確、沒清 attachment、沒呼叫 present。
  5. 執行緒卡住

    • 你忘了 vkDeviceWaitIdle 前要等所有提交完成,或 Fence 沒有等待/重設。

8)小字典(把名詞與直覺對上)

  • Instance:Vulkan 程式的大門票(全域階段)。
  • Surface:視窗與 Vulkan 的橋。
  • PhysicalDevice:實體顯卡(或整合 GPU)。
  • Device:你和顯卡溝通的「把手」;從這裡拿 Queue
  • Queue Family / Queue:像顯卡的公車路線(圖形、計算、傳輸)。
  • Swapchain:螢幕要播放的影像輪播(double/triple buffer)。
  • Image / ImageView:顏色貼圖 / 它的「如何被存取」視角。
  • Command Buffer:你錄好的工作清單,交給 GPU 執行。
  • Semaphore / Fence:GPU↔GPU / CPU↔GPU 的「完成通知」。

9)我現在可以做什麼?(三個 10 分鐘內完成的小步)

  1. 把主迴圈加上視窗縮放處理:視窗大小變動 → 重新建立 swapchain(銷毀舊的 image view & swapchain,再建新的)。
  2. 加入 Debug Messenger:體驗一下它的「講人話」錯誤提示。
  3. 清畫面顏色(Day 12 預習):加 Command Pool/Buffer + Render Pass + Framebuffer,錄一個「清成藍色」的 pass,Submit 後 Present。

10)下一集預告(Day 12)

  • Vulkan 的渲染流程:Command Buffer 與 Render Pass
    我們要把今天的骨架接上:Command Pool/Buffer、Render Pass /(或 Dynamic Rendering 更簡)、Graphics Pipeline、同步(Semaphore/Fence),把螢幕清成你指定的顏色,然後畫出第一個三角形。

11)一句話總結

Vulkan 初始化的本質
視窗弄好 → 把顯卡初始化(Instance/Device/Queue)→ 把交換鏈影像檢視備好 → (下一步)錄命令提交
一次只加一個階段,配合 Validation Layers,你會很穩地走到能畫第一張圖的那一步。


上一篇
Day 10|Vulkan 是什麼?與 OpenGL 的差異與優勢(零基礎友善版)
下一篇
Day 12|Vulkan 渲染流程:Command Buffer 與 Render Pass
系列文
渲染與GPU編程13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言