Vulkan'da pencere boyutlandırmasında swapchain yeniden oluşturma nasıl ele alınır

Example: VulkanExamples/p12-resize

Serideki 12. yazı (1: surface, 2: üçgen, 8: frames in flight, 9: index buffer’lar, 10: UBO’lar + MVP, 11: derinlik). Her Vulkan yeni başlayanın yakaladığı ilk pencereli-program bug’ı: pencereyi boyutlandır ve program çöker ya da çöp render eder, çünkü swapchain sabit bir surface state’ine aittir — boyutlandırma onu geçersiz kılar.

İki sinyal — ve kimsenin söylemediği üçüncüsü

sinyalneredeanlamı
VK_ERROR_OUT_OF_DATE_KHRacquireNextImageKHR, presentKHRswapchain kullanılamaz — çizimden önce yeniden oluşturulmalı
VK_SUBOPTIMAL_KHRacquireNextImageKHR, presentKHRhâlâ kullanılabilir — bu frame’i çiz, sonraki acquire’dan önce yeniden oluştur
framebuffer boyutu ≠ swapchain.extentglfwGetFramebufferSize vs sakladığın extentsürücü hiç sinyal vermese bile chain eskidir — bazı X11 WSI yollarında olur

Üçü de ele alınmalı. Demo acquire/present’ın eSuboptimalKHR sonucunu artı frame başına framebuffer boyutu karşılaştırmasını kullanır, böylece sinyal veren ve vermeyen sürücülerde çalışır.

Yeniden oluşturma kuralları

  1. acquire‘dan OUT_OF_DATE → bu frame’de hiçbir şey çizme.
  2. SUBOPTIMAL (acquire veya present) → frame’i normal bitir, sonraki acquire’dan önce yeniden oluştur.
  3. Yeni chain’i oluşturmadan önce: queue.waitIdle() (veya en azından eski image’ları hâlâ kullanabilecek her frame’in fence’i), sonra image view’ları ve eski swapchain’i yok et — bu sırayla.
  4. 0×0 framebuffer (simge durumuna küçültülmüş pencere) → hiç swapchain oluşturma; boyut sıfır olmayana dek glfwWaitEvents().
  5. Extent’e veya swapchain image sayısı/formatına bağlı her şey yeniden kurulmalı: image view’lar, depth attachment’lar, image başına sync nesneleri, belirli bir format için kurulmuş pipeline’lar. Bu örnek pipeline’ı koşulsuz yeniden kurar — ucuz ve surface formatı da değiştiyse doğru.

Build & çalıştırma

build.sh
g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc
./app            # windowed — resizes itself at frame 120
python3 check.py # drives ./app under Xvfb and asserts the log

Kontrol

Bu yönün assert edecek pikseli yok — gözlenebilir davranış log’da. check.py demoyu xvfb-run altında çalıştırır, sonra üç şeyi doğrular: temiz çıkış, gerçekten bir yeniden oluşturma olduğu ve yeniden oluşturulan swapchain’in extent’inin yeni pencere boyutuyla (800×600) eşleştiği.

check.py
#!/usr/bin/env python3
# SPDX-License-Identifier: CC0-1.0
# check.py — Xvfb altında boyutlandırma demosunu sür ve doğrula:
#   1. pencereli çalışma 0 ile çıkar
#   2. swapchain gerçekten yeni extent'te yeniden oluşturuldu
#   3. log beklenen yeni boyutu raporlar (800x600 framebuffer
#      veya bir HiDPI katı)
import re
import subprocess
import sys


def main():
    exe = sys.argv[1] if len(sys.argv) > 1 else "./app"
    r = subprocess.run(["xvfb-run", "-a", exe],
                       capture_output=True, text=True, timeout=180)
    out = r.stdout + r.stderr
    print(out, end="")
    assert r.returncode == 0, f"exit {r.returncode}"

    assert "[demo] resized window to 800x600" in out, \
        "resize was never triggered"
    m = re.findall(r"\[swapchain\] recreated: (\d+)x(\d+)", out)
    assert m, "no swapchain recreation happened"
    for w, h in m:
        # burada X11 framebuffer == pencere boyutu
        # (Xvfb altında HiDPI yok)
        assert (int(w), int(h)) == (800, 600), f"bad extent {w}x{h}"
    print(f"OK: clean exit after {len(m)} recreation(s) to 800x600")


if __name__ == "__main__":
    main()
