FIX: Memory error fix, debug scheme

This commit is contained in:
2025-11-01 01:21:41 +09:00
parent 235d9b2f83
commit d5ff6263ee
18 changed files with 609 additions and 95 deletions

View File

@@ -7,6 +7,22 @@ inline constexpr bool kUseValidationLayers = false;
inline constexpr bool kUseValidationLayers = true;
#endif
// VMA diagnostics (stats prints + JSON dumps + allocation naming)
// - Default: disabled to avoid noise and I/O at shutdown.
// - Enable at runtime by setting environment variable `VE_VMA_DEBUG=1`.
#include <cstdlib>
inline constexpr bool kEnableVmaDebugByDefault = false;
inline bool vmaDebugEnabled()
{
const char *env = std::getenv("VE_VMA_DEBUG");
if (env && *env)
{
// Accept 1/true/yes (case-insensitive)
return (*env == '1') || (*env == 'T') || (*env == 't') || (*env == 'Y') || (*env == 'y');
}
return kEnableVmaDebugByDefault;
}
// Shadow mapping configuration
inline constexpr int kShadowCascadeCount = 4;
// Maximum shadow distance for CSM in view-space units
@@ -22,9 +38,9 @@ inline constexpr float kShadowCascadeRadiusMargin = 10.0f;
inline constexpr float kShadowClipBaseRadius = 20.0f;
// When using dynamic pullback, compute it from the covered XY range of each level.
// pullback = max(kShadowClipPullbackMin, cover * kShadowClipPullbackFactor)
inline constexpr float kShadowClipPullbackFactor = 2.5f; // fraction of XY half-size behind center
inline constexpr float kShadowClipForwardFactor = 2.5f; // fraction of XY half-size in front of center for zFar
inline constexpr float kShadowClipPullbackMin = 160.0f; // lower bound on pullback so near levels dont collapse
inline constexpr float kShadowClipPullbackFactor = 1.5f; // fraction of XY half-size behind center
inline constexpr float kShadowClipForwardFactor = 1.5f; // fraction of XY half-size in front of center for zFar
inline constexpr float kShadowClipPullbackMin = 40.0f; // lower bound on pullback so near levels dont collapse
// Additional Z padding for the orthographic frustum along light direction
inline constexpr float kShadowClipZPadding = 40.0f;

View File

@@ -9,7 +9,9 @@ void DescriptorManager::init(DeviceManager *deviceManager)
{
DescriptorLayoutBuilder builder;
builder.add_binding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
_singleImageDescriptorLayout = builder.build(_deviceManager->device(), VK_SHADER_STAGE_FRAGMENT_BIT);
_singleImageDescriptorLayout = builder.build(
_deviceManager->device(), VK_SHADER_STAGE_FRAGMENT_BIT,
nullptr, VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT);
} {
DescriptorLayoutBuilder builder;
builder.add_binding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
@@ -19,7 +21,8 @@ void DescriptorManager::init(DeviceManager *deviceManager)
builder.add_binding(1, VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR);
}
_gpuSceneDataDescriptorLayout = builder.build(
_deviceManager->device(), VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);
_deviceManager->device(), VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
nullptr, VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT);
}
}

View File

