From b16f1e99d5fb7943c528256d2678995e7e30f2f6 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 18 Oct 2026 01:12:21 +0200 Subject: [PATCH] fix(font-atlas): the atlas destroys its image FontAtlas::Initialize creates an ImageVulkan and nothing ever called its Destroy(): every atlas left its 1024x1024 image, memory and view behind. The atlas now owns the image for its lifetime and is no longer copyable. ImageVulkan's handles start as VK_NULL_HANDLE and Destroy() resets them, so an atlas that was never initialised, or an image destroyed by hand before the atlas goes, is a no-op in the destructor. The owner waits for the GPU before the atlas goes and, in sets mode, destroys the descriptor heap that holds the image's view before the atlas. --- interfaces/Crafter.Graphics-FontAtlas.cppm | 12 ++++++++++++ interfaces/Crafter.Graphics-ImageVulkan.cppm | 9 ++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/interfaces/Crafter.Graphics-FontAtlas.cppm b/interfaces/Crafter.Graphics-FontAtlas.cppm index 4f72522..732455d 100644 --- a/interfaces/Crafter.Graphics-FontAtlas.cppm +++ b/interfaces/Crafter.Graphics-FontAtlas.cppm @@ -96,6 +96,18 @@ export namespace Crafter { bool dirty = false; DirtyRect dirtyRect; +#ifndef CRAFTER_GRAPHICS_WINDOW_DOM + FontAtlas() = default; + FontAtlas(const FontAtlas&) = delete; + FontAtlas& operator=(const FontAtlas&) = delete; + // The image goes with the atlas. The owner waits for the GPU first + // and, in sets mode, destroys the heap holding the image's view before + // the atlas. + ~FontAtlas() { + if (image.image != VK_NULL_HANDLE) image.Destroy(); + } +#endif + void Initialize(GraphicsCommandBuffer cmd); // Returns the row-major byte pointer the CPU writes pixels into. diff --git a/interfaces/Crafter.Graphics-ImageVulkan.cppm b/interfaces/Crafter.Graphics-ImageVulkan.cppm index 3864d60..8ae4df8 100644 --- a/interfaces/Crafter.Graphics-ImageVulkan.cppm +++ b/interfaces/Crafter.Graphics-ImageVulkan.cppm @@ -56,8 +56,8 @@ export namespace Crafter { std::uint16_t width; std::uint16_t height; std::uint8_t mipLevels; - VkImage image; - VkDeviceMemory imageMemory; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory imageMemory = VK_NULL_HANDLE; VulkanBuffer buffer; // Transient host-visible staging for the compressed Update path. Same // lifetime contract as Mesh::compressedStaging: the compressed Update @@ -66,7 +66,7 @@ export namespace Crafter { // submit's frame has cleared instead of pinning it for the image's life. // Between Updates the handle is null; the next Update's Resize re-creates it. VulkanBuffer compressedStaging; - VkImageView imageView; + VkImageView imageView = VK_NULL_HANDLE; VkDescriptorImageInfo descriptor; // Pipeline stage that samples this image after upload. Set once at // Create and reused by every Update so the upload barrier's dst (and @@ -378,6 +378,9 @@ export namespace Crafter { vkDestroyImageView(Device::device, imageView, nullptr); vkDestroyImage(Device::device, image, nullptr); vkFreeMemory(Device::device, imageMemory, nullptr); + imageView = VK_NULL_HANDLE; + image = VK_NULL_HANDLE; + imageMemory = VK_NULL_HANDLE; // Free any staging this image still owns: a streamed image's // persistent `buffer`, or either staging buffer on an image // destroyed before its first upload released them (issue #114).