#include "rendering/terrain_renderer.hpp" #include "rendering/vk_context.hpp" #include "rendering/vk_texture.hpp" #include "rendering/vk_buffer.hpp" #include "rendering/vk_pipeline.hpp" #include "rendering/vk_shader.hpp" #include "rendering/vk_utils.hpp" #include "rendering/vk_frame_data.hpp" #include "rendering/frustum.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/blp_loader.hpp" #include "core/logger.hpp" #include #include #include #include #include #include namespace wowee { namespace rendering { // Matches set 1 binding 7 in terrain.frag.glsl struct TerrainParamsUBO { int32_t layerCount; int32_t hasLayer1; int32_t hasLayer2; int32_t hasLayer3; }; TerrainRenderer::TerrainRenderer() = default; TerrainRenderer::~TerrainRenderer() { shutdown(); } bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* assets) { vkCtx = ctx; assetManager = assets; if (!vkCtx || !assetManager) { LOG_ERROR("TerrainRenderer: null context or asset manager"); return false; } LOG_INFO("Initializing terrain renderer (Vulkan)"); VkDevice device = vkCtx->getDevice(); // --- Create material descriptor set layout (set 1) --- // bindings 0-6: combined image samplers (base + 3 layer + 3 alpha) // binding 7: uniform buffer (TerrainParams) std::vector materialBindings(8); for (uint32_t i = 0; i < 7; i++) { materialBindings[i] = {}; materialBindings[i].binding = i; materialBindings[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; materialBindings[i].descriptorCount = 1; materialBindings[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; } materialBindings[7] = {}; materialBindings[7].binding = 7; materialBindings[7].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; materialBindings[7].descriptorCount = 1; materialBindings[7].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; materialSetLayout = createDescriptorSetLayout(device, materialBindings); if (!materialSetLayout) { LOG_ERROR("TerrainRenderer: failed to create material set layout"); return false; } // --- Create descriptor pool --- VkDescriptorPoolSize poolSizes[] = { { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_MATERIAL_SETS * 7 }, { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, MAX_MATERIAL_SETS }, }; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; poolInfo.maxSets = MAX_MATERIAL_SETS; poolInfo.poolSizeCount = 2; poolInfo.pPoolSizes = poolSizes; if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &materialDescPool) != VK_SUCCESS) { LOG_ERROR("TerrainRenderer: failed to create descriptor pool"); return false; } // --- Create pipeline layout --- VkPushConstantRange pushRange{}; pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; pushRange.offset = 0; pushRange.size = sizeof(GPUPushConstants); std::vector setLayouts = { perFrameLayout, materialSetLayout }; pipelineLayout = createPipelineLayout(device, setLayouts, { pushRange }); if (!pipelineLayout) { LOG_ERROR("TerrainRenderer: failed to create pipeline layout"); return false; } // --- Load shaders --- VkShaderModule vertShader, fragShader; if (!vertShader.loadFromFile(device, "assets/shaders/terrain.vert.spv")) { LOG_ERROR("TerrainRenderer: failed to load vertex shader"); return false; } if (!fragShader.loadFromFile(device, "assets/shaders/terrain.frag.spv")) { LOG_ERROR("TerrainRenderer: failed to load fragment shader"); return false; } // --- Vertex input --- VkVertexInputBindingDescription vertexBinding{}; vertexBinding.binding = 0; vertexBinding.stride = sizeof(pipeline::TerrainVertex); vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; std::vector vertexAttribs(4); vertexAttribs[0] = { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast(offsetof(pipeline::TerrainVertex, position)) }; vertexAttribs[1] = { 1, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast(offsetof(pipeline::TerrainVertex, normal)) }; vertexAttribs[2] = { 2, 0, VK_FORMAT_R32G32_SFLOAT, static_cast(offsetof(pipeline::TerrainVertex, texCoord)) }; vertexAttribs[3] = { 3, 0, VK_FORMAT_R32G32_SFLOAT, static_cast(offsetof(pipeline::TerrainVertex, layerUV)) }; // --- Build fill pipeline --- VkRenderPass mainPass = vkCtx->getImGuiRenderPass(); pipeline = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) .setVertexInput({ vertexBinding }, vertexAttribs) .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(PipelineBuilder::blendDisabled()) .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) .build(device); if (!pipeline) { LOG_ERROR("TerrainRenderer: failed to create fill pipeline"); vertShader.destroy(); fragShader.destroy(); return false; } // --- Build wireframe pipeline --- wireframePipeline = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) .setVertexInput({ vertexBinding }, vertexAttribs) .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) .setRasterization(VK_POLYGON_MODE_LINE, VK_CULL_MODE_NONE) .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(PipelineBuilder::blendDisabled()) .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) .build(device); if (!wireframePipeline) { LOG_WARNING("TerrainRenderer: wireframe pipeline not available"); } vertShader.destroy(); fragShader.destroy(); // --- Create fallback textures --- whiteTexture = std::make_unique(); uint8_t whitePixel[4] = {255, 255, 255, 255}; whiteTexture->upload(*vkCtx, whitePixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false); whiteTexture->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_REPEAT); opaqueAlphaTexture = std::make_unique(); uint8_t opaqueAlpha = 255; opaqueAlphaTexture->upload(*vkCtx, &opaqueAlpha, 1, 1, VK_FORMAT_R8_UNORM, false); opaqueAlphaTexture->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE); textureCacheBudgetBytes_ = envSizeMBOrDefault("WOWEE_TERRAIN_TEX_CACHE_MB", 4096) * 1024ull * 1024ull; LOG_INFO("Terrain texture cache budget: ", textureCacheBudgetBytes_ / (1024 * 1024), " MB"); LOG_INFO("Terrain renderer initialized (Vulkan)"); return true; } void TerrainRenderer::recreatePipelines() { if (!vkCtx) return; VkDevice device = vkCtx->getDevice(); // Destroy old pipelines (keep layouts) if (pipeline) { vkDestroyPipeline(device, pipeline, nullptr); pipeline = VK_NULL_HANDLE; } if (wireframePipeline) { vkDestroyPipeline(device, wireframePipeline, nullptr); wireframePipeline = VK_NULL_HANDLE; } // Load shaders VkShaderModule vertShader, fragShader; if (!vertShader.loadFromFile(device, "assets/shaders/terrain.vert.spv")) { LOG_ERROR("TerrainRenderer::recreatePipelines: failed to load vertex shader"); return; } if (!fragShader.loadFromFile(device, "assets/shaders/terrain.frag.spv")) { LOG_ERROR("TerrainRenderer::recreatePipelines: failed to load fragment shader"); vertShader.destroy(); return; } // Vertex input (same as initialize) VkVertexInputBindingDescription vertexBinding{}; vertexBinding.binding = 0; vertexBinding.stride = sizeof(pipeline::TerrainVertex); vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; std::vector vertexAttribs(4); vertexAttribs[0] = { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast(offsetof(pipeline::TerrainVertex, position)) }; vertexAttribs[1] = { 1, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast(offsetof(pipeline::TerrainVertex, normal)) }; vertexAttribs[2] = { 2, 0, VK_FORMAT_R32G32_SFLOAT, static_cast(offsetof(pipeline::TerrainVertex, texCoord)) }; vertexAttribs[3] = { 3, 0, VK_FORMAT_R32G32_SFLOAT, static_cast(offsetof(pipeline::TerrainVertex, layerUV)) }; VkRenderPass mainPass = vkCtx->getImGuiRenderPass(); // Rebuild fill pipeline pipeline = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) .setVertexInput({ vertexBinding }, vertexAttribs) .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(PipelineBuilder::blendDisabled()) .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) .build(device); if (!pipeline) { LOG_ERROR("TerrainRenderer::recreatePipelines: failed to create fill pipeline"); } // Rebuild wireframe pipeline wireframePipeline = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) .setVertexInput({ vertexBinding }, vertexAttribs) .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) .setRasterization(VK_POLYGON_MODE_LINE, VK_CULL_MODE_NONE) .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(PipelineBuilder::blendDisabled()) .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) .build(device); if (!wireframePipeline) { LOG_WARNING("TerrainRenderer::recreatePipelines: wireframe pipeline not available"); } vertShader.destroy(); fragShader.destroy(); } void TerrainRenderer::shutdown() { LOG_INFO("Shutting down terrain renderer"); if (!vkCtx) return; VkDevice device = vkCtx->getDevice(); VmaAllocator allocator = vkCtx->getAllocator(); vkDeviceWaitIdle(device); clear(); for (auto& [path, entry] : textureCache) { if (entry.texture) entry.texture->destroy(device, allocator); } textureCache.clear(); textureCacheBytes_ = 0; textureCacheCounter_ = 0; failedTextureCache_.clear(); loggedTextureLoadFails_.clear(); textureBudgetRejectWarnings_ = 0; if (whiteTexture) { whiteTexture->destroy(device, allocator); whiteTexture.reset(); } if (opaqueAlphaTexture) { opaqueAlphaTexture->destroy(device, allocator); opaqueAlphaTexture.reset(); } if (pipeline) { vkDestroyPipeline(device, pipeline, nullptr); pipeline = VK_NULL_HANDLE; } if (wireframePipeline) { vkDestroyPipeline(device, wireframePipeline, nullptr); wireframePipeline = VK_NULL_HANDLE; } if (pipelineLayout) { vkDestroyPipelineLayout(device, pipelineLayout, nullptr); pipelineLayout = VK_NULL_HANDLE; } if (materialDescPool) { vkDestroyDescriptorPool(device, materialDescPool, nullptr); materialDescPool = VK_NULL_HANDLE; } if (materialSetLayout) { vkDestroyDescriptorSetLayout(device, materialSetLayout, nullptr); materialSetLayout = VK_NULL_HANDLE; } // Shadow pipeline cleanup if (shadowPipeline_) { vkDestroyPipeline(device, shadowPipeline_, nullptr); shadowPipeline_ = VK_NULL_HANDLE; } if (shadowPipelineLayout_) { vkDestroyPipelineLayout(device, shadowPipelineLayout_, nullptr); shadowPipelineLayout_ = VK_NULL_HANDLE; } if (shadowParamsPool_) { vkDestroyDescriptorPool(device, shadowParamsPool_, nullptr); shadowParamsPool_ = VK_NULL_HANDLE; shadowParamsSet_ = VK_NULL_HANDLE; } if (shadowParamsLayout_) { vkDestroyDescriptorSetLayout(device, shadowParamsLayout_, nullptr); shadowParamsLayout_ = VK_NULL_HANDLE; } if (shadowParamsUBO_) { vmaDestroyBuffer(allocator, shadowParamsUBO_, shadowParamsAlloc_); shadowParamsUBO_ = VK_NULL_HANDLE; shadowParamsAlloc_ = VK_NULL_HANDLE; } vkCtx = nullptr; } bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh, const std::vector& texturePaths, int tileX, int tileY) { if (mesh.validChunkCount == 0) { LOG_WARNING("loadTerrain[", tileX, ",", tileY, "]: mesh has 0 valid chunks (", texturePaths.size(), " textures)"); return false; } LOG_DEBUG("Loading terrain mesh: ", mesh.validChunkCount, " chunks"); vkCtx->beginUploadBatch(); for (int y = 0; y < 16; y++) { for (int x = 0; x < 16; x++) { const auto& chunk = mesh.getChunk(x, y); if (!chunk.isValid()) continue; TerrainChunkGPU gpuChunk = uploadChunk(chunk); if (!gpuChunk.isValid()) { LOG_WARNING("Failed to upload chunk [", x, ",", y, "]"); continue; } calculateBoundingSphere(gpuChunk, chunk); // Load textures for this chunk if (!chunk.layers.empty()) { uint32_t baseTexId = chunk.layers[0].textureId; if (baseTexId < texturePaths.size()) { gpuChunk.baseTexture = loadTexture(texturePaths[baseTexId]); } else { gpuChunk.baseTexture = whiteTexture.get(); } for (size_t i = 1; i < chunk.layers.size() && i < 4; i++) { const auto& layer = chunk.layers[i]; int li = static_cast(i) - 1; VkTexture* layerTex = whiteTexture.get(); if (layer.textureId < texturePaths.size()) { layerTex = loadTexture(texturePaths[layer.textureId]); } gpuChunk.layerTextures[li] = layerTex; VkTexture* alphaTex = opaqueAlphaTexture.get(); if (!layer.alphaData.empty()) { alphaTex = createAlphaTexture(layer.alphaData); } gpuChunk.alphaTextures[li] = alphaTex; gpuChunk.layerCount = static_cast(i); } } else { gpuChunk.baseTexture = whiteTexture.get(); } gpuChunk.tileX = tileX; gpuChunk.tileY = tileY; // Create per-chunk params UBO TerrainParamsUBO params{}; params.layerCount = gpuChunk.layerCount; params.hasLayer1 = gpuChunk.layerCount >= 1 ? 1 : 0; params.hasLayer2 = gpuChunk.layerCount >= 2 ? 1 : 0; params.hasLayer3 = gpuChunk.layerCount >= 3 ? 1 : 0; VkBufferCreateInfo bufCI{}; bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufCI.size = sizeof(TerrainParamsUBO); bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; VmaAllocationCreateInfo allocCI{}; allocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; allocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; VmaAllocationInfo mapInfo{}; vmaCreateBuffer(vkCtx->getAllocator(), &bufCI, &allocCI, &gpuChunk.paramsUBO, &gpuChunk.paramsAlloc, &mapInfo); if (mapInfo.pMappedData) { std::memcpy(mapInfo.pMappedData, ¶ms, sizeof(params)); } // Allocate and write material descriptor set gpuChunk.materialSet = allocateMaterialSet(); if (gpuChunk.materialSet) { writeMaterialDescriptors(gpuChunk.materialSet, gpuChunk); } chunks.push_back(std::move(gpuChunk)); } } vkCtx->endUploadBatch(); LOG_DEBUG("Loaded ", chunks.size(), " terrain chunks to GPU"); return !chunks.empty(); } bool TerrainRenderer::loadTerrainIncremental(const pipeline::TerrainMesh& mesh, const std::vector& texturePaths, int tileX, int tileY, int& chunkIndex, int maxChunksPerCall) { // Batch all GPU uploads (VBs, IBs, textures) into a single command buffer // submission with one fence wait, instead of one per buffer/texture. vkCtx->beginUploadBatch(); int uploaded = 0; while (chunkIndex < 256 && uploaded < maxChunksPerCall) { int cy = chunkIndex / 16; int cx = chunkIndex % 16; chunkIndex++; const auto& chunk = mesh.getChunk(cx, cy); if (!chunk.isValid()) continue; TerrainChunkGPU gpuChunk = uploadChunk(chunk); if (!gpuChunk.isValid()) continue; calculateBoundingSphere(gpuChunk, chunk); if (!chunk.layers.empty()) { uint32_t baseTexId = chunk.layers[0].textureId; if (baseTexId < texturePaths.size()) { gpuChunk.baseTexture = loadTexture(texturePaths[baseTexId]); } else { gpuChunk.baseTexture = whiteTexture.get(); } for (size_t i = 1; i < chunk.layers.size() && i < 4; i++) { const auto& layer = chunk.layers[i]; int li = static_cast(i) - 1; VkTexture* layerTex = whiteTexture.get(); if (layer.textureId < texturePaths.size()) { layerTex = loadTexture(texturePaths[layer.textureId]); } gpuChunk.layerTextures[li] = layerTex; VkTexture* alphaTex = opaqueAlphaTexture.get(); if (!layer.alphaData.empty()) { alphaTex = createAlphaTexture(layer.alphaData); } gpuChunk.alphaTextures[li] = alphaTex; gpuChunk.layerCount = static_cast(i); } } else { gpuChunk.baseTexture = whiteTexture.get(); } gpuChunk.tileX = tileX; gpuChunk.tileY = tileY; TerrainParamsUBO params{}; params.layerCount = gpuChunk.layerCount; params.hasLayer1 = gpuChunk.layerCount >= 1 ? 1 : 0; params.hasLayer2 = gpuChunk.layerCount >= 2 ? 1 : 0; params.hasLayer3 = gpuChunk.layerCount >= 3 ? 1 : 0; VkBufferCreateInfo bufCI{}; bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufCI.size = sizeof(TerrainParamsUBO); bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; VmaAllocationCreateInfo allocCI{}; allocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; allocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; VmaAllocationInfo mapInfo{}; vmaCreateBuffer(vkCtx->getAllocator(), &bufCI, &allocCI, &gpuChunk.paramsUBO, &gpuChunk.paramsAlloc, &mapInfo); if (mapInfo.pMappedData) { std::memcpy(mapInfo.pMappedData, ¶ms, sizeof(params)); } gpuChunk.materialSet = allocateMaterialSet(); if (gpuChunk.materialSet) { writeMaterialDescriptors(gpuChunk.materialSet, gpuChunk); } chunks.push_back(std::move(gpuChunk)); uploaded++; } vkCtx->endUploadBatch(); return chunkIndex >= 256; } TerrainChunkGPU TerrainRenderer::uploadChunk(const pipeline::ChunkMesh& chunk) { TerrainChunkGPU gpuChunk; gpuChunk.worldX = chunk.worldX; gpuChunk.worldY = chunk.worldY; gpuChunk.worldZ = chunk.worldZ; gpuChunk.indexCount = static_cast(chunk.indices.size()); VkDeviceSize vbSize = chunk.vertices.size() * sizeof(pipeline::TerrainVertex); AllocatedBuffer vb = uploadBuffer(*vkCtx, chunk.vertices.data(), vbSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); gpuChunk.vertexBuffer = vb.buffer; gpuChunk.vertexAlloc = vb.allocation; VkDeviceSize ibSize = chunk.indices.size() * sizeof(pipeline::TerrainIndex); AllocatedBuffer ib = uploadBuffer(*vkCtx, chunk.indices.data(), ibSize, VK_BUFFER_USAGE_INDEX_BUFFER_BIT); gpuChunk.indexBuffer = ib.buffer; gpuChunk.indexAlloc = ib.allocation; return gpuChunk; } VkTexture* TerrainRenderer::loadTexture(const std::string& path) { auto normalizeKey = [](std::string key) { std::replace(key.begin(), key.end(), '/', '\\'); std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return key; }; std::string key = normalizeKey(path); auto it = textureCache.find(key); if (it != textureCache.end()) { it->second.lastUse = ++textureCacheCounter_; return it->second.texture.get(); } pipeline::BLPImage blp = assetManager->loadTexture(key); if (!blp.isValid()) { // Return white fallback but don't cache the failure — allow retry // on next tile load in case the asset becomes available. if (loggedTextureLoadFails_.insert(key).second) { LOG_WARNING("Failed to load texture: ", path); } return whiteTexture.get(); } size_t base = static_cast(blp.width) * static_cast(blp.height) * 4ull; size_t approxBytes = base + (base / 3); if (textureCacheBytes_ + approxBytes > textureCacheBudgetBytes_) { if (textureBudgetRejectWarnings_ < 3) { LOG_WARNING("Terrain texture cache full (", textureCacheBytes_ / (1024 * 1024), " MB / ", textureCacheBudgetBytes_ / (1024 * 1024), " MB), rejecting texture: ", path); } ++textureBudgetRejectWarnings_; return whiteTexture.get(); } auto tex = std::make_unique(); if (!tex->upload(*vkCtx, blp.data.data(), blp.width, blp.height, VK_FORMAT_R8G8B8A8_UNORM, true)) { LOG_WARNING("Failed to upload texture to GPU: ", path); return whiteTexture.get(); } tex->createSampler(vkCtx->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_REPEAT); VkTexture* raw = tex.get(); TextureCacheEntry e; e.texture = std::move(tex); e.approxBytes = approxBytes; e.lastUse = ++textureCacheCounter_; textureCacheBytes_ += e.approxBytes; textureCache[key] = std::move(e); return raw; } void TerrainRenderer::uploadPreloadedTextures( const std::unordered_map& textures) { auto normalizeKey = [](std::string key) { std::replace(key.begin(), key.end(), '/', '\\'); std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return key; }; // Batch all texture uploads into a single command buffer submission vkCtx->beginUploadBatch(); for (const auto& [path, blp] : textures) { std::string key = normalizeKey(path); if (textureCache.find(key) != textureCache.end()) continue; if (!blp.isValid()) continue; auto tex = std::make_unique(); if (!tex->upload(*vkCtx, blp.data.data(), blp.width, blp.height, VK_FORMAT_R8G8B8A8_UNORM, true)) continue; tex->createSampler(vkCtx->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_REPEAT); TextureCacheEntry e; e.texture = std::move(tex); size_t base = static_cast(blp.width) * static_cast(blp.height) * 4ull; e.approxBytes = base + (base / 3); e.lastUse = ++textureCacheCounter_; textureCacheBytes_ += e.approxBytes; textureCache[key] = std::move(e); } vkCtx->endUploadBatch(); } VkTexture* TerrainRenderer::createAlphaTexture(const std::vector& alphaData) { if (alphaData.empty()) return opaqueAlphaTexture.get(); std::vector expanded; const uint8_t* src = alphaData.data(); if (alphaData.size() < 4096) { expanded.assign(4096, 255); std::copy(alphaData.begin(), alphaData.end(), expanded.begin()); src = expanded.data(); } auto tex = std::make_unique(); if (!tex->upload(*vkCtx, src, 64, 64, VK_FORMAT_R8_UNORM, false)) { return opaqueAlphaTexture.get(); } tex->createSampler(vkCtx->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE); VkTexture* raw = tex.get(); static uint64_t alphaCounter = 0; std::string key = "__alpha_" + std::to_string(++alphaCounter); TextureCacheEntry e; e.texture = std::move(tex); e.approxBytes = 64 * 64; e.lastUse = ++textureCacheCounter_; textureCacheBytes_ += e.approxBytes; textureCache[key] = std::move(e); return raw; } VkDescriptorSet TerrainRenderer::allocateMaterialSet() { VkDescriptorSetAllocateInfo allocInfo{}; allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; allocInfo.descriptorPool = materialDescPool; allocInfo.descriptorSetCount = 1; allocInfo.pSetLayouts = &materialSetLayout; VkDescriptorSet set = VK_NULL_HANDLE; if (vkAllocateDescriptorSets(vkCtx->getDevice(), &allocInfo, &set) != VK_SUCCESS) { LOG_WARNING("TerrainRenderer: failed to allocate material descriptor set"); return VK_NULL_HANDLE; } return set; } void TerrainRenderer::writeMaterialDescriptors(VkDescriptorSet set, const TerrainChunkGPU& chunk) { VkTexture* white = whiteTexture.get(); VkTexture* opaque = opaqueAlphaTexture.get(); VkDescriptorImageInfo imageInfos[7]; imageInfos[0] = (chunk.baseTexture ? chunk.baseTexture : white)->descriptorInfo(); for (int i = 0; i < 3; i++) { imageInfos[1 + i] = (chunk.layerTextures[i] ? chunk.layerTextures[i] : white)->descriptorInfo(); imageInfos[4 + i] = (chunk.alphaTextures[i] ? chunk.alphaTextures[i] : opaque)->descriptorInfo(); } VkDescriptorBufferInfo bufInfo{}; bufInfo.buffer = chunk.paramsUBO; bufInfo.offset = 0; bufInfo.range = sizeof(TerrainParamsUBO); VkWriteDescriptorSet writes[8] = {}; for (int i = 0; i < 7; i++) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = set; writes[i].dstBinding = static_cast(i); writes[i].descriptorCount = 1; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[i].pImageInfo = &imageInfos[i]; } writes[7].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[7].dstSet = set; writes[7].dstBinding = 7; writes[7].descriptorCount = 1; writes[7].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[7].pBufferInfo = &bufInfo; vkUpdateDescriptorSets(vkCtx->getDevice(), 8, writes, 0, nullptr); } void TerrainRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) { if (chunks.empty() || !pipeline) { static int emptyLog = 0; if (++emptyLog <= 3) LOG_WARNING("TerrainRenderer::render: chunks=", chunks.size(), " pipeline=", (pipeline != VK_NULL_HANDLE)); return; } // One-time diagnostic: log chunk nearest to camera static bool loggedDiag = false; if (!loggedDiag && !chunks.empty()) { loggedDiag = true; glm::vec3 cam = camera.getPosition(); // Find chunk nearest to camera const TerrainChunkGPU* nearest = nullptr; float nearestDist = 1e30f; for (const auto& ch : chunks) { float dx = ch.boundingSphereCenter.x - cam.x; float dy = ch.boundingSphereCenter.y - cam.y; float dz = ch.boundingSphereCenter.z - cam.z; float d = dx*dx + dy*dy + dz*dz; if (d < nearestDist) { nearestDist = d; nearest = &ch; } } if (nearest) { float d2d = std::sqrt((nearest->boundingSphereCenter.x-cam.x)*(nearest->boundingSphereCenter.x-cam.x) + (nearest->boundingSphereCenter.y-cam.y)*(nearest->boundingSphereCenter.y-cam.y)); LOG_INFO("Terrain diag: chunks=", chunks.size(), " cam=(", cam.x, ",", cam.y, ",", cam.z, ")", " nearest_center=(", nearest->boundingSphereCenter.x, ",", nearest->boundingSphereCenter.y, ",", nearest->boundingSphereCenter.z, ")", " dist2d=", d2d, " dist3d=", std::sqrt(nearestDist), " radius=", nearest->boundingSphereRadius, " matSet=", (nearest->materialSet != VK_NULL_HANDLE ? "ok" : "NULL")); } } VkPipeline activePipeline = (wireframe && wireframePipeline) ? wireframePipeline : pipeline; vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, activePipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &perFrameSet, 0, nullptr); GPUPushConstants push{}; push.model = glm::mat4(1.0f); vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUPushConstants), &push); Frustum frustum; if (frustumCullingEnabled) { glm::mat4 viewProj = camera.getProjectionMatrix() * camera.getViewMatrix(); frustum.extractFromMatrix(viewProj); } glm::vec3 camPos = camera.getPosition(); const float maxTerrainDistSq = 1200.0f * 1200.0f; renderedChunks = 0; culledChunks = 0; for (const auto& chunk : chunks) { if (!chunk.isValid() || !chunk.materialSet) continue; float dx = chunk.boundingSphereCenter.x - camPos.x; float dy = chunk.boundingSphereCenter.y - camPos.y; float distSq = dx * dx + dy * dy; if (distSq > maxTerrainDistSq) { culledChunks++; continue; } if (frustumCullingEnabled && !isChunkVisible(chunk, frustum)) { culledChunks++; continue; } vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 1, 1, &chunk.materialSet, 0, nullptr); VkDeviceSize offset = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &chunk.vertexBuffer, &offset); vkCmdBindIndexBuffer(cmd, chunk.indexBuffer, 0, VK_INDEX_TYPE_UINT32); vkCmdDrawIndexed(cmd, chunk.indexCount, 1, 0, 0, 0); renderedChunks++; } } bool TerrainRenderer::initializeShadow(VkRenderPass shadowRenderPass) { if (!vkCtx || shadowRenderPass == VK_NULL_HANDLE) return false; if (shadowPipeline_ != VK_NULL_HANDLE) return true; // already initialised VkDevice device = vkCtx->getDevice(); VmaAllocator allocator = vkCtx->getAllocator(); // ShadowParams UBO — terrain uses no bones, no texture, no alpha test VkBufferCreateInfo bufCI{}; bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufCI.size = sizeof(ShadowParamsUBO); bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; VmaAllocationCreateInfo allocCI{}; allocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; allocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; VmaAllocationInfo allocInfo{}; if (vmaCreateBuffer(allocator, &bufCI, &allocCI, &shadowParamsUBO_, &shadowParamsAlloc_, &allocInfo) != VK_SUCCESS) { LOG_ERROR("TerrainRenderer: failed to create shadow params UBO"); return false; } ShadowParamsUBO defaultParams{}; std::memcpy(allocInfo.pMappedData, &defaultParams, sizeof(defaultParams)); // Descriptor set layout: binding 0 = combined sampler (unused), binding 1 = ShadowParams UBO VkDescriptorSetLayoutBinding layoutBindings[2]{}; layoutBindings[0].binding = 0; layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; layoutBindings[0].descriptorCount = 1; layoutBindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; layoutBindings[1].binding = 1; layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; layoutBindings[1].descriptorCount = 1; layoutBindings[1].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; VkDescriptorSetLayoutCreateInfo layoutCI{}; layoutCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; layoutCI.bindingCount = 2; layoutCI.pBindings = layoutBindings; if (vkCreateDescriptorSetLayout(device, &layoutCI, nullptr, &shadowParamsLayout_) != VK_SUCCESS) { LOG_ERROR("TerrainRenderer: failed to create shadow params set layout"); return false; } VkDescriptorPoolSize poolSizes[2]{}; poolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; poolSizes[0].descriptorCount = 1; poolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; poolSizes[1].descriptorCount = 1; VkDescriptorPoolCreateInfo poolCI{}; poolCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; poolCI.maxSets = 1; poolCI.poolSizeCount = 2; poolCI.pPoolSizes = poolSizes; if (vkCreateDescriptorPool(device, &poolCI, nullptr, &shadowParamsPool_) != VK_SUCCESS) { LOG_ERROR("TerrainRenderer: failed to create shadow params pool"); return false; } VkDescriptorSetAllocateInfo setAlloc{}; setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; setAlloc.descriptorPool = shadowParamsPool_; setAlloc.descriptorSetCount = 1; setAlloc.pSetLayouts = &shadowParamsLayout_; if (vkAllocateDescriptorSets(device, &setAlloc, &shadowParamsSet_) != VK_SUCCESS) { LOG_ERROR("TerrainRenderer: failed to allocate shadow params set"); return false; } // Write descriptors — sampler uses whiteTexture as dummy (useTexture=0 so never sampled) VkDescriptorBufferInfo bufInfo{ shadowParamsUBO_, 0, sizeof(ShadowParamsUBO) }; VkDescriptorImageInfo imgInfo{}; imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; imgInfo.imageView = whiteTexture->getImageView(); imgInfo.sampler = whiteTexture->getSampler(); VkWriteDescriptorSet writes[2]{}; writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[0].dstSet = shadowParamsSet_; writes[0].dstBinding = 0; writes[0].descriptorCount = 1; writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[0].pImageInfo = &imgInfo; writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[1].dstSet = shadowParamsSet_; writes[1].dstBinding = 1; writes[1].descriptorCount = 1; writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[1].pBufferInfo = &bufInfo; vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); // Pipeline layout: set 0 = shadowParamsLayout_, push 128 bytes (lightSpaceMatrix + model) VkPushConstantRange pc{}; pc.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; pc.offset = 0; pc.size = 128; shadowPipelineLayout_ = createPipelineLayout(device, {shadowParamsLayout_}, {pc}); if (!shadowPipelineLayout_) { LOG_ERROR("TerrainRenderer: failed to create shadow pipeline layout"); return false; } VkShaderModule vertShader, fragShader; if (!vertShader.loadFromFile(device, "assets/shaders/shadow.vert.spv")) { LOG_ERROR("TerrainRenderer: failed to load shadow vertex shader"); return false; } if (!fragShader.loadFromFile(device, "assets/shaders/shadow.frag.spv")) { LOG_ERROR("TerrainRenderer: failed to load shadow fragment shader"); vertShader.destroy(); return false; } // Terrain vertex layout: pos(0,off0) normal(1,off12) texCoord(2,off24) layerUV(3,off32) // stride = sizeof(TerrainVertex) = 44 bytes // Shadow shader expects: aPos(loc0), aTexCoord(loc1), aBoneWeights(loc2), aBoneIndicesF(loc3) // Alias unused bone attrs to position (offset 0); useBones=0 so they are never read. const uint32_t stride = static_cast(sizeof(pipeline::TerrainVertex)); VkVertexInputBindingDescription vertBind{}; vertBind.binding = 0; vertBind.stride = stride; vertBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; std::vector vertAttrs = { {0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // aPos -> position {1, 0, VK_FORMAT_R32G32_SFLOAT, 24}, // aTexCoord -> texCoord (unused) {2, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}, // aBoneWeights -> position (unused) {3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}, // aBoneIndices -> position (unused) }; shadowPipeline_ = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) .setVertexInput({vertBind}, vertAttrs) .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) .setDepthBias(0.05f, 0.20f) .setNoColorAttachment() .setLayout(shadowPipelineLayout_) .setRenderPass(shadowRenderPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) .build(device); vertShader.destroy(); fragShader.destroy(); if (!shadowPipeline_) { LOG_ERROR("TerrainRenderer: failed to create shadow pipeline"); return false; } LOG_INFO("TerrainRenderer shadow pipeline initialized"); return true; } void TerrainRenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix, const glm::vec3& shadowCenter, float shadowRadius) { if (!shadowPipeline_ || !shadowParamsSet_) return; if (chunks.empty()) return; vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipeline_); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_, 0, 1, &shadowParamsSet_, 0, nullptr); // Identity model matrix — terrain vertices are already in world space static const glm::mat4 identity(1.0f); ShadowPush push{ lightSpaceMatrix, identity }; vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, 128, &push); for (const auto& chunk : chunks) { if (!chunk.isValid()) continue; // Sphere-cull chunk against shadow region glm::vec3 diff = chunk.boundingSphereCenter - shadowCenter; float distSq = glm::dot(diff, diff); float combinedRadius = shadowRadius + chunk.boundingSphereRadius; if (distSq > combinedRadius * combinedRadius) continue; VkDeviceSize offset = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &chunk.vertexBuffer, &offset); vkCmdBindIndexBuffer(cmd, chunk.indexBuffer, 0, VK_INDEX_TYPE_UINT16); vkCmdDrawIndexed(cmd, chunk.indexCount, 1, 0, 0, 0); } } void TerrainRenderer::removeTile(int tileX, int tileY) { int removed = 0; auto it = chunks.begin(); while (it != chunks.end()) { if (it->tileX == tileX && it->tileY == tileY) { destroyChunkGPU(*it); it = chunks.erase(it); removed++; } else { ++it; } } if (removed > 0) { LOG_DEBUG("Removed ", removed, " terrain chunks for tile [", tileX, ",", tileY, "]"); } } void TerrainRenderer::clear() { if (!vkCtx) return; for (auto& chunk : chunks) { destroyChunkGPU(chunk); } chunks.clear(); renderedChunks = 0; if (materialDescPool) { vkResetDescriptorPool(vkCtx->getDevice(), materialDescPool, 0); } } void TerrainRenderer::destroyChunkGPU(TerrainChunkGPU& chunk) { VmaAllocator allocator = vkCtx->getAllocator(); if (chunk.vertexBuffer) { AllocatedBuffer ab{}; ab.buffer = chunk.vertexBuffer; ab.allocation = chunk.vertexAlloc; destroyBuffer(allocator, ab); chunk.vertexBuffer = VK_NULL_HANDLE; } if (chunk.indexBuffer) { AllocatedBuffer ab{}; ab.buffer = chunk.indexBuffer; ab.allocation = chunk.indexAlloc; destroyBuffer(allocator, ab); chunk.indexBuffer = VK_NULL_HANDLE; } if (chunk.paramsUBO) { AllocatedBuffer ab{}; ab.buffer = chunk.paramsUBO; ab.allocation = chunk.paramsAlloc; destroyBuffer(allocator, ab); chunk.paramsUBO = VK_NULL_HANDLE; } // Return material descriptor set to the pool so it can be reused by new chunks if (chunk.materialSet && materialDescPool) { vkFreeDescriptorSets(vkCtx->getDevice(), materialDescPool, 1, &chunk.materialSet); } chunk.materialSet = VK_NULL_HANDLE; // Destroy owned alpha textures (VkTexture::~VkTexture is a no-op, must call destroy() explicitly) VkDevice device = vkCtx->getDevice(); for (auto& tex : chunk.ownedAlphaTextures) { if (tex) tex->destroy(device, allocator); } chunk.ownedAlphaTextures.clear(); } int TerrainRenderer::getTriangleCount() const { int total = 0; for (const auto& chunk : chunks) { total += chunk.indexCount / 3; } return total; } bool TerrainRenderer::isChunkVisible(const TerrainChunkGPU& chunk, const Frustum& frustum) { return frustum.intersectsSphere(chunk.boundingSphereCenter, chunk.boundingSphereRadius); } void TerrainRenderer::calculateBoundingSphere(TerrainChunkGPU& gpuChunk, const pipeline::ChunkMesh& meshChunk) { if (meshChunk.vertices.empty()) { gpuChunk.boundingSphereRadius = 0.0f; gpuChunk.boundingSphereCenter = glm::vec3(0.0f); return; } glm::vec3 min(std::numeric_limits::max()); glm::vec3 max(std::numeric_limits::lowest()); for (const auto& vertex : meshChunk.vertices) { glm::vec3 pos(vertex.position[0], vertex.position[1], vertex.position[2]); min = glm::min(min, pos); max = glm::max(max, pos); } gpuChunk.boundingSphereCenter = (min + max) * 0.5f; float maxDistSq = 0.0f; for (const auto& vertex : meshChunk.vertices) { glm::vec3 pos(vertex.position[0], vertex.position[1], vertex.position[2]); glm::vec3 diff = pos - gpuChunk.boundingSphereCenter; float distSq = glm::dot(diff, diff); maxDistSq = std::max(maxDistSq, distSq); } gpuChunk.boundingSphereRadius = std::sqrt(maxDistSq); } } // namespace rendering } // namespace wowee