@@ -77,10 +77,13 @@ void DescriptorWriter::write_image(int binding, VkImageView image, VkSampler sam
void DescriptorWriter::write_acceleration_structure(int binding, VkAccelerationStructureKHR as)
{
// Store the handle to ensure the pointer we give to Vulkan stays valid
VkAccelerationStructureKHR &storedAS = accelHandles.emplace_back(as);
VkWriteDescriptorSetAccelerationStructureKHR &acc = accelInfos.emplace_back(
VkWriteDescriptorSetAccelerationStructureKHR{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET_ACCELERATION_STRUCTURE_KHR });
acc.accelerationStructureCount = 1;
acc.pAccelerationStructures = &as;
acc.pAccelerationStructures = &storedAS;
VkWriteDescriptorSet write{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET };
write.dstBinding = binding;
@@ -95,6 +98,8 @@ void DescriptorWriter::clear()
imageInfos.clear();
writes.clear();
bufferInfos.clear();
accelInfos.clear();
accelHandles.clear();
}
void DescriptorWriter::update_set(VkDevice device, VkDescriptorSet set)
@@ -118,7 +123,10 @@ void DescriptorAllocator::init_pool(VkDevice device, uint32_t maxSets, std::span
}
VkDescriptorPoolCreateInfo pool_info = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool_info.flags = 0;
// Enable update-after-bind so descriptors used by previous frame can be
// safely rewritten (e.g., compute instances). It is valid to allocate
// non-update-after-bind sets from such a pool.
pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT;
pool_info.maxSets = maxSets;
pool_info.poolSizeCount = (uint32_t) poolSizes.size();
pool_info.pPoolSizes = poolSizes.data();
@@ -187,7 +195,8 @@ VkDescriptorPool DescriptorAllocatorGrowable::create_pool(VkDevice device, uint3
VkDescriptorPoolCreateInfo pool_info = {};
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
pool_info.flags = 0;
// Use update-after-bind pools to support cross-frame rewrites.
pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT;
pool_info.maxSets = setCount;
pool_info.poolSizeCount = (uint32_t) poolSizes.size();
pool_info.pPoolSizes = poolSizes.data();

View File

@@ -20,6 +20,8 @@ struct DescriptorWriter
std::deque<VkDescriptorImageInfo> imageInfos;
std::deque<VkDescriptorBufferInfo> bufferInfos;
std::deque<VkWriteDescriptorSetAccelerationStructureKHR> accelInfos;
// Keep AS handles alive so pAccelerationStructures points to valid memory
std::deque<VkAccelerationStructureKHR> accelHandles;
std::vector<VkWriteDescriptorSet> writes;
void write_image(int binding, VkImageView image, VkSampler sampler, VkImageLayout layout, VkDescriptorType type);

View File

@@ -30,8 +30,16 @@ void DeviceManager::init_vulkan(SDL_Window *window)
features.synchronization2 = true;
VkPhysicalDeviceVulkan12Features features12{.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES};
features12.bufferDeviceAddress = true;
features12.descriptorIndexing = true;
features12.bufferDeviceAddress = VK_TRUE;
features12.descriptorIndexing = VK_TRUE;
// Enable update-after-bind related toggles for graphics/compute descriptors
features12.descriptorBindingPartiallyBound = VK_TRUE;
features12.descriptorBindingUpdateUnusedWhilePending = VK_TRUE;
features12.runtimeDescriptorArray = VK_TRUE;
features12.descriptorBindingUniformBufferUpdateAfterBind = VK_TRUE;
features12.descriptorBindingStorageBufferUpdateAfterBind = VK_TRUE;
features12.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE;
features12.descriptorBindingStorageImageUpdateAfterBind = VK_TRUE;
//use vkbootstrap to select a gpu.
//We want a gpu that can write to the SDL surface and supports vulkan 1.3
@@ -72,14 +80,16 @@ void DeviceManager::init_vulkan(SDL_Window *window)
//create the final vulkan device
vkb::DeviceBuilder deviceBuilder{physicalDevice};
// Enable ray query + accel struct features in device create pNext if supported
// Ray features are optional and enabled only if supported on the chosen GPU
VkPhysicalDeviceAccelerationStructureFeaturesKHR accelReq{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR };
VkPhysicalDeviceRayQueryFeaturesKHR rayqReq{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR };
if (_rayQuerySupported && _accelStructSupported)
{
VkPhysicalDeviceAccelerationStructureFeaturesKHR accelReq{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR };
accelReq.accelerationStructure = VK_TRUE;
VkPhysicalDeviceRayQueryFeaturesKHR rayqReq{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR };
rayqReq.pNext = &accelReq;
rayqReq.rayQuery = VK_TRUE;
rayqReq.pNext = &accelReq;
}
if (_rayQuerySupported && _accelStructSupported) {
deviceBuilder.add_pNext(&rayqReq);
}
@@ -111,6 +121,18 @@ void DeviceManager::init_vulkan(SDL_Window *window)
void DeviceManager::cleanup()
{
// Optional VMA stats print
if (_allocator && vmaDebugEnabled())
{
VmaTotalStatistics stats{};
vmaCalculateStatistics(_allocator, &stats);
const VmaStatistics& s = stats.total.statistics;
fmt::print("[VMA] Blocks: {} | Allocations: {} | BlockBytes: {} | AllocationBytes: {}\n",
(size_t)s.blockCount,
(size_t)s.allocationCount,
(unsigned long long)s.blockBytes,
(unsigned long long)s.allocationBytes);
}
vkDestroySurfaceKHR(_instance, _surface, nullptr);
_deletionQueue.flush();
vkDestroyDevice(_device, nullptr);

View File

@@ -53,6 +53,46 @@
VulkanEngine *loadedEngine = nullptr;
static void print_vma_stats(DeviceManager* dev, const char* tag)
{
if (!vmaDebugEnabled()) return;
if (!dev) return;
VmaAllocator alloc = dev->allocator();
if (!alloc) return;
VmaTotalStatistics stats{};
vmaCalculateStatistics(alloc, &stats);
const VmaStatistics &s = stats.total.statistics;
fmt::print("[VMA][{}] Blocks:{} Allocs:{} BlockBytes:{} AllocBytes:{}\n",
tag,
(size_t)s.blockCount,
(size_t)s.allocationCount,
(unsigned long long)s.blockBytes,
(unsigned long long)s.allocationBytes);
}
static void dump_vma_json(DeviceManager* dev, const char* tag)
{
if (!vmaDebugEnabled()) return;
if (!dev) return;
VmaAllocator alloc = dev->allocator();
if (!alloc) return;
char* json = nullptr;
vmaBuildStatsString(alloc, &json, VK_TRUE);
if (json)
{
// Write to a small temp file beside the binary
std::string fname = std::string("vma_") + tag + ".json";
FILE* f = fopen(fname.c_str(), "wb");
if (f)
{
fwrite(json, 1, strlen(json), f);
fclose(f);
fmt::print("[VMA] Wrote {}\n", fname);
}
vmaFreeStatsString(alloc, json);
}
}
void VulkanEngine::init()
{
// We initialize SDL and create a window with it.
@@ -150,7 +190,7 @@ void VulkanEngine::init()
auto imguiPass = std::make_unique<ImGuiPass>();
_renderPassManager->setImGuiPass(std::move(imguiPass));
const std::string structurePath = _assetManager->modelPath("police_office.glb");
const std::string structurePath = _assetManager->modelPath("seoul_high.glb");
const auto structureFile = _assetManager->loadGLTF(structurePath);
assert(structureFile.has_value());
@@ -233,7 +273,11 @@ void VulkanEngine::cleanup()
{
vkDeviceWaitIdle(_deviceManager->device());
print_vma_stats(_deviceManager.get(), "begin");
_sceneManager->cleanup();
print_vma_stats(_deviceManager.get(), "after SceneManager");
dump_vma_json(_deviceManager.get(), "after_SceneManager");
if (_isInitialized)
{
@@ -253,24 +297,53 @@ void VulkanEngine::cleanup()
metalRoughMaterial.clear_resources(_deviceManager->device());
_mainDeletionQueue.flush();
print_vma_stats(_deviceManager.get(), "after MainDQ flush");
dump_vma_json(_deviceManager.get(), "after_MainDQ");
_renderPassManager->cleanup();
print_vma_stats(_deviceManager.get(), "after RenderPassManager");
dump_vma_json(_deviceManager.get(), "after_RenderPassManager");
_pipelineManager->cleanup();
print_vma_stats(_deviceManager.get(), "after PipelineManager");
dump_vma_json(_deviceManager.get(), "after_PipelineManager");
compute.cleanup();
print_vma_stats(_deviceManager.get(), "after Compute");
dump_vma_json(_deviceManager.get(), "after_Compute");
_swapchainManager->cleanup();
print_vma_stats(_deviceManager.get(), "after Swapchain");
dump_vma_json(_deviceManager.get(), "after_Swapchain");
if (_assetManager) _assetManager->cleanup();
print_vma_stats(_deviceManager.get(), "after AssetManager");
dump_vma_json(_deviceManager.get(), "after_AssetManager");
// Ensure ray tracing resources (BLAS/TLAS/instance buffers) are freed before VMA is destroyed
if (_rayManager) { _rayManager->cleanup(); }
print_vma_stats(_deviceManager.get(), "after RTManager");
dump_vma_json(_deviceManager.get(), "after_RTManager");
_resourceManager->cleanup();
print_vma_stats(_deviceManager.get(), "after ResourceManager");
dump_vma_json(_deviceManager.get(), "after_ResourceManager");
_samplerManager->cleanup();
_descriptorManager->cleanup();
print_vma_stats(_deviceManager.get(), "after Samplers+Descriptors");
dump_vma_json(_deviceManager.get(), "after_Samplers_Descriptors");
_context->descriptors->destroy_pools(_deviceManager->device());
// Extra safety: flush frame deletion queues once more before destroying VMA
for (int i = 0; i < FRAME_OVERLAP; i++)
{
_frames[i]._deletionQueue.flush();
}
print_vma_stats(_deviceManager.get(), "before DeviceManager");
dump_vma_json(_deviceManager.get(), "before_DeviceManager");
_deviceManager->cleanup();
SDL_DestroyWindow(_window);
@@ -280,11 +353,6 @@ void VulkanEngine::cleanup()
void VulkanEngine::draw()
{
_sceneManager->update_scene();
// Build or update TLAS for current frame if RT mode enabled (1 or 2)
if (_rayManager && _context->shadowSettings.mode != 0u)
{
_rayManager->buildTLASFromDrawContext(_context->getMainDrawContext());
}
//> frame_clear
//wait until the gpu has finished rendering the last frame. Timeout of 1 second
VK_CHECK(vkWaitForFences(_deviceManager->device(), 1, &get_current_frame()._renderFence, true, 1000000000));
@@ -319,6 +387,12 @@ void VulkanEngine::draw()
//now that we are sure that the commands finished executing, we can safely reset the command buffer to begin recording again.
VK_CHECK(vkResetCommandBuffer(get_current_frame()._mainCommandBuffer, 0));
// Build or update TLAS for current frame now that the previous frame is idle
if (_rayManager && _context->shadowSettings.mode != 0u)
{
_rayManager->buildTLASFromDrawContext(_context->getMainDrawContext(), get_current_frame()._deletionQueue);
}
//naming it cmd for shorter writing
VkCommandBuffer cmd = get_current_frame()._mainCommandBuffer;

View File

@@ -21,6 +21,12 @@ void RayTracingManager::init(DeviceManager *dev, ResourceManager *res)
vkGetDeviceProcAddr(_device->device(), "vkCmdBuildAccelerationStructuresKHR"));
_vkGetAccelerationStructureDeviceAddressKHR = reinterpret_cast<PFN_vkGetAccelerationStructureDeviceAddressKHR>(
vkGetDeviceProcAddr(_device->device(), "vkGetAccelerationStructureDeviceAddressKHR"));
// Query AS properties for scratch alignment
VkPhysicalDeviceAccelerationStructurePropertiesKHR asProps{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_PROPERTIES_KHR };
VkPhysicalDeviceProperties2 props2{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &asProps };
vkGetPhysicalDeviceProperties2(_device->physicalDevice(), &props2);
_minScratchAlignment = std::max<VkDeviceSize>(asProps.minAccelerationStructureScratchOffsetAlignment, 256);
}
void RayTracingManager::cleanup()
@@ -150,11 +156,15 @@ AccelStructureHandle RayTracingManager::getOrBuildBLAS(const std::shared_ptr<Mes
asci.size = sizes.accelerationStructureSize;
VK_CHECK(_vkCreateAccelerationStructureKHR(_device->device(), &asci, nullptr, &blas.handle));
AllocatedBuffer scratch = _resources->create_buffer(sizes.buildScratchSize,
// Allocate scratch with padding to satisfy alignment requirements
const VkDeviceSize align = _minScratchAlignment;
const VkDeviceSize padded = sizes.buildScratchSize + (align - 1);
AllocatedBuffer scratch = _resources->create_buffer(padded,
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VMA_MEMORY_USAGE_GPU_ONLY);
VkDeviceAddress scratchAddr = get_buffer_address(_device->device(), scratch.buffer);
VkDeviceAddress scratchBase = get_buffer_address(_device->device(), scratch.buffer);
VkDeviceAddress scratchAddr = (scratchBase + (align - 1)) & ~VkDeviceAddress(align - 1);
buildInfo.dstAccelerationStructure = blas.handle;
buildInfo.scratchData.deviceAddress = scratchAddr;
@@ -178,18 +188,20 @@ AccelStructureHandle RayTracingManager::getOrBuildBLAS(const std::shared_ptr<Mes
return blas;
}
void RayTracingManager::ensure_tlas_storage(VkDeviceSize requiredASSize, VkDeviceSize /*requiredScratch*/)
void RayTracingManager::ensure_tlas_storage(VkDeviceSize requiredASSize, VkDeviceSize /*requiredScratch*/, DeletionQueue& dq)
{
// Simple: recreate TLAS storage if size grows
if (_tlas.handle)
// Recreate TLAS storage if size grows. Defer destruction to the frame DQ to
// avoid freeing while referenced by in-flight frames.
if (_tlas.handle || _tlas.storage.buffer)
{
_vkDestroyAccelerationStructureKHR(_device->device(), _tlas.handle, nullptr);
_tlas.handle = VK_NULL_HANDLE;
}
if (_tlas.storage.buffer)
{
_resources->destroy_buffer(_tlas.storage);
_tlas.storage = {};
AccelStructureHandle old = _tlas;
dq.push_function([this, old]() {
if (old.handle)
_vkDestroyAccelerationStructureKHR(_device->device(), old.handle, nullptr);
if (old.storage.buffer)
_resources->destroy_buffer(old.storage);
});
_tlas = {};
}
_tlas.storage = _resources->create_buffer(requiredASSize,
VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR |
@@ -203,7 +215,7 @@ void RayTracingManager::ensure_tlas_storage(VkDeviceSize requiredASSize, VkDevic
VK_CHECK(_vkCreateAccelerationStructureKHR(_device->device(), &asci, nullptr, &_tlas.handle));
}
VkAccelerationStructureKHR RayTracingManager::buildTLASFromDrawContext(const DrawContext &dc)
VkAccelerationStructureKHR RayTracingManager::buildTLASFromDrawContext(const DrawContext &dc, DeletionQueue& dq)
{
// Collect instances; one per render object (opaque only).
std::vector<VkAccelerationStructureInstanceKHR> instances;
@@ -239,8 +251,19 @@ VkAccelerationStructureKHR RayTracingManager::buildTLASFromDrawContext(const Dra
if (instances.empty())
{
// nothing to build
return _tlas.handle;
// No instances this frame: defer TLAS destruction to avoid racing with previous frames
if (_tlas.handle || _tlas.storage.buffer)
{
AccelStructureHandle old = _tlas;
dq.push_function([this, old]() {
if (old.handle)
_vkDestroyAccelerationStructureKHR(_device->device(), old.handle, nullptr);
if (old.storage.buffer)
_resources->destroy_buffer(old.storage);
});
_tlas = {};
}
return VK_NULL_HANDLE;
}
// Ensure instance buffer capacity
@@ -293,15 +316,18 @@ VkAccelerationStructureKHR RayTracingManager::buildTLASFromDrawContext(const Dra
_vkGetAccelerationStructureBuildSizesKHR(_device->device(), VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR,
&buildInfo, &primCount, &sizes);
ensure_tlas_storage(sizes.accelerationStructureSize, sizes.buildScratchSize);
ensure_tlas_storage(sizes.accelerationStructureSize, sizes.buildScratchSize, dq);
buildInfo.dstAccelerationStructure = _tlas.handle;
AllocatedBuffer scratch = _resources->create_buffer(sizes.buildScratchSize,
const VkDeviceSize align2 = _minScratchAlignment;
const VkDeviceSize padded2 = sizes.buildScratchSize + (align2 - 1);
AllocatedBuffer scratch = _resources->create_buffer(padded2,
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VMA_MEMORY_USAGE_GPU_ONLY);
VkDeviceAddress scratchAddr = get_buffer_address(_device->device(), scratch.buffer);
buildInfo.scratchData.deviceAddress = scratchAddr;
VkDeviceAddress scratchBase2 = get_buffer_address(_device->device(), scratch.buffer);
VkDeviceAddress scratchAddr2 = (scratchBase2 + (align2 - 1)) & ~VkDeviceAddress(align2 - 1);
buildInfo.scratchData.deviceAddress = scratchAddr2;
VkAccelerationStructureBuildRangeInfoKHR range{};
range.primitiveCount = primCount;

View File

@@ -25,8 +25,9 @@ public:
// Build (or get) BLAS for a mesh. Safe to call multiple times.
AccelStructureHandle getOrBuildBLAS(const std::shared_ptr<MeshAsset>& mesh);
// Rebuild TLAS from current draw context; returns TLAS handle (or null if unavailable)
VkAccelerationStructureKHR buildTLASFromDrawContext(const DrawContext& dc);
// Rebuild TLAS from current draw context; returns TLAS handle (or null if unavailable)
// Destruction of previous TLAS resources is deferred via the provided frame deletion queue
VkAccelerationStructureKHR buildTLASFromDrawContext(const DrawContext& dc, DeletionQueue& frameDQ);
VkAccelerationStructureKHR tlas() const { return _tlas.handle; }
VkDeviceAddress tlasAddress() const { return _tlas.deviceAddress; }
@@ -34,7 +35,7 @@ public:
// Safe to call even if no BLAS exists for the buffer.
void removeBLASForBuffer(VkBuffer vertexBuffer);
private:
private:
// function pointers (resolved on init)
PFN_vkCreateAccelerationStructureKHR _vkCreateAccelerationStructureKHR{};
PFN_vkDestroyAccelerationStructureKHR _vkDestroyAccelerationStructureKHR{};
@@ -42,17 +43,20 @@ public:
PFN_vkCmdBuildAccelerationStructuresKHR _vkCmdBuildAccelerationStructuresKHR{};
PFN_vkGetAccelerationStructureDeviceAddressKHR _vkGetAccelerationStructureDeviceAddressKHR{};
DeviceManager* _device{nullptr};
ResourceManager* _resources{nullptr};
DeviceManager* _device{nullptr};
ResourceManager* _resources{nullptr};
// BLAS cache by vertex buffer handle
std::unordered_map<VkBuffer, AccelStructureHandle> _blasByVB;
// TLAS + scratch / instance buffer (rebuilt per frame)
AccelStructureHandle _tlas{};
AllocatedBuffer _tlasInstanceBuffer{};
size_t _tlasInstanceCapacity{0};
// TLAS + scratch / instance buffer (rebuilt per frame)
AccelStructureHandle _tlas{};
AllocatedBuffer _tlasInstanceBuffer{};
size_t _tlasInstanceCapacity{0};
// Properties
VkDeviceSize _minScratchAlignment{256};
void ensure_tlas_storage(VkDeviceSize requiredASSize, VkDeviceSize requiredScratch);
};
void ensure_tlas_storage(VkDeviceSize requiredASSize, VkDeviceSize requiredScratch, DeletionQueue& frameDQ);
};