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

@ -343,7 +343,8 @@ bool Renderer::createPerFrameResources() {
sampCI.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
sampCI.compareEnable = VK_TRUE;
sampCI.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
if (vkCreateSampler(device, &sampCI, nullptr, &shadowSampler) != VK_SUCCESS) {
shadowSampler = vkCtx->getOrCreateSampler(sampCI);
if (shadowSampler == VK_NULL_HANDLE) {
LOG_ERROR("Failed to create shadow sampler");
return false;
}
@ -597,7 +598,7 @@ void Renderer::destroyPerFrameResources() {
shadowDepthLayout_[i] = VK_IMAGE_LAYOUT_UNDEFINED;
}
if (shadowRenderPass) { vkDestroyRenderPass(device, shadowRenderPass, nullptr); shadowRenderPass = VK_NULL_HANDLE; }
if (shadowSampler) { vkDestroySampler(device, shadowSampler, nullptr); shadowSampler = VK_NULL_HANDLE; }
shadowSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
}
void Renderer::updatePerFrameUBO() {
@ -4057,7 +4058,8 @@ bool Renderer::initFSRResources() {
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
if (vkCreateSampler(device, &samplerInfo, nullptr, &fsr_.sceneSampler) != VK_SUCCESS) {
fsr_.sceneSampler = vkCtx->getOrCreateSampler(samplerInfo);
if (fsr_.sceneSampler == VK_NULL_HANDLE) {
LOG_ERROR("FSR: failed to create sampler");
destroyFSRResources();
return false;
@ -4171,7 +4173,7 @@ void Renderer::destroyFSRResources() {
if (fsr_.descPool) { vkDestroyDescriptorPool(device, fsr_.descPool, nullptr); fsr_.descPool = VK_NULL_HANDLE; fsr_.descSet = VK_NULL_HANDLE; }
if (fsr_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fsr_.descSetLayout, nullptr); fsr_.descSetLayout = VK_NULL_HANDLE; }
if (fsr_.sceneFramebuffer) { vkDestroyFramebuffer(device, fsr_.sceneFramebuffer, nullptr); fsr_.sceneFramebuffer = VK_NULL_HANDLE; }
if (fsr_.sceneSampler) { vkDestroySampler(device, fsr_.sceneSampler, nullptr); fsr_.sceneSampler = VK_NULL_HANDLE; }
fsr_.sceneSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
destroyImage(device, alloc, fsr_.sceneDepthResolve);
destroyImage(device, alloc, fsr_.sceneMsaaColor);
destroyImage(device, alloc, fsr_.sceneDepth);
@ -4350,11 +4352,11 @@ bool Renderer::initFSR2Resources() {
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
vkCreateSampler(device, &samplerInfo, nullptr, &fsr2_.linearSampler);
fsr2_.linearSampler = vkCtx->getOrCreateSampler(samplerInfo);
samplerInfo.minFilter = VK_FILTER_NEAREST;
samplerInfo.magFilter = VK_FILTER_NEAREST;
vkCreateSampler(device, &samplerInfo, nullptr, &fsr2_.nearestSampler);
fsr2_.nearestSampler = vkCtx->getOrCreateSampler(samplerInfo);
#if WOWEE_HAS_AMD_FSR2
// Initialize AMD FSR2 context; fall back to internal path on any failure.
@ -4753,8 +4755,8 @@ void Renderer::destroyFSR2Resources() {
if (fsr2_.motionVecDescSetLayout) { vkDestroyDescriptorSetLayout(device, fsr2_.motionVecDescSetLayout, nullptr); fsr2_.motionVecDescSetLayout = VK_NULL_HANDLE; }
if (fsr2_.sceneFramebuffer) { vkDestroyFramebuffer(device, fsr2_.sceneFramebuffer, nullptr); fsr2_.sceneFramebuffer = VK_NULL_HANDLE; }
if (fsr2_.linearSampler) { vkDestroySampler(device, fsr2_.linearSampler, nullptr); fsr2_.linearSampler = VK_NULL_HANDLE; }
if (fsr2_.nearestSampler) { vkDestroySampler(device, fsr2_.nearestSampler, nullptr); fsr2_.nearestSampler = VK_NULL_HANDLE; }
fsr2_.linearSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
fsr2_.nearestSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
destroyImage(device, alloc, fsr2_.motionVectors);
for (int i = 0; i < 2; i++) destroyImage(device, alloc, fsr2_.history[i]);
@ -5273,7 +5275,8 @@ bool Renderer::initFXAAResources() {
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
if (vkCreateSampler(device, &samplerInfo, nullptr, &fxaa_.sceneSampler) != VK_SUCCESS) {
fxaa_.sceneSampler = vkCtx->getOrCreateSampler(samplerInfo);
if (fxaa_.sceneSampler == VK_NULL_HANDLE) {
LOG_ERROR("FXAA: failed to create sampler");
destroyFXAAResources();
return false;
@ -5383,7 +5386,7 @@ void Renderer::destroyFXAAResources() {
if (fxaa_.descPool) { vkDestroyDescriptorPool(device, fxaa_.descPool, nullptr); fxaa_.descPool = VK_NULL_HANDLE; fxaa_.descSet = VK_NULL_HANDLE; }
if (fxaa_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fxaa_.descSetLayout, nullptr); fxaa_.descSetLayout = VK_NULL_HANDLE; }
if (fxaa_.sceneFramebuffer) { vkDestroyFramebuffer(device, fxaa_.sceneFramebuffer, nullptr); fxaa_.sceneFramebuffer = VK_NULL_HANDLE; }
if (fxaa_.sceneSampler) { vkDestroySampler(device, fxaa_.sceneSampler, nullptr); fxaa_.sceneSampler = VK_NULL_HANDLE; }
fxaa_.sceneSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
destroyImage(device, alloc, fxaa_.sceneDepthResolve);
destroyImage(device, alloc, fxaa_.sceneMsaaColor);
destroyImage(device, alloc, fxaa_.sceneDepth);