fix: add VkSampler cache to prevent sampler exhaustion crash

Validation layers revealed 9965 VkSamplers allocated against a device
limit of 4000 — every VkTexture created its own sampler even when
configurations were identical. This exhausted NVIDIA's sampler pool
and caused intermittent SIGSEGV in vkCmdBeginRenderPass.

Add a thread-safe sampler cache in VkContext that deduplicates samplers
by FNV-1a hash of all 14 VkSamplerCreateInfo fields. All texture,
render target, renderer, water, and loading screen sampler creation
now goes through getOrCreateSampler(). Textures set ownsSampler_=false
so shared samplers aren't double-freed.

Also auto-disable anisotropy in the cache when the physical device
doesn't support the samplerAnisotropy feature, fixing the validation
error VUID-VkSamplerCreateInfo-anisotropyEnable-01070.
This commit is contained in:
Kelsi 2026-03-24 11:44:54 -07:00
parent 1556559211
commit a152023e5e
10 changed files with 194 additions and 40 deletions

View file

@ -352,8 +352,8 @@ void WaterRenderer::destroySceneHistoryResources() {
if (sh.depthImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.depthImage, sh.depthAlloc); sh.depthImage = VK_NULL_HANDLE; sh.depthAlloc = VK_NULL_HANDLE; }
sh.sceneSet = VK_NULL_HANDLE;
}
if (sceneColorSampler) { vkDestroySampler(device, sceneColorSampler, nullptr); sceneColorSampler = VK_NULL_HANDLE; }
if (sceneDepthSampler) { vkDestroySampler(device, sceneDepthSampler, nullptr); sceneDepthSampler = VK_NULL_HANDLE; }
sceneColorSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
sceneDepthSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
sceneHistoryExtent = {0, 0};
sceneHistoryReady = false;
}
@ -374,13 +374,15 @@ void WaterRenderer::createSceneHistoryResources(VkExtent2D extent, VkFormat colo
sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
if (vkCreateSampler(device, &sampCI, nullptr, &sceneColorSampler) != VK_SUCCESS) {
sceneColorSampler = vkCtx->getOrCreateSampler(sampCI);
if (sceneColorSampler == VK_NULL_HANDLE) {
LOG_ERROR("WaterRenderer: failed to create scene color sampler");
return;
}
sampCI.magFilter = VK_FILTER_NEAREST;
sampCI.minFilter = VK_FILTER_NEAREST;
if (vkCreateSampler(device, &sampCI, nullptr, &sceneDepthSampler) != VK_SUCCESS) {
sceneDepthSampler = vkCtx->getOrCreateSampler(sampCI);
if (sceneDepthSampler == VK_NULL_HANDLE) {
LOG_ERROR("WaterRenderer: failed to create scene depth sampler");
return;
}
@ -1718,7 +1720,8 @@ void WaterRenderer::createReflectionResources() {
sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
if (vkCreateSampler(device, &sampCI, nullptr, &reflectionSampler) != VK_SUCCESS) {
reflectionSampler = vkCtx->getOrCreateSampler(sampCI);
if (reflectionSampler == VK_NULL_HANDLE) {
LOG_ERROR("WaterRenderer: failed to create reflection sampler");
return;
}
@ -1848,7 +1851,7 @@ void WaterRenderer::destroyReflectionResources() {
if (reflectionDepthView) { vkDestroyImageView(device, reflectionDepthView, nullptr); reflectionDepthView = VK_NULL_HANDLE; }
if (reflectionColorImage) { vmaDestroyImage(allocator, reflectionColorImage, reflectionColorAlloc); reflectionColorImage = VK_NULL_HANDLE; }
if (reflectionDepthImage) { vmaDestroyImage(allocator, reflectionDepthImage, reflectionDepthAlloc); reflectionDepthImage = VK_NULL_HANDLE; }
if (reflectionSampler) { vkDestroySampler(device, reflectionSampler, nullptr); reflectionSampler = VK_NULL_HANDLE; }
reflectionSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
if (reflectionUBO) {
AllocatedBuffer ab{}; ab.buffer = reflectionUBO; ab.allocation = reflectionUBOAlloc;
destroyBuffer(allocator, ab);