From c8c01f8ac005f2ba384101d69bd0cdddd545273e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 24 Mar 2026 09:47:03 -0700 Subject: [PATCH] perf: add Vulkan pipeline cache persistence for faster startup Create a VkPipelineCache at device init, loaded from disk if available. All 65 pipeline creation calls across 19 renderer files now use the shared cache. On shutdown, the cache is serialized to disk so subsequent launches skip redundant shader compilation. Cache path: ~/.local/share/wowee/pipeline_cache.bin (Linux), ~/Library/Caches/wowee/ (macOS), %APPDATA%\wowee\ (Windows). Stale/corrupt caches are handled gracefully (fallback to empty cache). --- include/rendering/vk_context.hpp | 6 ++ include/rendering/vk_pipeline.hpp | 4 +- src/rendering/celestial.cpp | 4 +- src/rendering/character_renderer.cpp | 6 +- src/rendering/charge_effect.cpp | 8 +- src/rendering/clouds.cpp | 4 +- src/rendering/lens_flare.cpp | 4 +- src/rendering/lightning.cpp | 8 +- src/rendering/m2_renderer.cpp | 18 ++-- src/rendering/minimap.cpp | 6 +- src/rendering/mount_dust.cpp | 4 +- src/rendering/quest_marker_renderer.cpp | 4 +- src/rendering/renderer.cpp | 10 +-- src/rendering/skybox.cpp | 4 +- src/rendering/starfield.cpp | 4 +- src/rendering/swim_effects.cpp | 12 +-- src/rendering/terrain_renderer.cpp | 10 +-- src/rendering/vk_context.cpp | 114 ++++++++++++++++++++++++ src/rendering/vk_pipeline.cpp | 4 +- src/rendering/water_renderer.cpp | 6 +- src/rendering/weather.cpp | 4 +- src/rendering/wmo_renderer.cpp | 18 ++-- src/rendering/world_map.cpp | 2 +- 23 files changed, 192 insertions(+), 72 deletions(-) diff --git a/include/rendering/vk_context.hpp b/include/rendering/vk_context.hpp index 654729b3..a9186439 100644 --- a/include/rendering/vk_context.hpp +++ b/include/rendering/vk_context.hpp @@ -74,6 +74,7 @@ public: uint32_t getGraphicsQueueFamily() const { return graphicsQueueFamily; } VmaAllocator getAllocator() const { return allocator; } VkSurfaceKHR getSurface() const { return surface; } + VkPipelineCache getPipelineCache() const { return pipelineCache_; } VkSwapchainKHR getSwapchain() const { return swapchain; } VkFormat getSwapchainFormat() const { return swapchainFormat; } @@ -130,6 +131,8 @@ private: void destroySwapchain(); bool createCommandPools(); bool createSyncObjects(); + bool createPipelineCache(); + void savePipelineCache(); bool createImGuiResources(); void destroyImGuiResources(); @@ -144,6 +147,9 @@ private: VkDevice device = VK_NULL_HANDLE; VmaAllocator allocator = VK_NULL_HANDLE; + // Pipeline cache (persisted to disk for faster startup) + VkPipelineCache pipelineCache_ = VK_NULL_HANDLE; + VkQueue graphicsQueue = VK_NULL_HANDLE; VkQueue presentQueue = VK_NULL_HANDLE; uint32_t graphicsQueueFamily = 0; diff --git a/include/rendering/vk_pipeline.hpp b/include/rendering/vk_pipeline.hpp index ea0a3e10..e95337f8 100644 --- a/include/rendering/vk_pipeline.hpp +++ b/include/rendering/vk_pipeline.hpp @@ -75,8 +75,8 @@ public: // Dynamic state PipelineBuilder& setDynamicStates(const std::vector& states); - // Build the pipeline - VkPipeline build(VkDevice device) const; + // Build the pipeline (pass a VkPipelineCache for faster creation) + VkPipeline build(VkDevice device, VkPipelineCache cache = VK_NULL_HANDLE) const; // Common blend states static VkPipelineColorBlendAttachmentState blendDisabled(); diff --git a/src/rendering/celestial.cpp b/src/rendering/celestial.cpp index 798ac5d5..ad7804ba 100644 --- a/src/rendering/celestial.cpp +++ b/src/rendering/celestial.cpp @@ -90,7 +90,7 @@ bool Celestial::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -162,7 +162,7 @@ void Celestial::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 6b4e00b8..d709f0c9 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -264,7 +264,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; opaquePipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); @@ -2648,7 +2648,7 @@ bool CharacterRenderer::initializeShadow(VkRenderPass shadowRenderPass) { .setLayout(shadowPipelineLayout_) .setRenderPass(shadowRenderPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -3315,7 +3315,7 @@ void CharacterRenderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; LOG_INFO("CharacterRenderer::recreatePipelines: renderPass=", (void*)mainPass, diff --git a/src/rendering/charge_effect.cpp b/src/rendering/charge_effect.cpp index d6fba4de..32a3b36d 100644 --- a/src/rendering/charge_effect.cpp +++ b/src/rendering/charge_effect.cpp @@ -101,7 +101,7 @@ bool ChargeEffect::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayo .setLayout(ribbonPipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -165,7 +165,7 @@ bool ChargeEffect::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayo .setLayout(dustPipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -314,7 +314,7 @@ void ChargeEffect::recreatePipelines() { .setLayout(ribbonPipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -360,7 +360,7 @@ void ChargeEffect::recreatePipelines() { .setLayout(dustPipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/clouds.cpp b/src/rendering/clouds.cpp index eb2a5a25..6b682850 100644 --- a/src/rendering/clouds.cpp +++ b/src/rendering/clouds.cpp @@ -83,7 +83,7 @@ bool Clouds::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -149,7 +149,7 @@ void Clouds::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/lens_flare.cpp b/src/rendering/lens_flare.cpp index 3dd6b734..e9a9bb04 100644 --- a/src/rendering/lens_flare.cpp +++ b/src/rendering/lens_flare.cpp @@ -109,7 +109,7 @@ bool LensFlare::initialize(VkContext* ctx, VkDescriptorSetLayout /*perFrameLayou .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); // Shader modules can be freed after pipeline creation vertModule.destroy(); @@ -198,7 +198,7 @@ void LensFlare::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/lightning.cpp b/src/rendering/lightning.cpp index 9dbd1b95..b7d28c1d 100644 --- a/src/rendering/lightning.cpp +++ b/src/rendering/lightning.cpp @@ -107,7 +107,7 @@ bool Lightning::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setLayout(boltPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -169,7 +169,7 @@ bool Lightning::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setLayout(flashPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -306,7 +306,7 @@ void Lightning::recreatePipelines() { .setLayout(boltPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -344,7 +344,7 @@ void Lightning::recreatePipelines() { .setLayout(flashPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 654717ab..fb5f0bb9 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -507,7 +507,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; opaquePipeline_ = buildM2Pipeline(PipelineBuilder::blendDisabled(), true); @@ -542,7 +542,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout .setLayout(particlePipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; particlePipeline_ = buildParticlePipeline(PipelineBuilder::blendAlpha()); @@ -575,7 +575,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout .setLayout(smokePipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); } // --- Build ribbon pipelines --- @@ -617,7 +617,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout .setLayout(ribbonPipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; ribbonPipeline_ = buildRibbonPipeline(PipelineBuilder::blendAlpha()); @@ -3228,7 +3228,7 @@ bool M2Renderer::initializeShadow(VkRenderPass shadowRenderPass) { .setLayout(shadowPipelineLayout_) .setRenderPass(shadowRenderPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -5076,7 +5076,7 @@ void M2Renderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; opaquePipeline_ = buildM2Pipeline(PipelineBuilder::blendDisabled(), true); @@ -5111,7 +5111,7 @@ void M2Renderer::recreatePipelines() { .setLayout(particlePipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; particlePipeline_ = buildParticlePipeline(PipelineBuilder::blendAlpha()); @@ -5144,7 +5144,7 @@ void M2Renderer::recreatePipelines() { .setLayout(smokePipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); } // --- Ribbon pipelines --- @@ -5178,7 +5178,7 @@ void M2Renderer::recreatePipelines() { .setLayout(ribbonPipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; ribbonPipeline_ = buildRibbonPipeline(PipelineBuilder::blendAlpha()); diff --git a/src/rendering/minimap.cpp b/src/rendering/minimap.cpp index cce494d9..7cccca2b 100644 --- a/src/rendering/minimap.cpp +++ b/src/rendering/minimap.cpp @@ -165,7 +165,7 @@ bool Minimap::initialize(VkContext* ctx, VkDescriptorSetLayout /*perFrameLayout* .setLayout(tilePipelineLayout) .setRenderPass(compositeTarget->getRenderPass()) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vs.destroy(); fs.destroy(); @@ -192,7 +192,7 @@ bool Minimap::initialize(VkContext* ctx, VkDescriptorSetLayout /*perFrameLayout* .setLayout(displayPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vs.destroy(); fs.destroy(); @@ -270,7 +270,7 @@ void Minimap::recreatePipelines() { .setLayout(displayPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vs.destroy(); fs.destroy(); diff --git a/src/rendering/mount_dust.cpp b/src/rendering/mount_dust.cpp index 5678f31c..560e8a42 100644 --- a/src/rendering/mount_dust.cpp +++ b/src/rendering/mount_dust.cpp @@ -92,7 +92,7 @@ bool MountDust::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -199,7 +199,7 @@ void MountDust::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/quest_marker_renderer.cpp b/src/rendering/quest_marker_renderer.cpp index b274a880..07498285 100644 --- a/src/rendering/quest_marker_renderer.cpp +++ b/src/rendering/quest_marker_renderer.cpp @@ -114,7 +114,7 @@ bool QuestMarkerRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFr .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -233,7 +233,7 @@ void QuestMarkerRenderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 7199273d..6e3d46c1 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -3820,7 +3820,7 @@ void Renderer::initSelectionCircle() { .setLayout(selCirclePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -3932,7 +3932,7 @@ void Renderer::initOverlayPipeline() { .setLayout(overlayPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertMod.destroy(); fragMod.destroy(); @@ -4144,7 +4144,7 @@ bool Renderer::initFSRResources() { .setLayout(fsr_.pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertMod.destroy(); fragMod.destroy(); @@ -4668,7 +4668,7 @@ bool Renderer::initFSR2Resources() { .setLayout(fsr2_.sharpenPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertMod.destroy(); fragMod.destroy(); @@ -5353,7 +5353,7 @@ bool Renderer::initFXAAResources() { .setLayout(fxaa_.pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertMod.destroy(); fragMod.destroy(); diff --git a/src/rendering/skybox.cpp b/src/rendering/skybox.cpp index 3e0e7de6..1e08ac4f 100644 --- a/src/rendering/skybox.cpp +++ b/src/rendering/skybox.cpp @@ -81,7 +81,7 @@ bool Skybox::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); // Shader modules can be freed after pipeline creation vertModule.destroy(); @@ -133,7 +133,7 @@ void Skybox::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/starfield.cpp b/src/rendering/starfield.cpp index e472bc8d..b51d419b 100644 --- a/src/rendering/starfield.cpp +++ b/src/rendering/starfield.cpp @@ -91,7 +91,7 @@ bool StarField::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -164,7 +164,7 @@ void StarField::recreatePipelines() { .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/swim_effects.cpp b/src/rendering/swim_effects.cpp index 9a7ad119..9bc4885a 100644 --- a/src/rendering/swim_effects.cpp +++ b/src/rendering/swim_effects.cpp @@ -98,7 +98,7 @@ bool SwimEffects::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(ripplePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -142,7 +142,7 @@ bool SwimEffects::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(bubblePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -186,7 +186,7 @@ bool SwimEffects::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(insectPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -366,7 +366,7 @@ void SwimEffects::recreatePipelines() { .setLayout(ripplePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -393,7 +393,7 @@ void SwimEffects::recreatePipelines() { .setLayout(bubblePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -420,7 +420,7 @@ void SwimEffects::recreatePipelines() { .setLayout(insectPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index 775881d3..7543e639 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -143,7 +143,7 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); if (!pipeline) { LOG_ERROR("TerrainRenderer: failed to create fill pipeline"); @@ -165,7 +165,7 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); if (!wireframePipeline) { LOG_WARNING("TerrainRenderer: wireframe pipeline not available"); @@ -245,7 +245,7 @@ void TerrainRenderer::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); if (!pipeline) { LOG_ERROR("TerrainRenderer::recreatePipelines: failed to create fill pipeline"); @@ -264,7 +264,7 @@ void TerrainRenderer::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); if (!wireframePipeline) { LOG_WARNING("TerrainRenderer::recreatePipelines: wireframe pipeline not available"); @@ -932,7 +932,7 @@ bool TerrainRenderer::initializeShadow(VkRenderPass shadowRenderPass) { .setLayout(shadowPipelineLayout_) .setRenderPass(shadowRenderPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); diff --git a/src/rendering/vk_context.cpp b/src/rendering/vk_context.cpp index 51781a3c..aa223502 100644 --- a/src/rendering/vk_context.cpp +++ b/src/rendering/vk_context.cpp @@ -6,6 +6,9 @@ #include #include #include +#include +#include +#include namespace wowee { namespace rendering { @@ -37,6 +40,10 @@ bool VkContext::initialize(SDL_Window* window) { if (!createLogicalDevice()) return false; if (!createAllocator()) return false; + // Pipeline cache: try to load from disk, fall back to empty cache. + // Not fatal — if it fails we just skip caching. + createPipelineCache(); + int w, h; SDL_Vulkan_GetDrawableSize(window, &w, &h); if (!createSwapchain(w, h)) return false; @@ -83,6 +90,13 @@ void VkContext::shutdown() { if (immFence) { vkDestroyFence(device, immFence, nullptr); immFence = VK_NULL_HANDLE; } if (immCommandPool) { vkDestroyCommandPool(device, immCommandPool, nullptr); immCommandPool = VK_NULL_HANDLE; } + // Persist pipeline cache to disk before tearing down the device. + savePipelineCache(); + if (pipelineCache_) { + vkDestroyPipelineCache(device, pipelineCache_, nullptr); + pipelineCache_ = VK_NULL_HANDLE; + } + LOG_WARNING("VkContext::shutdown - destroySwapchain..."); destroySwapchain(); @@ -267,6 +281,106 @@ bool VkContext::createAllocator() { return true; } +// --------------------------------------------------------------------------- +// Pipeline cache persistence +// --------------------------------------------------------------------------- + +static std::string getPipelineCachePath() { +#ifdef _WIN32 + if (const char* appdata = std::getenv("APPDATA")) + return std::string(appdata) + "\\wowee\\pipeline_cache.bin"; + return ".\\pipeline_cache.bin"; +#elif defined(__APPLE__) + if (const char* home = std::getenv("HOME")) + return std::string(home) + "/Library/Caches/wowee/pipeline_cache.bin"; + return "./pipeline_cache.bin"; +#else + if (const char* home = std::getenv("HOME")) + return std::string(home) + "/.local/share/wowee/pipeline_cache.bin"; + return "./pipeline_cache.bin"; +#endif +} + +bool VkContext::createPipelineCache() { + std::string path = getPipelineCachePath(); + + // Try to load existing cache data from disk. + std::vector cacheData; + { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (file.is_open()) { + auto size = file.tellg(); + if (size > 0) { + cacheData.resize(static_cast(size)); + file.seekg(0); + file.read(cacheData.data(), size); + if (!file) { + LOG_WARNING("Pipeline cache file read failed, starting with empty cache"); + cacheData.clear(); + } + } + } + } + + VkPipelineCacheCreateInfo cacheCI{}; + cacheCI.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + cacheCI.initialDataSize = cacheData.size(); + cacheCI.pInitialData = cacheData.empty() ? nullptr : cacheData.data(); + + VkResult result = vkCreatePipelineCache(device, &cacheCI, nullptr, &pipelineCache_); + if (result != VK_SUCCESS) { + // If loading stale/corrupt data caused failure, retry with empty cache. + if (!cacheData.empty()) { + LOG_WARNING("Pipeline cache creation failed with saved data, retrying empty"); + cacheCI.initialDataSize = 0; + cacheCI.pInitialData = nullptr; + result = vkCreatePipelineCache(device, &cacheCI, nullptr, &pipelineCache_); + } + if (result != VK_SUCCESS) { + LOG_WARNING("Pipeline cache creation failed — pipelines will not be cached"); + pipelineCache_ = VK_NULL_HANDLE; + return false; + } + } + + if (!cacheData.empty()) { + LOG_INFO("Pipeline cache loaded from disk (", cacheData.size(), " bytes)"); + } else { + LOG_INFO("Pipeline cache created (empty)"); + } + return true; +} + +void VkContext::savePipelineCache() { + if (!pipelineCache_ || !device) return; + + size_t dataSize = 0; + if (vkGetPipelineCacheData(device, pipelineCache_, &dataSize, nullptr) != VK_SUCCESS || dataSize == 0) { + LOG_WARNING("Failed to query pipeline cache size"); + return; + } + + std::vector data(dataSize); + if (vkGetPipelineCacheData(device, pipelineCache_, &dataSize, data.data()) != VK_SUCCESS) { + LOG_WARNING("Failed to retrieve pipeline cache data"); + return; + } + + std::string path = getPipelineCachePath(); + std::filesystem::create_directories(std::filesystem::path(path).parent_path()); + + std::ofstream file(path, std::ios::binary | std::ios::trunc); + if (!file.is_open()) { + LOG_WARNING("Failed to open pipeline cache file for writing: ", path); + return; + } + + file.write(data.data(), static_cast(dataSize)); + file.close(); + + LOG_INFO("Pipeline cache saved to disk (", dataSize, " bytes)"); +} + bool VkContext::createSwapchain(int width, int height) { vkb::SwapchainBuilder swapchainBuilder{physicalDevice, device, surface}; diff --git a/src/rendering/vk_pipeline.cpp b/src/rendering/vk_pipeline.cpp index 4e565b07..4119d8c8 100644 --- a/src/rendering/vk_pipeline.cpp +++ b/src/rendering/vk_pipeline.cpp @@ -111,7 +111,7 @@ PipelineBuilder& PipelineBuilder::setDynamicStates(const std::vectorgetPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -257,7 +257,7 @@ void WaterRenderer::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -2092,7 +2092,7 @@ bool WaterRenderer::createWater1xPass(VkFormat colorFormat, VkFormat depthFormat .setLayout(pipelineLayout) .setRenderPass(water1xRenderPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); diff --git a/src/rendering/weather.cpp b/src/rendering/weather.cpp index 5dc525da..6f81aae0 100644 --- a/src/rendering/weather.cpp +++ b/src/rendering/weather.cpp @@ -85,7 +85,7 @@ bool Weather::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -165,7 +165,7 @@ void Weather::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 0f8f6b76..68e7f7b3 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -183,7 +183,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); if (!opaquePipeline_) { core::Logger::getInstance().error("WMORenderer: failed to create opaque pipeline"); @@ -205,7 +205,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); if (!transparentPipeline_) { core::Logger::getInstance().warning("WMORenderer: transparent pipeline not available"); @@ -224,7 +224,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); // --- Build wireframe pipeline --- wireframePipeline_ = PipelineBuilder() @@ -239,7 +239,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); if (!wireframePipeline_) { core::Logger::getInstance().warning("WMORenderer: wireframe pipeline not available"); @@ -1679,7 +1679,7 @@ bool WMORenderer::initializeShadow(VkRenderPass shadowRenderPass) { .setLayout(shadowPipelineLayout_) .setRenderPass(shadowRenderPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -3681,7 +3681,7 @@ void WMORenderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); transparentPipeline_ = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), @@ -3695,7 +3695,7 @@ void WMORenderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); glassPipeline_ = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), @@ -3709,7 +3709,7 @@ void WMORenderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); wireframePipeline_ = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), @@ -3723,7 +3723,7 @@ void WMORenderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); diff --git a/src/rendering/world_map.cpp b/src/rendering/world_map.cpp index 8fe955ca..03da7972 100644 --- a/src/rendering/world_map.cpp +++ b/src/rendering/world_map.cpp @@ -165,7 +165,7 @@ bool WorldMap::initialize(VkContext* ctx, pipeline::AssetManager* am) { .setLayout(tilePipelineLayout) .setRenderPass(compositeTarget->getRenderPass()) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vs.destroy(); fs.destroy();