feat(window): a destructor for the native backends

A Window could be created but never destroyed: the constructor's command
buffers, semaphores, fences, swapchain and Vulkan surface, and on Wayland
the viewport, fractional scale, decoration, xdg toplevel, xdg surface and
wl_surface stayed alive, and Device::windows kept a dangling pointer the
pointer listeners still compared against. An app that hides its window
and shows it again — a resident messaging app closing to a notification
— needs the window to go for real.

The destructor waits for the device, frees and destroys the Vulkan objects
in dependency order (the swapchain before the surface), releases the
Wayland objects from the cursor down to the wl_surface, flushes, and
removes the window from Device::windows and the focus pointers. The
decoration object the constructor used to leak is a member now. The frame
callback member is left alone: wl_surface_frame_done destroys the callback
it is handed and re-arms through its own local, so the member may name a
freed one.
This commit is contained in:
Jorijn van der Graaf 2026-10-18 00:40:25 +02:00
commit b412640fac
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
2 changed files with 55 additions and 5 deletions

View file

@ -299,7 +299,7 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
wl_surface_commit(surface); wl_surface_commit(surface);
zxdg_toplevel_decoration_v1* decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(Device::manager, xdgToplevel); decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(Device::manager, xdgToplevel);
zxdg_toplevel_decoration_v1_set_mode(decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); zxdg_toplevel_decoration_v1_set_mode(decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
wpViewport = wp_viewporter_get_viewport(Device::wpViewporter, surface); wpViewport = wp_viewporter_get_viewport(Device::wpViewporter, surface);
@ -515,6 +515,52 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
currentMousePos = {0,0}; currentMousePos = {0,0};
} }
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
Window::~Window() {
// Nothing may still run against the semaphores, fences or images below.
if (Device::device != VK_NULL_HANDLE) vkDeviceWaitIdle(Device::device);
if (Device::device != VK_NULL_HANDLE) {
vkFreeCommandBuffers(Device::device, Device::commandPool, numFrames, drawCmdBuffers);
for (std::uint8_t i = 0; i < numFrames; i++) {
if (renderFinishedSemaphores[i]) vkDestroySemaphore(Device::device, renderFinishedSemaphores[i], nullptr);
if (waitFences[i]) vkDestroyFence(Device::device, waitFences[i], nullptr);
}
for (std::uint8_t i = 0; i < numAcquireSemaphores; i++) {
if (imageAcquiredSemaphores[i]) vkDestroySemaphore(Device::device, imageAcquiredSemaphores[i], nullptr);
}
if (swapChain != VK_NULL_HANDLE) vkDestroySwapchainKHR(Device::device, swapChain, nullptr);
swapChain = VK_NULL_HANDLE;
}
if (vulkanSurface != VK_NULL_HANDLE) vkDestroySurfaceKHR(Device::instance, vulkanSurface, nullptr);
vulkanSurface = VK_NULL_HANDLE;
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
SetDefaultCursor();
// `cb` is not destroyed here: wl_surface_frame_done frees the callback it
// was called with and re-arms through its own local, so the member may
// already name a freed one.
if (wpViewport) wp_viewport_destroy(wpViewport);
if (wp_scale) {
wp_fractional_scale_v1_destroy(wp_scale);
wp_scale = nullptr;
}
if (decoration) zxdg_toplevel_decoration_v1_destroy(decoration);
if (xdgToplevel) xdg_toplevel_destroy(xdgToplevel);
if (xdgSurface) xdg_surface_destroy(xdgSurface);
if (surface) wl_surface_destroy(surface);
wpViewport = nullptr;
decoration = nullptr;
xdgToplevel = nullptr;
xdgSurface = nullptr;
surface = nullptr;
wl_display_flush(Device::display);
std::erase(Device::windows, this);
if (Device::focusedWindow == this) Device::focusedWindow = nullptr;
if (Device::keyboardWindow == this) Device::keyboardWindow = nullptr;
#endif
}
#endif
void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) { void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) {
// Skip degenerate resizes. Win32 minimised windows give (0, 0); Wayland // Skip degenerate resizes. Win32 minimised windows give (0, 0); Wayland
// sometimes echoes the current size in a configure. // sometimes echoes the current size in a configure.

View file

@ -110,16 +110,19 @@ export namespace Crafter {
Window(Window&) = delete; Window(Window&) = delete;
Window(Window&&) = delete; Window(Window&&) = delete;
Window& operator=(const Window&) = delete; Window& operator=(const Window&) = delete;
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
// DOM mode keeps a process-global pointer to the live Window so // DOM mode keeps a process-global pointer to the live Window so
// the JS bridge can deliver document/window events back. If the // the JS bridge can deliver document/window events back. If the
// Window is destroyed (e.g. it was stack-allocated in main and // Window is destroyed (e.g. it was stack-allocated in main and
// main returned), the global must be cleared so subsequent // main returned), the global must be cleared so subsequent
// browser callbacks become no-ops instead of dereferencing // browser callbacks become no-ops instead of dereferencing
// freed memory. Native builds have nothing to clean up at the // freed memory. Native builds release what the constructor made:
// Window level (Vulkan resources teardown is the user's job). // after a device wait, the command buffers, semaphores and fences,
// the swapchain and the Vulkan surface, then (Wayland) the cursor,
// viewport, fractional scale, decoration, xdg toplevel, xdg surface
// and wl_surface, and the window leaves Device::windows and the
// focus pointers — so a window can be closed and another created,
// which a resident app does on every hide and show.
~Window(); ~Window();
#endif
// Enter the platform event loop and never return. In DOM mode this // Enter the platform event loop and never return. In DOM mode this
// hands the loop to requestAnimationFrame and _Exit(0)s so the wasm // hands the loop to requestAnimationFrame and _Exit(0)s so the wasm
@ -191,6 +194,7 @@ export namespace Crafter {
std::int32_t pendingLogicalWidth = 0; std::int32_t pendingLogicalWidth = 0;
std::int32_t pendingLogicalHeight = 0; std::int32_t pendingLogicalHeight = 0;
xdg_toplevel* xdgToplevel = nullptr; xdg_toplevel* xdgToplevel = nullptr;
zxdg_toplevel_decoration_v1* decoration = nullptr;
wp_viewport* wpViewport = nullptr; wp_viewport* wpViewport = nullptr;
wl_surface* surface = nullptr; wl_surface* surface = nullptr;
xdg_surface* xdgSurface = nullptr; xdg_surface* xdgSurface = nullptr;