//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM #include "vulkan/vulkan.h" #endif // !CRAFTER_GRAPHICS_WINDOW_DOM export module Crafter.Graphics:VulkanBuffer; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM import std; import :Device; namespace Crafter { // Round a host-write flush range outward to nonCoherentAtomSize boundaries — // the alignment vkFlushMappedMemoryRanges demands for a sub-buffer range // (whole-buffer VK_WHOLE_SIZE calls sidestep it). `atom` is // VkPhysicalDeviceLimits::nonCoherentAtomSize (a power of two ≥ 1) and // `mappingSize` is the byte size the memory was mapped with (the allocation // size). The start rounds down and the end rounds up to atom multiples; the // end is then clamped to `mappingSize`. Clamping is what keeps the range // valid even when `mappingSize` itself is not atom-aligned: the spec permits // a non-atom-multiple size only when offset+size equals the mapping size, and // the clamp lands exactly on that exception. Pure math, so it is unit-tested // without a device. export struct MappedFlushRange { VkDeviceSize offset; VkDeviceSize size; }; export constexpr MappedFlushRange AlignMappedFlushRange( VkDeviceSize offset, VkDeviceSize bytes, VkDeviceSize atom, VkDeviceSize mappingSize) { VkDeviceSize begin = (offset / atom) * atom; VkDeviceSize end = ((offset + bytes + atom - 1) / atom) * atom; if (end > mappingSize) end = mappingSize; return { begin, end - begin }; } export class VulkanBufferBase { public: VkDeviceAddress address; std::uint32_t size; // Byte size the memory was mapped with — equal to the allocation's // VkMemoryRequirements::size, which is ≥ `size` and what ranged flushes // clamp their upper bound to. Only meaningful for mapped buffers. VkDeviceSize mappedSize = 0; VkBuffer buffer = VK_NULL_HANDLE; VkDeviceMemory memory; // Property flags of the memory type actually chosen by GetMemoryType — // not the requested flags. GetMemoryType may land a request without the // COHERENT bit on a coherent type (and vice versa), so the flush/ // invalidate paths gate on this recorded value, never on the request. VkMemoryPropertyFlags memoryPropertyFlagsChosen = 0; // Byte capacity the buffer was created with, and the usage flags it was // created with. Resize reuses the allocation in place when a new // request still fits within `capacity` and these immutable-at-create // properties match, avoiding a destroy+reallocate. std::uint32_t capacity = 0; VkBufferUsageFlags2 usageFlagsCreated = 0; }; export template class VulkanBufferMapped { public: T* value; }; export class VulkanBufferMappedEmpty {}; template using VulkanBufferMappedConditional = std::conditional_t< Mapped, VulkanBufferMapped, VulkanBufferMappedEmpty >; export template class VulkanBuffer : public VulkanBufferBase, public VulkanBufferMappedConditional { public: VulkanBuffer() = default; // `preferredPropertyFlags` are best-effort: the allocation prefers a // memory type satisfying them on top of `memoryPropertyFlags`, but // falls back to the required flags alone rather than failing. Use it // for perf hints (e.g. DEVICE_LOCAL on a mapped buffer) that aren't // available on every device — see Device::GetMemoryType. void Create(VkBufferUsageFlags2 usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, std::uint32_t count, VkMemoryPropertyFlags preferredPropertyFlags = 0) { size = count * sizeof(T); capacity = size; usageFlagsCreated = usageFlags; // Carry usage in the maintenance5 flags2 chain so 64-bit bits // (e.g. VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT, bit 35) // are not truncated. VkBufferUsageFlags2CreateInfo usageFlags2 { .sType = VK_STRUCTURE_TYPE_BUFFER_USAGE_FLAGS_2_CREATE_INFO, .usage = usageFlags, }; VkBufferCreateInfo bufferCreateInfo {}; bufferCreateInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufferCreateInfo.pNext = &usageFlags2; bufferCreateInfo.usage = 0; bufferCreateInfo.size = sizeof(T)*count; Device::CheckVkResult(vkCreateBuffer(Device::device, &bufferCreateInfo, nullptr, &buffer)); VkMemoryRequirements memReqs; vkGetBufferMemoryRequirements(Device::device, buffer, &memReqs); std::uint32_t memoryTypeIndex = Device::GetMemoryType(memReqs.memoryTypeBits, memoryPropertyFlags, preferredPropertyFlags); memoryPropertyFlagsChosen = Device::memoryProperties.memoryTypes[memoryTypeIndex].propertyFlags; VkMemoryAllocateInfo memAlloc { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .allocationSize = memReqs.size, .memoryTypeIndex = memoryTypeIndex }; // Only a buffer created for SHADER_DEVICE_ADDRESS may have its // address queried (VUID-VkBufferDeviceAddressInfo-buffer-02601); // the others keep address 0. const bool addressable = (usageFlags & VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT) != 0; VkMemoryAllocateFlagsInfoKHR allocFlagsInfo { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO_KHR, .flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT_KHR, }; if (addressable) { memAlloc.pNext = &allocFlagsInfo; } Device::CheckVkResult(vkAllocateMemory(Device::device, &memAlloc, nullptr, &memory)); Device::CheckVkResult(vkBindBufferMemory(Device::device, buffer, memory, 0)); VkBufferDeviceAddressInfo addressInfo = { .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, .buffer = buffer }; address = addressable ? vkGetBufferDeviceAddress(Device::device, &addressInfo) : 0; // Record the allocation's byte size (≥ `size`): a ranged // FlushDevice flushes only the dirty sub-range and clamps its // rounded-up upper bound to this (the nonCoherentAtomSize-aligned-end // exception, see AlignMappedFlushRange). mappedSize = memReqs.size; if constexpr(Mapped) { Device::CheckVkResult(vkMapMemory(Device::device, memory, 0, memReqs.size, 0, reinterpret_cast(&(VulkanBufferMappedConditional::value)))); } } void Clear() { if constexpr(Mapped) { vkUnmapMemory(Device::device, memory); } vkDestroyBuffer(Device::device, buffer, nullptr); vkFreeMemory(Device::device, memory, nullptr); buffer = VK_NULL_HANDLE; } // Like Clear(), but hands the destroy+free to Device's fence-keyed // deletion queue instead of doing it immediately. Use when the buffer // may still be read by an in-flight frame's GPU work — destroying it // now would be a use-after-free, since frames are pipelined up to // framesInFlight deep (issue #101). The handle is nulled immediately so // this VulkanBuffer no longer owns it; vkFreeMemory (deferred) implicitly // unmaps mapped memory, so no explicit vkUnmapMemory is needed here. void DeferredClear() { Device::EnqueueDeletion(buffer, memory); buffer = VK_NULL_HANDLE; } void Resize(VkBufferUsageFlags2 usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, std::uint32_t count, VkMemoryPropertyFlags preferredPropertyFlags = 0) { // Reuse the existing allocation in place when the request still fits // and the fixed-at-create properties match: usage flags are // immutable after creation, and the memory type already chosen must // still satisfy the required property flags. preferredPropertyFlags // is a best-effort perf hint and does not affect correctness, so it // is intentionally not part of the guard. The buffer handle (and its // device address / mapped pointer) is preserved, only `size` shrinks // to the new logical extent. std::uint32_t requestedSize = count * sizeof(T); if(buffer != VK_NULL_HANDLE && requestedSize <= capacity && usageFlags == usageFlagsCreated && (memoryPropertyFlagsChosen & memoryPropertyFlags) == memoryPropertyFlags) { size = requestedSize; return; } // Defer the old allocation's destruction: an in-flight frame may // still reference it (the #63 hazard, no longer masked by a // per-frame wait-idle since #40). DeferredClear nulls the handle, // so the Create below starts from a clean slate. if(buffer != VK_NULL_HANDLE) { DeferredClear(); } Create(usageFlags, memoryPropertyFlags, count, preferredPropertyFlags); } void Copy(VkCommandBuffer cmd, VulkanBuffer& dst) { VkBufferCopy copyRegion = { .srcOffset = 0, .dstOffset = 0, .size = size }; vkCmdCopyBuffer( cmd, buffer, dst.buffer, 1, ©Region ); } void Copy(VkCommandBuffer cmd, VulkanBuffer& dst, VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask, VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask) { Copy(cmd, dst); VkBufferMemoryBarrier barrier = { .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, .srcAccessMask = srcAccessMask, .dstAccessMask = dstAccessMask, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .buffer = buffer, .offset = 0, .size = VK_WHOLE_SIZE }; vkCmdPipelineBarrier( cmd, srcStageMask, dstStageMask, 0, 0, NULL, 1, &barrier, 0, NULL ); } // Upload `count` host elements from `src` into this buffer as // device-local, GPU-read geometry, choosing placement at runtime via // the #89 upload strategy (Device::PreferDirectDeviceWrite), and order // the upload before (dstStageMask, dstAccessMask) on `cmd`: // ReBAR / UMA (direct) — allocate HOST_VISIBLE with DEVICE_LOCAL as a // best-effort preference (GetMemoryType lands the host-visible // device-local type that the strategy already proved exists), map // transiently, memcpy, flush if the chosen type isn't coherent, // unmap. No staging buffer: it would be pure overhead on a bar where // the device-local heap is itself host-writable. // No / small BAR (staged) — allocate pure DEVICE_LOCAL and copy from // the staging ring on the GPU (CopyThroughStaging). // The direct write is only for a fresh allocation, which nothing can be // reading yet. A same-size re-upload reuses the allocation (Resize), so // the device address stays stable across an in-place AS UPDATE refit // that re-calls this, but a frame in flight may still read it: that // upload always copies, on the GPU timeline, after those reads. // dstStageMask must name every stage that reads the buffer, since the // next upload waits for them. Requires !Mapped: the destination must be // free to be device-local-only (a persistent map would force // HOST_VISIBLE), so the direct path maps just long enough to write. void UploadDeviceLocal(VkBufferUsageFlags2 usageFlags, const T* src, std::uint32_t count, VkCommandBuffer cmd, VkAccessFlags dstAccessMask, VkPipelineStageFlags dstStageMask) requires(!Mapped) { // Nothing to put anywhere: no buffer of no bytes, no copy of none. if (count == 0) return; const VkDeviceSize bytes = static_cast(count) * sizeof(T); // Any upload may be followed by one that copies in. usageFlags |= VK_BUFFER_USAGE_TRANSFER_DST_BIT; const bool direct = Device::PreferDirectDeviceWrite(bytes); const VkBuffer previous = buffer; if (direct) { Resize(usageFlags, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, count, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); } else { Resize(usageFlags, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, count); } if (!direct || buffer == previous) { CopyThroughStaging(src, 0, count, cmd, dstAccessMask, dstStageMask); return; } void* mapped = nullptr; Device::CheckVkResult(vkMapMemory(Device::device, memory, 0, VK_WHOLE_SIZE, 0, &mapped)); std::memcpy(mapped, src, bytes); // Match FlushDevice()'s gate: a non-coherent type needs an // explicit flush before the device reads the written range. if (!(memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) { VkMappedMemoryRange range { .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, .memory = memory, .offset = 0, .size = VK_WHOLE_SIZE }; vkFlushMappedMemoryRanges(Device::device, 1, &range); } vkUnmapMemory(Device::device, memory); VkBufferMemoryBarrier barrier = { .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, .srcAccessMask = VK_ACCESS_HOST_WRITE_BIT, .dstAccessMask = dstAccessMask, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .buffer = buffer, .offset = 0, .size = VK_WHOLE_SIZE }; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_HOST_BIT, dstStageMask, 0, 0, NULL, 1, &barrier, 0, NULL); } // Re-upload only the half-open sub-range [offset, offset+count) of a // buffer UploadDeviceLocal made — the dirty-range counterpart of // UploadDeviceLocal. `src` points at the first changed element (not the // start of the whole array); `offset` is that element's index in this // buffer. This never Resizes, so the device address stays stable and the // untouched elements keep their last contents. Only the dirty elements // are staged and copied, and the barriers cover only that sub-range — so // a deforming-mesh refit that nudges a few vertices pays for those // vertices, not a full-array re-upload (#119). The buffer is one a frame // in flight may still read, so the copy waits for its readers // (dstStageMask, as in UploadDeviceLocal) on the GPU, whatever memory // type it lives in. void UploadDeviceLocalRange(const T* src, std::uint32_t offset, std::uint32_t count, VkCommandBuffer cmd, VkAccessFlags dstAccessMask, VkPipelineStageFlags dstStageMask) requires(!Mapped) { CopyThroughStaging(src, offset, count, cmd, dstAccessMask, dstStageMask); } // Copy `count` elements from `src` to element `offset` of this buffer on // the GPU timeline: after the reads by `dstStageMask` recorded earlier // or still in flight, and before the ones that follow. The bytes go // through this frame's slot of the staging ring first. void CopyThroughStaging(const T* src, std::uint32_t offset, std::uint32_t count, VkCommandBuffer cmd, VkAccessFlags dstAccessMask, VkPipelineStageFlags dstStageMask) requires(!Mapped) { // A copy or barrier of no bytes is invalid usage. if (count == 0) return; const VkDeviceSize byteOffset = static_cast(offset) * sizeof(T); const VkDeviceSize bytes = static_cast(count) * sizeof(T); // Write after read: an execution dependency on the earlier readers. VkBufferMemoryBarrier readsDone = { .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, .srcAccessMask = 0, .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .buffer = buffer, .offset = byteOffset, .size = bytes }; vkCmdPipelineBarrier(cmd, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 1, &readsDone, 0, NULL); const auto [staging, stagingOffset] = Stage(src, count); VkBufferCopy region { .srcOffset = stagingOffset, .dstOffset = byteOffset, .size = bytes }; vkCmdCopyBuffer(cmd, staging, buffer, 1, ®ion); VkBufferMemoryBarrier copied = { .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .dstAccessMask = dstAccessMask, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .buffer = buffer, .offset = byteOffset, .size = bytes }; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask, 0, 0, NULL, 1, &copied, 0, NULL); } // Put `count` elements in this frame's slot of the staging ring, after // whatever this frame staged before, and say where: the slot's buffer // and the byte offset. A copy still in flight never reads the slot, as // a slot is used again only framesInFlight frames later — the window // after which single-queue submission order (the same guarantee the // #101 deletion queue relies on) ensures that copy has completed. Each // slot keeps its high-water size (issue #120); outgrowing it mid-frame // hands the old allocation, with this frame's earlier bytes, to the // deletion queue. Without a window no frame passes; after a // Device::DrainDeletions (which follows a wait for the queue) nothing // reads any slot, so they all start over. std::pair Stage(const T* src, std::uint32_t count) requires(!Mapped) { const std::uint32_t ringSize = Device::framesInFlight ? Device::framesInFlight : 1; if (stagingRing.size() < ringSize) { stagingRing.resize(ringSize); stagingFrame.resize(ringSize, std::numeric_limits::max()); stagingUsed.resize(ringSize, 0); } if (stagingDrains != Device::drains) { stagingDrains = Device::drains; std::ranges::fill(stagingUsed, 0); } const std::size_t slot = Device::frameCounter % ringSize; if (stagingFrame[slot] != Device::frameCounter) { stagingFrame[slot] = Device::frameCounter; stagingUsed[slot] = 0; } VulkanBuffer& staging = stagingRing[slot]; const std::uint32_t needed = stagingUsed[slot] + count; if (staging.buffer == VK_NULL_HANDLE || static_cast(needed) * sizeof(T) > staging.capacity) { staging.Resize(VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, needed); stagingUsed[slot] = 0; } const std::uint32_t first = stagingUsed[slot]; std::memcpy(staging.value + first, src, static_cast(count) * sizeof(T)); staging.FlushDevice(static_cast(first) * sizeof(T), static_cast(count) * sizeof(T)); stagingUsed[slot] = first + count; return { staging.buffer, static_cast(first) * sizeof(T) }; } void FlushDevice() requires(Mapped) { // Coherent memory needs no explicit flush — host writes are // automatically visible to the device. if (memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) { return; } VkMappedMemoryRange range = { .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, .memory = memory, .offset = 0, .size = VK_WHOLE_SIZE }; vkFlushMappedMemoryRanges(Device::device, 1, &range); } // Flush only the host writes in [offset, offset+bytes) to the device, // instead of the whole buffer. Use after touching a small sub-range // (e.g. one descriptor in a multi-KB descriptor heap) so cache // maintenance scales with the bytes actually written. The range is // rounded outward to nonCoherentAtomSize as the Vulkan spec requires. // No-op on coherent memory, same gate as the whole-buffer FlushDevice(). void FlushDevice(VkDeviceSize offset, VkDeviceSize bytes) requires(Mapped) { if (memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) { return; } MappedFlushRange r = AlignMappedFlushRange( offset, bytes, Device::nonCoherentAtomSize, mappedSize); VkMappedMemoryRange range = { .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, .memory = memory, .offset = r.offset, .size = r.size }; vkFlushMappedMemoryRanges(Device::device, 1, &range); } void FlushDevice(VkCommandBuffer cmd, VkAccessFlags dstAccessMask, VkPipelineStageFlags dstStageMask) requires(Mapped) { FlushDevice(); VkBufferMemoryBarrier barrier = { .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, .srcAccessMask = VK_ACCESS_HOST_WRITE_BIT, .dstAccessMask = dstAccessMask, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .buffer = buffer, .offset = 0, .size = VK_WHOLE_SIZE }; vkCmdPipelineBarrier( cmd, VK_PIPELINE_STAGE_HOST_BIT, dstStageMask, 0, 0, NULL, 1, &barrier, 0, NULL ); } // Ranged variant of FlushDevice(cmd, ...): flushes only the host writes // in [offset, offset+bytes) (rounded outward to nonCoherentAtomSize, and // a no-op on coherent memory — same gate as the other FlushDevice // overloads) and records the HOST->(dstStageMask, dstAccessMask) barrier. // Use after writing a sub-range so the cache-flush cost scales with the // bytes touched rather than the whole high-water capacity. The barrier // itself still spans the whole buffer (VK_WHOLE_SIZE): the execution/ // visibility dependency is cheap regardless of range, and only the // flush's cache maintenance is bandwidth-sensitive. void FlushDevice(VkCommandBuffer cmd, VkAccessFlags dstAccessMask, VkPipelineStageFlags dstStageMask, VkDeviceSize offset, VkDeviceSize bytes) requires(Mapped) { FlushDevice(offset, bytes); VkBufferMemoryBarrier barrier = { .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, .srcAccessMask = VK_ACCESS_HOST_WRITE_BIT, .dstAccessMask = dstAccessMask, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .buffer = buffer, .offset = 0, .size = VK_WHOLE_SIZE }; vkCmdPipelineBarrier( cmd, VK_PIPELINE_STAGE_HOST_BIT, dstStageMask, 0, 0, NULL, 1, &barrier, 0, NULL ); } void FlushHost() requires(Mapped) { // Coherent memory needs no explicit invalidate — device writes are // automatically visible to the host. if (memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) { return; } VkMappedMemoryRange range = { .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, .memory = memory, .offset = 0, .size = VK_WHOLE_SIZE }; vkInvalidateMappedMemoryRanges(Device::device, 1, &range); } // Persistent per-frame-in-flight staging ring for the copies Stage // records (issue #120). Empty — and so holding zero host-visible // allocations — until the first upload that stages: on ReBAR/UMA only // re-uploads do, as a first upload writes directly. Sized to // Device::framesInFlight so each in-flight frame copies from its own // slot; each slot grows to a high-water mark via Resize and is reused // across frames rather than reallocated every call — mirroring the TLAS // instance/metadata reuse. stagingFrame and stagingUsed say, per slot, // which frame last staged into it and how many elements it has staged; // stagingDrains, the Device::drains it last saw. std::vector> stagingRing; std::vector stagingFrame; std::vector stagingUsed; std::uint64_t stagingDrains = 0; VulkanBuffer(VulkanBuffer&& other) noexcept { *this = std::move(other); } // Takes `other`'s buffer over. The one this held goes to the deletion // queue, as the destructor would send it: a frame in flight may still // read it. VulkanBuffer& operator=(VulkanBuffer&& other) noexcept { if (this == &other) return *this; DeferredClear(); buffer = other.buffer; memory = other.memory; size = other.size; mappedSize = other.mappedSize; capacity = other.capacity; usageFlagsCreated = other.usageFlagsCreated; memoryPropertyFlagsChosen = other.memoryPropertyFlagsChosen; other.buffer = VK_NULL_HANDLE; address = other.address; stagingRing = std::move(other.stagingRing); stagingFrame = std::move(other.stagingFrame); stagingUsed = std::move(other.stagingUsed); stagingDrains = other.stagingDrains; if constexpr(Mapped) { VulkanBufferMappedConditional::value = other.VulkanBufferMappedConditional::value; } return *this; } // Deferred, like DeferredClear: the owner may go while a frame in // flight still reads the buffer, and a static owner goes after the // driver has torn down. Call Clear() first to free at once when the // GPU is known to be idle. The staging ring's buffers defer the same way. ~VulkanBuffer() { DeferredClear(); } VulkanBuffer(VulkanBuffer&) = delete; VulkanBuffer& operator=(const VulkanBuffer&) = delete; }; } #endif // !CRAFTER_GRAPHICS_WINDOW_DOM