output.txt
$ python3 check.py
device: AMD Radeon RX 7800 XT (RADV NAVI32)
[demo] resized window to 800x600
[swapchain] recreated: 800x600 (#1)
[demo] done: 1 recreation(s)
OK: clean exit after 1 recreation(s) to 800x600

Tam kaynak

main.cpp
// SPDX-License-Identifier: CC0-1.0
//
// Post 12: boyutlandırmada swapchain yeniden oluşturma. Sadece-
// pencere konusu — boyutlandırılmış pencere swapchain'i geçersiz
// kılar. acquireNextImageKHR sonra VK_ERROR_OUT_OF_DATE_KHR döner
// ve presentKHR VK_SUBOPTIMAL_KHR dönebilir. Yanlış ele alınır:
// çökme veya bozuk frame'ler. Doğru ele alınır: yeni surface
// cap'lerine karşı swapchain'i yeniden oluştur.
//
// Demo kendi penceresini çalışma ortasında boyutlandırır
// (frame 120, 640x480 -> 800x600) ki tüm döngü otomatik
// işletilsin.
//
//   ./app     pencereli, kendini boyutlandırır, ~240 frame, 0 çıkar
//   check.py  Xvfb altında sürer; log + çıkış kodunu assert eder
//
// Build:  g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc

#include "vkmini.hpp"

#include <GLFW/glfw3.h>
#include <cmath>

static const char* kVert = R"GLSL(
#version 460
layout(location = 0) out vec3 vColor;
void main() {
    vec2 p[3] = vec2[](vec2(-0.6, 0.6), vec2(0.6, 0.6),
                       vec2(0.0, -0.6));
    vec3 c[3] = vec3[](vec3(1, 0, 0), vec3(0, 1, 0),
                       vec3(0, 0, 1));
    gl_Position = vec4(p[gl_VertexIndex], 0.0, 1.0);
    vColor = c[gl_VertexIndex];
}
)GLSL";

static const char* kFrag = R"GLSL(
#version 460
layout(location = 0) in vec3 vColor;
layout(location = 0) out vec4 outColor;
void main() { outColor = vec4(vColor, 1.0); }
)GLSL";

struct Pipe {
    vk::UniquePipelineLayout layout;
    vk::UniquePipeline pipeline;
};

/// Swapchain başına kurulur çünkü color attachment formatı
/// (prensipte) surface yeniden müzakereleri arasında değişebilir.
static Pipe makePipeline(vkmini::Vk& vk, vk::Format fmt) {
    Pipe o;
    auto vs = vk.shader(kVert, shaderc_vertex_shader, "t.vert");
    auto fs = vk.shader(kFrag, shaderc_fragment_shader, "t.frag");
    vk::PipelineShaderStageCreateInfo stages[2];
    stages[0].setStage(vk::ShaderStageFlagBits::eVertex)
        .setModule(vs.get())
        .setPName("main");
    stages[1].setStage(vk::ShaderStageFlagBits::eFragment)
        .setModule(fs.get())
        .setPName("main");
    vk::PipelineVertexInputStateCreateInfo vin{};
    vk::PipelineInputAssemblyStateCreateInfo ia{};
    ia.setTopology(vk::PrimitiveTopology::eTriangleList);
    vk::PipelineViewportStateCreateInfo vp{};
    vp.setViewportCount(1).setScissorCount(1);
    std::array dynStates{vk::DynamicState::eViewport,
                         vk::DynamicState::eScissor};
    vk::PipelineDynamicStateCreateInfo dyn{};
    dyn.setDynamicStates(dynStates);
    vk::PipelineRasterizationStateCreateInfo rs{};
    rs.setPolygonMode(vk::PolygonMode::eFill)
        .setCullMode(vk::CullModeFlagBits::eNone)
        .setFrontFace(vk::FrontFace::eCounterClockwise)
        .setLineWidth(1.0f);
    vk::PipelineMultisampleStateCreateInfo ms{};
    ms.setRasterizationSamples(vk::SampleCountFlagBits::e1);
    vk::PipelineColorBlendAttachmentState cb{};
    cb.setColorWriteMask(
        vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
        vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA);
    vk::PipelineColorBlendStateCreateInfo blend{};
    blend.setAttachments(cb);
    o.layout = vk.device->createPipelineLayoutUnique({});
    vk::PipelineRenderingCreateInfo rendering{};
    rendering.setColorAttachmentFormats(fmt);
    vk::GraphicsPipelineCreateInfo gi{};
    gi.setStages(stages)
        .setPVertexInputState(&vin)
        .setPInputAssemblyState(&ia)
        .setPViewportState(&vp)
        .setPDynamicState(&dyn)
        .setPRasterizationState(&rs)
        .setPMultisampleState(&ms)
        .setPColorBlendState(&blend)
        .setLayout(o.layout.get())
        .setPNext(&rendering);
    auto res = vk.device->createGraphicsPipelineUnique({}, gi);
    if (res.result != vk::Result::eSuccess)
        throw std::runtime_error("pipeline failed");
    o.pipeline = std::move(res.value);
    return o;
}

