/* Crafter®.Graphics Copyright (C) 2026 Catcrafts® catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ 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 }; VkMemoryAllocateFlagsInfoKHR allocFlagsInfo { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO_KHR, .flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT_KHR, }; 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 = vkGetBufferDeviceAddress(Device::device, &addressInfo); if constexpr(Mapped) { mappedSize = memReqs.size; 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 ); } 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 ); } 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); } VulkanBuffer(VulkanBuffer&& other) { 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; if constexpr(Mapped) { VulkanBufferMappedConditional::value = other.VulkanBufferMappedConditional::value; } }; ~VulkanBuffer() { if(buffer != VK_NULL_HANDLE) { Clear(); } } VulkanBuffer(VulkanBuffer&) = delete; VulkanBuffer& operator=(const VulkanBuffer&) = delete; }; } #endif // !CRAFTER_GRAPHICS_WINDOW_DOM