Uh oh!
There was an error while loading. Please reload this page.
Fix segfault on window close: let GLFW outlive the Vulkan objects - #443
Fix segfault on window close: let GLFW outlive the Vulkan objects#443ozanyasindogan wants to merge 2 commits into
Conversation
`cleanup()` called `glfwDestroyWindow()` and `glfwTerminate()` while every `vk::raii` member was still alive. Those members belong to the application class, so they are destroyed only after `run()` returns - that is, after GLFW has already torn down the window system connection they still reference. `vkDestroySwapchainKHR` then marshals a request on freed Wayland proxies and the process dies during exit: KhronosGroup#12 HelloTriangleApplication::~HelloTriangleApplication() KhronosGroup#11 vk::raii::SwapchainKHR::~SwapchainKHR() KhronosGroup#10 vk::raii::SwapchainKHR::clear() KhronosGroup#4 libnvidia-glcore.so KhronosGroup#3 wl_proxy_marshal_flags #0 libwayland-client.so.0 <- SEGV_MAPERR The pre-RAII tutorial destroyed every Vulkan object by hand at the end of `cleanup()`, so `glfwTerminate()` genuinely ran last. Converting to `vk::raii` removed those explicit calls but left the two GLFW calls behind, silently inverting the order. Ownership of the GLFW lifetime now belongs to a `GlfwGuard` member declared first in the class. Members are destroyed in reverse declaration order, so being first means it is destroyed last - after every Vulkan object, however many later chapters add. `glfwTerminate()` also destroys any windows still open, so the separate `glfwDestroyWindow()` call is no longer needed. Applied to all 34 affected chapters, plus 00_base_code so the pattern is correct from the very first chapter and never has to change again - which is what the Base Code chapter already promises the reader. Verified on Arch Linux / Wayland / KWin, NVIDIA 610.43.03, Vulkan 1.4.357: - all 304 build targets compile - 22 chapters that present a frame: launched and closed with a real compositor close request, all exit 0 (each exited 139 before) - 14_command_buffers (never presents, so its surface is never mapped and it cannot be closed by the compositor) checked by breaking out of the main loop instead: 3/3 SIGSEGV before, 3/3 clean after - no validation layer errors, and no coredumps from any fixed binary Refs: KhronosGroup#138
On windows, I don't have any issues with window closing, here. But shouldn't the GlfwGuard have a constructor like this: And even though implicit destruction of the GLFWwindow works fine, maybe should then introduce some wrapper for it as well, like Or, as already mentioned in #138, switch to vkfw? Besides that, I would not keep an empty |
Follow-up to review feedback on KhronosGroup#443. `glfwInit()` was called at the top of `initWindow()` and its return value discarded in every chapter, so a failed initialization surfaced later as a null window rather than as an error. Moving it into `GlfwGuard` gives the guard both ends of the GLFW lifetime and gives the failure somewhere to go: GlfwGuard() { if (!glfwInit()) { throw std::runtime_error("failed to initialize GLFW!"); } } The constructor runs during member initialization, before `run()` is entered, and every chapter already constructs the application object inside the `try` in `main()`, so the error is reported through the same path as every other `std::runtime_error`. If it throws, `~GlfwGuard()` never runs, so `glfwTerminate()` is correctly skipped after a failed init. Also in this change: - 34_android.cpp gains the same guard. Its PLATFORM_DESKTOP path creates a GLFW window but never called `glfwTerminate()` or `glfwDestroyWindow()`, so it leaked GLFW rather than destroying it out of order. Every chapter that owns a window now uses the guard. - `cleanup()` was left as bare empty braces in 31_compute_shader, 32_ecosystem_utilities, 35_gltf_ktx and 38_ray_tracing, while 28 other chapters carried a comment explaining where GLFW teardown had moved to. Those four now match. - The guard in 32_ecosystem_utilities moved above `AppInfo appInfo` so that "declared first" is literally true there. - 00_Base_code.adoc introduces the guard where GLFW initialization is taught, instead of reintroducing it in the cleanup section, and 04_Swap_chain_recreation.adoc drops `glfwInit()` from its initWindow snippet. Verified on Windows 11, MSVC 14.51, Vulkan 1.4.357, RTX 5080: - all 144 build targets compile with no new warnings - clang-format-diff.py over the diff against main reports no violations - every chapter built from a pristine main and from this branch, then closed with a real WM_CLOSE and its exit code recorded: 36/36 exit 0 on both sides. Windows does not fault on the original ordering, so this measures no regression rather than a fix on that platform. Refs: KhronosGroup#138 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ozanyasindogan
commented
Aug 10, 2026
Hi, I compiled and ran the tutorial files on a Linux/Wayland desktop and the crash happens on every desktop chapter there. On Windows it quits without crashing, like you say. But the ordering still looks wrong to me on both, it just doesn't blow up on Win32. I checked Windows here as well before answering, so it's not only guesswork. I built every chapter from a clean main and from this branch, ran them all and closed each window the normal way. Nothing crashed on either side and I couldn't see any regression from the change. I'm still learning Vulkan, so I don't want to overstate the Wayland part. What I could follow from the backtrace is that What I wanted with this PR was to fix that without pulling in a new dependency or rewriting too much of the tutorial code. About structGlfwGuard
{
GlfwGuard()
{
if (!glfwInit())
{
throwstd::runtime_error("failed to initialize GLFW!");
}
}
~GlfwGuard()
{
glfwTerminate();
}
} glfwGuard;
The None of that is an objection in principle. It just makes this a refactor of the tutorial's window setup rather than a crash fix, and it doesn't change teardown behaviour, since Same for vkfw, only more so, since that is a new dependency in every chapter plus all the prose around it. I'd have thought that one belongs in #138 rather than here. I also ran a code review with Claude Code over the rest of the chapters. Its notes are below. Claude Code review notes
Inconsistent
Verification. All 144 targets build clean under MSVC 14.51 with no new warnings. The repository's own |
ozanyasindogan
commented
Aug 10, 2026
The The job fails while applying the Android Gradle plugin, before any C++ gets compiled:
This PR seems to be the first one to actually run the job. For completeness, the change can't affect the Android build. In all three Android-supported chapters it touches ( Two ways to get the job green again, either as a one-liner here or as a separate PR:
|
Follow-up to review feedback on KhronosGroup#443. `glfwInit()` was called at the top of `initWindow()` and its return value discarded in every chapter, so a failed initialization surfaced later as a null window rather than as an error. Moving it into `GlfwGuard` gives the guard both ends of the GLFW lifetime and gives the failure somewhere to go: GlfwGuard() { if (!glfwInit()) { throw std::runtime_error("failed to initialize GLFW!"); } } The constructor runs during member initialization, before `run()` is entered, and every chapter already constructs the application object inside the `try` in `main()`, so the error is reported through the same path as every other `std::runtime_error`. If it throws, `~GlfwGuard()` never runs, so `glfwTerminate()` is correctly skipped after a failed init. Also in this change: - 34_android.cpp gains the same guard. Its PLATFORM_DESKTOP path creates a GLFW window but never called `glfwTerminate()` or `glfwDestroyWindow()`, so it leaked GLFW rather than destroying it out of order. Every chapter that owns a window now uses the guard. - `cleanup()` was left as bare empty braces in 31_compute_shader, 32_ecosystem_utilities, 35_gltf_ktx and 38_ray_tracing, while 28 other chapters carried a comment explaining where GLFW teardown had moved to. Those four now match. - The guard in 32_ecosystem_utilities moved above `AppInfo appInfo` so that "declared first" is literally true there. - 00_Base_code.adoc introduces the guard where GLFW initialization is taught, instead of reintroducing it in the cleanup section, and 04_Swap_chain_recreation.adoc drops `glfwInit()` from its initWindow snippet. Verified on Windows 11, MSVC 14.51, Vulkan 1.4.357, RTX 5080: - all 144 build targets compile with no new warnings - clang-format-diff.py over the diff against main reports no violations - every chapter built from a pristine main and from this branch, then closed with a real WM_CLOSE and its exit code recorded: 36/36 exit 0 on both sides. Windows does not fault on the original ordering, so this measures no regression rather than a fix on that platform. Refs: KhronosGroup#138
207324c to
8d7abf5CompareSaschaWillems
commented
Aug 10, 2026
We are aware of the Android issue, it also occurs in one of our own PRs. |
Fixes the segfault on window close reported in #138.
Problem
Every chapter that owns Vulkan objects crashes with
SIGSEGVwhen the user closes the window. The program renders correctly the whole time and only dies during exit, which is why it is easy to miss.Root cause
run()ends withcleanup(), which callsglfwDestroyWindow()andglfwTerminate():But the Vulkan objects are class members (
vk::raii::SwapchainKHR,SurfaceKHR,Device,Instance, ...). Members are destroyed when the object dies, which is afterrun()returns. So the actual order is:mainLoop()returnscleanup()callsglfwTerminate()-> window system connection and its proxies are freedrun()returns,~HelloTriangleApplication()runs~SwapchainKHR()->vkDestroySwapchainKHR-> the driver marshals a request on freed proxies -> crashThis is a regression from the RAII conversion. The pre-RAII code destroyed every Vulkan object by hand at the end of
cleanup(), soglfwTerminate()genuinely ran last:Moving to
vk::raiiremoved those explicit calls - each one individually redundant - but left the two GLFW calls incleanup(), silently inverting the order.Fix
Give the GLFW lifetime to a guard declared first in the class. Members are destroyed in reverse declaration order, so first declared means last destroyed - after every Vulkan object, no matter how many later chapters add:
glfwTerminate()also destroys any windows still open, so the separateglfwDestroyWindow()call is no longer needed.The constructor was added after review feedback.
glfwInit()used to be called at the top ofinitWindow()with its return value discarded in every chapter, so a failed initialization only showed up later as a null window. Putting it here gives the guard both ends of the lifetime and gives the failure somewhere to go: the constructor runs during member initialization, and every chapter already builds the application object inside thetryinmain(), so it is reported like any otherstd::runtime_error. If it throws,~GlfwGuard()never runs, soglfwTerminate()is correctly skipped after a failed init.A destructor body would not work here - it runs before member destruction, so the ordering bug would remain.
Scope
00_base_code.cpp, which cannot crash today but is where the pattern is introduced. Including it means the pattern is correct from the first chapter and never has to change - which is what00_Base_code.adocalready promises the reader ("this is the last time we'll have to do anything in thecleanup()function")en/03_Drawing_a_triangle/00_Setup/00_Base_code.adoc- teaches the guard and explains why the order mattersen/03_Drawing_a_triangle/04_Swap_chain_recreation.adoc- snippet no longer terminates GLFW incleanup()34_android.cppis included too, though not for the same reason. An earlier revision of this description claimed it had no GLFW; that was wrong. It has a fullPLATFORM_DESKTOPpath that callsglfwInit()andglfwCreateWindow(), and it avoided this crash only because it never calledglfwTerminate()orglfwDestroyWindow()at all - it leaked GLFW instead of destroying it out of order. It now uses the same guard, so every chapter that owns a window is consistent.Test environment
nvidia-open-dkms), device apiVersion 1.4.341Testing: before and after
This was measured as a matched pair, not inferred. A pristine worktree at
e8c3ba2(currentmain) was configured and built with the same toolchain and run through the same harness on the same machine, then the identical sweep was run against this branch.How the window was closed. Rather than approximating, the close was driven through the compositor: a KWin script calls
closeWindow()on the toplevel, which sends a realxdg_toplevelclose - identical to clicking the X button. The tested path is the one users actually hit.Crashes were detected two independent ways: the process exit code, and
systemd-coredumprecords.Results
main@e8c3ba2)14_command_buffers, 3 consecutive runs30_multisampling, 5 consecutive runsZero chapters survived close before the change; all 22 exit cleanly after it.
The 22 chapters closed and verified end to end:
Chapters before
15_hello_trianglenever callpresentKHR. On Wayland a surface is not mapped until a buffer is attached, so the compositor has no window to close and they cannot be tested this way.14_command_bufferswas therefore verified by breaking out of the main loop instead, which reaches the identicalcleanup()+ destructor path - it crashed 3/3 before and exited cleanly 3/3 after, confirming the fix applies to that group too.Isolating the cause. As a control, removing only
glfwTerminate()from the unmodified code - a single-variable change that leaks the connection instead of freeing it early - also stopped the crash (5/5 clean). That confirms the destruction order, and nothing else, was responsible. The shipped fix restores correct teardown rather than leaking.The
30_multisamplingand14_command_buffersrun-repeats above were driven by breaking out of the main loop after a fixed frame count, which reaches the samecleanup()+ destructor path and allows repeated unattended runs.Formatting. Only the added lines were formatted. Running the repository's own CI check locally (
clang-format-diff.pyover the diff againstmain) reports no violations. Pre-existing formatting violations elsewhere in these files were deliberately left untouched to keep the diff reviewable.Windows
Windows was measured separately, on Windows 11 / MSVC 14.51 / Vulkan 1.4.357 / RTX 5080. Every chapter was built twice, once from a pristine
mainate8c3ba2and once from this branch, then launched and closed with a realWM_CLOSE- the message the X button sends - with the process exit code recorded.main@e8c3ba2)WM_CLOSESo Windows does not fault on the original ordering, and this change does not regress it. That is a no-op result rather than a fix on that platform, which matches @asuessenbach's report. Only
22_descriptor_layoutproduced validation output, in both the before and after sweeps, which is a pre-existing issue onmainunrelated to this PR.Not covered
The same wrong ordering exists on every platform, but whether it faults depends on the WSI teardown path. Testing on X11, macOS/MoltenVK, and AMD/Intel drivers would still be welcome.
Notes
An alternative would be introducing the guard in chapter 01 rather than 00, keeping chapter 0 minimal at the cost of the reader editing
cleanup()mid-tutorial. Happy to switch if you prefer that.Prepared with AI assistance (Claude), reviewed and verified by me on the hardware and software listed above.