int main() {
    vkmini::Vk vk;
    glfwInit();
    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    GLFWwindow* win =
        glfwCreateWindow(640, 480, "vkmini p12", nullptr, nullptr);
    uint32_t n = 0;
    const char** e = glfwGetRequiredInstanceExtensions(&n);
    vk.createInstance({e, e + n});
    VkSurfaceKHR raw;
    glfwCreateWindowSurface(vk.instance.get(), win, nullptr, &raw);
    auto surface = vk::UniqueSurfaceKHR(raw, vk.instance.get());
    vk.pickPhysicalDevice();
    vk.createDevice({VK_KHR_SWAPCHAIN_EXTENSION_NAME});

    vkmini::Swapchain sc;
    auto createSwapchain = [&] {
        int fw = 0, fh = 0;
        // Simge durumundaki pencere 0x0 framebuffer raporlar —
        // 0-boyut chain kurmak yerine gerçek boyuta dönene dek bekle.
        while (fw == 0 || fh == 0) {
            glfwGetFramebufferSize(win, &fw, &fh);
            glfwWaitEvents();
        }
        sc.create(vk, vk.phys, surface.get(), uint32_t(fw),
                  uint32_t(fh));
    };
    createSwapchain();
    Pipe p = makePipeline(vk, sc.format);

    auto cmd = vk.device->allocateCommandBuffersUnique(
        {vk.pool.get(), vk::CommandBufferLevel::ePrimary, 1});
    auto acqSem = vk.device->createSemaphoreUnique({});
    auto doneSem = vk.device->createSemaphoreUnique({});
    auto fence = vk.device->createFenceUnique({});

    bool needRecreate = false;
    int recreations = 0;
    for (int f = 0; f < 480 && !glfwWindowShouldClose(win); ++f) {
        glfwPollEvents();
        // Frame 120'de kendi kendine boyutlandır — gerçek uygulama
        // bunu OS/pencere callback'inden yapar; tetik noktası önemli
        // değil.
        if (f == 120) {
            glfwSetWindowSize(win, 800, 600);
            printf("[demo] resized window to 800x600\n");
        }

        // Sürücü sinyalinden bağımsız ikinci tetik: gerçek framebuffer
        // boyutu swapchain extent'inden ayrışırsa, acquire/present
        // hiç hata vermemiş olsa bile chain eskidir — bazı X11 WSI
        // yollarında olur.
        {
            int fw = 0, fh = 0;
            glfwGetFramebufferSize(win, &fw, &fh);
            if (fw && fh &&
                (uint32_t(fw) != sc.extent.width ||
                 uint32_t(fh) != sc.extent.height))
                needRecreate = true;
        }

        if (needRecreate) {
            // Ancak GPU eski swapchain ile işini bitirdikten sonra.
            vk.queue.waitIdle();
            sc.views.clear();
            sc.sc.reset();
            createSwapchain();
            // Müzakere edilen format farklı olabilirse pipeline'ı da
            // yeniden oluştur — burada koşulsuz yapmak yeterince ucuz.
            p = makePipeline(vk, sc.format);
            needRecreate = false;
            ++recreations;
            printf("[swapchain] recreated: %ux%u (#%d)\n",
                   sc.extent.width, sc.extent.height, recreations);
        }

        vk.device->resetFences(fence.get());
        // ---- acquire with resize handling -------------------------
        auto acq = vk.device->acquireNextImageKHR(
            sc.sc.get(), UINT64_MAX, acqSem.get(), {});
        if (acq.result == vk::Result::eErrorOutOfDateKHR) {
            needRecreate = true;
            continue; // bu frame'e çizilecek bir şey yok
        }
        // SUBOPTIMAL: hâlâ kullanılabilir — bu frame'i çiz, sonraki
        // acquire'da yeniden oluştur
        if (acq.result == vk::Result::eSuboptimalKHR)
            needRecreate = true;

        vk::Image img = sc.images[acq.value];
        cmd[0]->reset();
        cmd[0]->begin(
            {vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
        vk::ImageSubresourceRange sr{vk::ImageAspectFlagBits::eColor,
                                     0, 1, 0, 1};
        vk::ImageMemoryBarrier toAtt{};
        toAtt.setOldLayout(vk::ImageLayout::eUndefined)
            .setNewLayout(vk::ImageLayout::eColorAttachmentOptimal)
            .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setImage(img)
            .setSubresourceRange(sr)
            .setSrcAccessMask({})
            .setDstAccessMask(
                vk::AccessFlagBits::eColorAttachmentWrite);
        cmd[0]->pipelineBarrier(
            vk::PipelineStageFlagBits::eTopOfPipe,
            vk::PipelineStageFlagBits::eColorAttachmentOutput, {}, {},
            {}, toAtt);
        vk::RenderingAttachmentInfo att{};
        att.setImageView(sc.views[acq.value].get())
            .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
            .setLoadOp(vk::AttachmentLoadOp::eClear)
            .setStoreOp(vk::AttachmentStoreOp::eStore)
            .setClearValue(vk::ClearValue{vk::ClearColorValue{
                std::array{0.05f, 0.05f, 0.08f, 1.f}}});
        vk::RenderingInfo ri{};
        ri.setRenderArea({{0, 0}, sc.extent})
            .setLayerCount(1)
            .setColorAttachments(att);
        cmd[0]->beginRendering(ri);
        cmd[0]->bindPipeline(vk::PipelineBindPoint::eGraphics,
                             p.pipeline.get());
        vk::Viewport vp{0, 0, float(sc.extent.width),
                        float(sc.extent.height), 0.f, 1.f};
        cmd[0]->setViewport(0, vp);
        vk::Rect2D sci{{0, 0}, sc.extent};
        cmd[0]->setScissor(0, sci);
        cmd[0]->draw(3, 1, 0, 0);
        cmd[0]->endRendering();
        vk::ImageMemoryBarrier toPresent{};
        toPresent.setOldLayout(
                     vk::ImageLayout::eColorAttachmentOptimal)
            .setNewLayout(vk::ImageLayout::ePresentSrcKHR)
            .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setImage(img)
            .setSubresourceRange(sr)
            .setSrcAccessMask(
                vk::AccessFlagBits::eColorAttachmentWrite)
            .setDstAccessMask({});
        cmd[0]->pipelineBarrier(
            vk::PipelineStageFlagBits::eColorAttachmentOutput,
            vk::PipelineStageFlagBits::eBottomOfPipe, {}, {}, {},
            toPresent);
        cmd[0]->end();

        vk::PipelineStageFlags waitStage =
            vk::PipelineStageFlagBits::eColorAttachmentOutput;
        vk::SubmitInfo si{};
        si.setWaitSemaphores(acqSem.get())
            .setWaitDstStageMask(waitStage)
            .setCommandBuffers(cmd[0].get())
            .setSignalSemaphores(doneSem.get());
        vk.queue.submit(si, fence.get());
        vk::PresentInfoKHR pi{};
        pi.setWaitSemaphores(doneSem.get())
            .setSwapchains(sc.sc.get())
            .setImageIndices(acq.value);
        vk::Result pr = vk.queue.presentKHR(pi);
        if (pr == vk::Result::eErrorOutOfDateKHR ||
            pr == vk::Result::eSuboptimalKHR)
            needRecreate = true;
        (void)vk.device->waitForFences(fence.get(), VK_TRUE,
                                     UINT64_MAX);
    }
    printf("[demo] done: %d recreation(s)\n", recreations);
    vk.queue.waitIdle();
    sc.views.clear();
    sc.sc.reset();
    surface.reset();
    glfwDestroyWindow(win);
    glfwTerminate();
    return 0;
}

Bu serideki tüm kaynak kodu CC0 1.0 Universal (CC0-1.0) altında yayınlandı — kamu malı adaması, atıf gerekmez.

Serinin devamı: instanced rendering — gl_InstanceIndex ve instance-rate vertex attribute’larıyla tek draw call’da yüzlerce kopya.


Check out similar posts by category: C++ Vulkan