Crafter.Graphics/interfaces/Crafter.Graphics-VulkanBuffer.cppm
catbot 1f12f074b1 Merge remote-tracking branch 'origin/master' into claude/issue-61
# Conflicts:
#	interfaces/Crafter.Graphics-Device.cppm
#	interfaces/Crafter.Graphics-VulkanBuffer.cppm
#	project.cpp
2026-06-16 18:33:08 +00:00

322 lines
14 KiB
C++

/*
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<typename T>
class VulkanBufferMapped {
public:
T* value;
};
export class VulkanBufferMappedEmpty {};
template<typename T, bool Mapped>
using VulkanBufferMappedConditional =
std::conditional_t<
Mapped,
VulkanBufferMapped<T>,
VulkanBufferMappedEmpty
>;
export template <typename T, bool Mapped>
class VulkanBuffer : public VulkanBufferBase, public VulkanBufferMappedConditional<T, Mapped> {
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<void**>(&(VulkanBufferMappedConditional<T, true>::value))));
}
}
void Clear() {
if constexpr(Mapped) {
vkUnmapMemory(Device::device, memory);
}
vkDestroyBuffer(Device::device, buffer, nullptr);
vkFreeMemory(Device::device, memory, nullptr);
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;
}
if(buffer != VK_NULL_HANDLE) {
Clear();
}
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,
&copyRegion
);
}
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<T, true>::value = other.VulkanBufferMappedConditional<T, true>::value;
}
};
~VulkanBuffer() {
if(buffer != VK_NULL_HANDLE) {
Clear();
}
}
VulkanBuffer(VulkanBuffer&) = delete;
VulkanBuffer& operator=(const VulkanBuffer&) = delete;
};
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM