Kelsidavis-WoWee/src/rendering/lightning.cpp
Kelsi c8c01f8ac0 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).
2026-03-24 09:47:03 -07:00

595 lines
20 KiB
C++

#include "rendering/lightning.hpp"
#include "rendering/camera.hpp"
#include "rendering/vk_context.hpp"
#include "rendering/vk_shader.hpp"
#include "rendering/vk_pipeline.hpp"
#include "rendering/vk_frame_data.hpp"
#include "rendering/vk_utils.hpp"
#include "core/logger.hpp"
#include <random>
#include <cmath>
#include <cstring>
namespace wowee {
namespace rendering {
namespace {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(0.0f, 1.0f);
float randomRange(float min, float max) {
return min + dist(gen) * (max - min);
}
}
Lightning::Lightning() {
flash.active = false;
flash.intensity = 0.0f;
flash.lifetime = 0.0f;
flash.maxLifetime = FLASH_LIFETIME;
bolts.resize(MAX_BOLTS);
for (auto& bolt : bolts) {
bolt.active = false;
bolt.lifetime = 0.0f;
bolt.maxLifetime = BOLT_LIFETIME;
bolt.brightness = 1.0f;
}
// Random initial strike time
nextStrikeTime = randomRange(MIN_STRIKE_INTERVAL, MAX_STRIKE_INTERVAL);
}
Lightning::~Lightning() {
shutdown();
}
bool Lightning::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) {
core::Logger::getInstance().info("Initializing lightning system...");
vkCtx = ctx;
VkDevice device = vkCtx->getDevice();
std::vector<VkDynamicState> dynamicStates = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR
};
// ---- Bolt pipeline (LINE_STRIP) ----
{
VkShaderModule vertModule;
if (!vertModule.loadFromFile(device, "assets/shaders/lightning_bolt.vert.spv")) {
core::Logger::getInstance().error("Failed to load lightning_bolt vertex shader");
return false;
}
VkShaderModule fragModule;
if (!fragModule.loadFromFile(device, "assets/shaders/lightning_bolt.frag.spv")) {
core::Logger::getInstance().error("Failed to load lightning_bolt fragment shader");
return false;
}
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
// Push constant: { float brightness; } = 4 bytes
VkPushConstantRange pushRange{};
pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
pushRange.offset = 0;
pushRange.size = sizeof(float);
boltPipelineLayout = createPipelineLayout(device, {perFrameLayout}, {pushRange});
if (boltPipelineLayout == VK_NULL_HANDLE) {
core::Logger::getInstance().error("Failed to create bolt pipeline layout");
return false;
}
// Vertex input: position only (vec3)
VkVertexInputBindingDescription binding{};
binding.binding = 0;
binding.stride = sizeof(glm::vec3);
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
VkVertexInputAttributeDescription posAttr{};
posAttr.location = 0;
posAttr.binding = 0;
posAttr.format = VK_FORMAT_R32G32B32_SFLOAT;
posAttr.offset = 0;
boltPipeline = PipelineBuilder()
.setShaders(vertStage, fragStage)
.setVertexInput({binding}, {posAttr})
.setTopology(VK_PRIMITIVE_TOPOLOGY_LINE_STRIP)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setNoDepthTest() // Always visible (like the GL version)
.setColorBlendAttachment(PipelineBuilder::blendAdditive()) // Additive for electric glow
.setMultisample(vkCtx->getMsaaSamples())
.setLayout(boltPipelineLayout)
.setRenderPass(vkCtx->getImGuiRenderPass())
.setDynamicStates(dynamicStates)
.build(device, vkCtx->getPipelineCache());
vertModule.destroy();
fragModule.destroy();
if (boltPipeline == VK_NULL_HANDLE) {
core::Logger::getInstance().error("Failed to create bolt pipeline");
return false;
}
}
// ---- Flash pipeline (fullscreen quad, TRIANGLE_STRIP) ----
{
VkShaderModule vertModule;
if (!vertModule.loadFromFile(device, "assets/shaders/lightning_flash.vert.spv")) {
core::Logger::getInstance().error("Failed to load lightning_flash vertex shader");
return false;
}
VkShaderModule fragModule;
if (!fragModule.loadFromFile(device, "assets/shaders/lightning_flash.frag.spv")) {
core::Logger::getInstance().error("Failed to load lightning_flash fragment shader");
return false;
}
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
// Push constant: { float intensity; } = 4 bytes
VkPushConstantRange pushRange{};
pushRange.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
pushRange.offset = 0;
pushRange.size = sizeof(float);
flashPipelineLayout = createPipelineLayout(device, {}, {pushRange});
if (flashPipelineLayout == VK_NULL_HANDLE) {
core::Logger::getInstance().error("Failed to create flash pipeline layout");
return false;
}
// Vertex input: position only (vec2)
VkVertexInputBindingDescription binding{};
binding.binding = 0;
binding.stride = 2 * sizeof(float);
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
VkVertexInputAttributeDescription posAttr{};
posAttr.location = 0;
posAttr.binding = 0;
posAttr.format = VK_FORMAT_R32G32_SFLOAT;
posAttr.offset = 0;
flashPipeline = PipelineBuilder()
.setShaders(vertStage, fragStage)
.setVertexInput({binding}, {posAttr})
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setNoDepthTest()
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
.setMultisample(vkCtx->getMsaaSamples())
.setLayout(flashPipelineLayout)
.setRenderPass(vkCtx->getImGuiRenderPass())
.setDynamicStates(dynamicStates)
.build(device, vkCtx->getPipelineCache());
vertModule.destroy();
fragModule.destroy();
if (flashPipeline == VK_NULL_HANDLE) {
core::Logger::getInstance().error("Failed to create flash pipeline");
return false;
}
}
// ---- Create dynamic mapped vertex buffer for bolt segments ----
// Each bolt can have up to MAX_SEGMENTS * 2 vec3 entries (segments + branches)
boltDynamicVBSize = MAX_SEGMENTS * 4 * sizeof(glm::vec3); // generous capacity
{
AllocatedBuffer buf = createBuffer(vkCtx->getAllocator(), boltDynamicVBSize,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
boltDynamicVB = buf.buffer;
boltDynamicVBAlloc = buf.allocation;
boltDynamicVBAllocInfo = buf.info;
if (boltDynamicVB == VK_NULL_HANDLE) {
core::Logger::getInstance().error("Failed to create bolt dynamic vertex buffer");
return false;
}
}
// ---- Create static flash quad vertex buffer ----
{
float flashQuad[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f
};
AllocatedBuffer buf = uploadBuffer(*vkCtx, flashQuad, sizeof(flashQuad),
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
flashQuadVB = buf.buffer;
flashQuadVBAlloc = buf.allocation;
if (flashQuadVB == VK_NULL_HANDLE) {
core::Logger::getInstance().error("Failed to create flash quad vertex buffer");
return false;
}
}
core::Logger::getInstance().info("Lightning system initialized");
return true;
}
void Lightning::shutdown() {
if (vkCtx) {
VkDevice device = vkCtx->getDevice();
VmaAllocator allocator = vkCtx->getAllocator();
if (boltPipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(device, boltPipeline, nullptr);
boltPipeline = VK_NULL_HANDLE;
}
if (boltPipelineLayout != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(device, boltPipelineLayout, nullptr);
boltPipelineLayout = VK_NULL_HANDLE;
}
if (boltDynamicVB != VK_NULL_HANDLE) {
vmaDestroyBuffer(allocator, boltDynamicVB, boltDynamicVBAlloc);
boltDynamicVB = VK_NULL_HANDLE;
boltDynamicVBAlloc = VK_NULL_HANDLE;
}
if (flashPipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(device, flashPipeline, nullptr);
flashPipeline = VK_NULL_HANDLE;
}
if (flashPipelineLayout != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(device, flashPipelineLayout, nullptr);
flashPipelineLayout = VK_NULL_HANDLE;
}
if (flashQuadVB != VK_NULL_HANDLE) {
vmaDestroyBuffer(allocator, flashQuadVB, flashQuadVBAlloc);
flashQuadVB = VK_NULL_HANDLE;
flashQuadVBAlloc = VK_NULL_HANDLE;
}
}
vkCtx = nullptr;
}
void Lightning::recreatePipelines() {
if (!vkCtx) return;
VkDevice device = vkCtx->getDevice();
// Destroy old pipelines (NOT layouts)
if (boltPipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(device, boltPipeline, nullptr);
boltPipeline = VK_NULL_HANDLE;
}
if (flashPipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(device, flashPipeline, nullptr);
flashPipeline = VK_NULL_HANDLE;
}
std::vector<VkDynamicState> dynamicStates = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR
};
// ---- Rebuild bolt pipeline (LINE_STRIP) ----
{
VkShaderModule vertModule;
vertModule.loadFromFile(device, "assets/shaders/lightning_bolt.vert.spv");
VkShaderModule fragModule;
fragModule.loadFromFile(device, "assets/shaders/lightning_bolt.frag.spv");
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
VkVertexInputBindingDescription binding{};
binding.binding = 0;
binding.stride = sizeof(glm::vec3);
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
VkVertexInputAttributeDescription posAttr{};
posAttr.location = 0;
posAttr.binding = 0;
posAttr.format = VK_FORMAT_R32G32B32_SFLOAT;
posAttr.offset = 0;
boltPipeline = PipelineBuilder()
.setShaders(vertStage, fragStage)
.setVertexInput({binding}, {posAttr})
.setTopology(VK_PRIMITIVE_TOPOLOGY_LINE_STRIP)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setNoDepthTest()
.setColorBlendAttachment(PipelineBuilder::blendAdditive())
.setMultisample(vkCtx->getMsaaSamples())
.setLayout(boltPipelineLayout)
.setRenderPass(vkCtx->getImGuiRenderPass())
.setDynamicStates(dynamicStates)
.build(device, vkCtx->getPipelineCache());
vertModule.destroy();
fragModule.destroy();
}
// ---- Rebuild flash pipeline (TRIANGLE_STRIP) ----
{
VkShaderModule vertModule;
vertModule.loadFromFile(device, "assets/shaders/lightning_flash.vert.spv");
VkShaderModule fragModule;
fragModule.loadFromFile(device, "assets/shaders/lightning_flash.frag.spv");
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
VkVertexInputBindingDescription binding{};
binding.binding = 0;
binding.stride = 2 * sizeof(float);
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
VkVertexInputAttributeDescription posAttr{};
posAttr.location = 0;
posAttr.binding = 0;
posAttr.format = VK_FORMAT_R32G32_SFLOAT;
posAttr.offset = 0;
flashPipeline = PipelineBuilder()
.setShaders(vertStage, fragStage)
.setVertexInput({binding}, {posAttr})
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setNoDepthTest()
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
.setMultisample(vkCtx->getMsaaSamples())
.setLayout(flashPipelineLayout)
.setRenderPass(vkCtx->getImGuiRenderPass())
.setDynamicStates(dynamicStates)
.build(device, vkCtx->getPipelineCache());
vertModule.destroy();
fragModule.destroy();
}
}
void Lightning::update(float deltaTime, const Camera& camera) {
if (!enabled) {
return;
}
// Update strike timer
strikeTimer += deltaTime;
// Spawn random strikes based on intensity
if (strikeTimer >= nextStrikeTime) {
spawnRandomStrike(camera.getPosition());
strikeTimer = 0.0f;
// Calculate next strike time (higher intensity = more frequent)
float intervalRange = MAX_STRIKE_INTERVAL - MIN_STRIKE_INTERVAL;
float adjustedInterval = MIN_STRIKE_INTERVAL + intervalRange * (1.0f - intensity);
nextStrikeTime = randomRange(adjustedInterval * 0.8f, adjustedInterval * 1.2f);
}
updateBolts(deltaTime);
updateFlash(deltaTime);
}
void Lightning::updateBolts(float deltaTime) {
for (auto& bolt : bolts) {
if (!bolt.active) {
continue;
}
bolt.lifetime += deltaTime;
if (bolt.lifetime >= bolt.maxLifetime) {
bolt.active = false;
continue;
}
// Fade out
float t = bolt.lifetime / bolt.maxLifetime;
bolt.brightness = 1.0f - t;
}
}
void Lightning::updateFlash(float deltaTime) {
if (!flash.active) {
return;
}
flash.lifetime += deltaTime;
if (flash.lifetime >= flash.maxLifetime) {
flash.active = false;
flash.intensity = 0.0f;
return;
}
// Quick fade
float t = flash.lifetime / flash.maxLifetime;
flash.intensity = 1.0f - (t * t); // Quadratic fade
}
void Lightning::spawnRandomStrike(const glm::vec3& cameraPos) {
// Find inactive bolt
LightningBolt* bolt = nullptr;
for (auto& b : bolts) {
if (!b.active) {
bolt = &b;
break;
}
}
if (!bolt) {
return; // All bolts active
}
// Random position around camera
float angle = randomRange(0.0f, 2.0f * 3.14159f);
float distance = randomRange(50.0f, STRIKE_DISTANCE);
glm::vec3 strikePos;
strikePos.x = cameraPos.x + std::cos(angle) * distance;
strikePos.z = cameraPos.z + std::sin(angle) * distance;
strikePos.y = cameraPos.y + randomRange(80.0f, 150.0f); // High in sky
triggerStrike(strikePos);
}
void Lightning::triggerStrike(const glm::vec3& position) {
// Find inactive bolt
LightningBolt* bolt = nullptr;
for (auto& b : bolts) {
if (!b.active) {
bolt = &b;
break;
}
}
if (!bolt) {
return;
}
// Setup bolt
bolt->active = true;
bolt->lifetime = 0.0f;
bolt->brightness = 1.0f;
bolt->startPos = position;
bolt->endPos = position;
bolt->endPos.y = position.y - randomRange(100.0f, 200.0f); // Strike downward
// Generate segments
bolt->segments.clear();
bolt->branches.clear();
generateLightningBolt(*bolt);
// Trigger screen flash
flash.active = true;
flash.lifetime = 0.0f;
flash.intensity = 1.0f;
}
void Lightning::generateLightningBolt(LightningBolt& bolt) {
generateBoltSegments(bolt.startPos, bolt.endPos, bolt.segments, 0);
}
void Lightning::generateBoltSegments(const glm::vec3& start, const glm::vec3& end,
std::vector<glm::vec3>& segments, int depth) {
if (depth > 4) { // Max recursion depth
return;
}
int numSegments = 8 + static_cast<int>(randomRange(0.0f, 8.0f));
glm::vec3 direction = end - start;
float length = glm::length(direction);
direction = glm::normalize(direction);
glm::vec3 current = start;
segments.push_back(current);
for (int i = 1; i < numSegments; i++) {
float t = static_cast<float>(i) / static_cast<float>(numSegments);
glm::vec3 target = start + direction * (length * t);
// Add random offset perpendicular to direction
float offsetAmount = (1.0f - t) * 8.0f; // More offset at start
glm::vec3 perpendicular1 = glm::normalize(glm::cross(direction, glm::vec3(0.0f, 1.0f, 0.0f)));
glm::vec3 perpendicular2 = glm::normalize(glm::cross(direction, perpendicular1));
glm::vec3 offset = perpendicular1 * randomRange(-offsetAmount, offsetAmount) +
perpendicular2 * randomRange(-offsetAmount, offsetAmount);
current = target + offset;
segments.push_back(current);
// Random branches
if (dist(gen) < BRANCH_PROBABILITY && depth < 3) {
glm::vec3 branchEnd = current;
branchEnd += glm::vec3(randomRange(-20.0f, 20.0f),
randomRange(-30.0f, -10.0f),
randomRange(-20.0f, 20.0f));
generateBoltSegments(current, branchEnd, segments, depth + 1);
}
}
segments.push_back(end);
}
void Lightning::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
if (!enabled) {
return;
}
renderBolts(cmd, perFrameSet);
renderFlash(cmd);
}
void Lightning::renderBolts(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
if (boltPipeline == VK_NULL_HANDLE) return;
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, boltPipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, boltPipelineLayout,
0, 1, &perFrameSet, 0, nullptr);
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &boltDynamicVB, &offset);
for (const auto& bolt : bolts) {
if (!bolt.active || bolt.segments.empty()) {
continue;
}
// Upload bolt segments to mapped buffer
VkDeviceSize uploadSize = bolt.segments.size() * sizeof(glm::vec3);
if (uploadSize > boltDynamicVBSize) {
// Clamp to buffer size
uploadSize = boltDynamicVBSize;
}
if (boltDynamicVBAllocInfo.pMappedData) {
std::memcpy(boltDynamicVBAllocInfo.pMappedData, bolt.segments.data(), uploadSize);
}
// Push brightness
vkCmdPushConstants(cmd, boltPipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
0, sizeof(float), &bolt.brightness);
uint32_t vertexCount = static_cast<uint32_t>(uploadSize / sizeof(glm::vec3));
vkCmdDraw(cmd, vertexCount, 1, 0, 0);
}
}
void Lightning::renderFlash(VkCommandBuffer cmd) {
if (!flash.active || flash.intensity <= 0.01f || flashPipeline == VK_NULL_HANDLE) {
return;
}
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, flashPipeline);
// Push flash intensity
vkCmdPushConstants(cmd, flashPipelineLayout,
VK_SHADER_STAGE_FRAGMENT_BIT,
0, sizeof(float), &flash.intensity);
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &flashQuadVB, &offset);
vkCmdDraw(cmd, 4, 1, 0, 0);
}
void Lightning::setEnabled(bool enabled) {
this->enabled = enabled;
if (!enabled) {
// Clear active effects
for (auto& bolt : bolts) {
bolt.active = false;
}
flash.active = false;
}
}
void Lightning::setIntensity(float intensity) {
this->intensity = glm::clamp(intensity, 0.0f, 1.0f);
}
} // namespace rendering
} // namespace wowee