diff --git a/CMakeLists.txt b/CMakeLists.txt index cbcbe5ab..aa88c313 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -522,9 +522,14 @@ if(NOT MSVC) target_compile_options(wowee PRIVATE $<$:-fvisibility=hidden -fvisibility-inlines-hidden>) endif() -# Copy assets to build directory -file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/assets - DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) +# Copy assets to build directory (runs every build, not just configure) +add_custom_target(copy_assets ALL + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/assets + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets + COMMENT "Syncing assets to build directory" +) +add_dependencies(wowee copy_assets) # Install targets install(TARGETS wowee diff --git a/assets/krayonload.png b/assets/krayonload.png new file mode 100644 index 00000000..4d580744 Binary files /dev/null and b/assets/krayonload.png differ diff --git a/assets/krayonsignin.png b/assets/krayonsignin.png new file mode 100644 index 00000000..f0d3bfa4 Binary files /dev/null and b/assets/krayonsignin.png differ diff --git a/assets/loading1.jpeg b/assets/loading1.jpeg deleted file mode 100755 index 79cae0dc..00000000 Binary files a/assets/loading1.jpeg and /dev/null differ diff --git a/assets/loading2.jpeg b/assets/loading2.jpeg deleted file mode 100755 index 2403b28a..00000000 Binary files a/assets/loading2.jpeg and /dev/null differ diff --git a/assets/shaders/terrain.frag.glsl b/assets/shaders/terrain.frag.glsl index 07d53093..be0bfc98 100644 --- a/assets/shaders/terrain.frag.glsl +++ b/assets/shaders/terrain.frag.glsl @@ -55,18 +55,22 @@ float sampleAlpha(sampler2D tex, vec2 uv) { void main() { vec4 baseColor = texture(uBaseTexture, TexCoord); - float a1 = hasLayer1 != 0 ? sampleAlpha(uLayer1Alpha, LayerUV) : 0.0; - float a2 = hasLayer2 != 0 ? sampleAlpha(uLayer2Alpha, LayerUV) : 0.0; - float a3 = hasLayer3 != 0 ? sampleAlpha(uLayer3Alpha, LayerUV) : 0.0; - float w0 = 1.0, w1 = a1, w2 = a2, w3 = a3; - float sum = w0 + w1 + w2 + w3; - if (sum > 0.0) { w0 /= sum; w1 /= sum; w2 /= sum; w3 /= sum; } - - vec4 finalColor = baseColor * w0; - if (hasLayer1 != 0) finalColor += texture(uLayer1Texture, TexCoord) * w1; - if (hasLayer2 != 0) finalColor += texture(uLayer2Texture, TexCoord) * w2; - if (hasLayer3 != 0) finalColor += texture(uLayer3Texture, TexCoord) * w3; + // WoW terrain: layers are blended sequentially, each on top of the previous result. + // Alpha=1 means the layer fully covers everything below; alpha=0 means invisible. + vec4 finalColor = baseColor; + if (hasLayer1 != 0) { + float a1 = sampleAlpha(uLayer1Alpha, LayerUV); + finalColor = mix(finalColor, texture(uLayer1Texture, TexCoord), a1); + } + if (hasLayer2 != 0) { + float a2 = sampleAlpha(uLayer2Alpha, LayerUV); + finalColor = mix(finalColor, texture(uLayer2Texture, TexCoord), a2); + } + if (hasLayer3 != 0) { + float a3 = sampleAlpha(uLayer3Alpha, LayerUV); + finalColor = mix(finalColor, texture(uLayer3Texture, TexCoord), a3); + } vec3 norm = normalize(Normal); vec3 lightDir2 = normalize(-lightDir.xyz); diff --git a/assets/shaders/terrain.frag.spv b/assets/shaders/terrain.frag.spv index 61630749..2dfc8007 100644 Binary files a/assets/shaders/terrain.frag.spv and b/assets/shaders/terrain.frag.spv differ diff --git a/assets/startscreen.mp4 b/assets/startscreen.mp4 deleted file mode 100755 index d58bb228..00000000 Binary files a/assets/startscreen.mp4 and /dev/null differ diff --git a/include/rendering/celestial.hpp b/include/rendering/celestial.hpp index 6fbd871d..26abd6c6 100644 --- a/include/rendering/celestial.hpp +++ b/include/rendering/celestial.hpp @@ -32,6 +32,7 @@ public: */ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); + void recreatePipelines(); /** * Render celestial bodies (sun and moons). diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index ed73b51d..1ef0f24c 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -58,6 +58,7 @@ public: void update(float deltaTime, const glm::vec3& cameraPos = glm::vec3(0.0f)); void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera); + void recreatePipelines(); bool initializeShadow(VkRenderPass shadowRenderPass); void renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix); diff --git a/include/rendering/charge_effect.hpp b/include/rendering/charge_effect.hpp index eec5d514..4154df4d 100644 --- a/include/rendering/charge_effect.hpp +++ b/include/rendering/charge_effect.hpp @@ -24,6 +24,7 @@ public: bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); + void recreatePipelines(); /// Try to load M2 spell models (Charge_Caster.m2, etc.) void tryLoadM2Models(M2Renderer* m2Renderer, pipeline::AssetManager* assets); diff --git a/include/rendering/clouds.hpp b/include/rendering/clouds.hpp index d36a8058..fb8008ba 100644 --- a/include/rendering/clouds.hpp +++ b/include/rendering/clouds.hpp @@ -35,6 +35,7 @@ public: */ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); + void recreatePipelines(); /** * Render clouds. diff --git a/include/rendering/lens_flare.hpp b/include/rendering/lens_flare.hpp index 31473529..1578dd76 100644 --- a/include/rendering/lens_flare.hpp +++ b/include/rendering/lens_flare.hpp @@ -39,6 +39,8 @@ public: */ void shutdown(); + void recreatePipelines(); + /** * @brief Render lens flare effect * @param cmd Command buffer to record into diff --git a/include/rendering/lightning.hpp b/include/rendering/lightning.hpp index 354c1d1c..33152192 100644 --- a/include/rendering/lightning.hpp +++ b/include/rendering/lightning.hpp @@ -28,6 +28,7 @@ public: bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); + void recreatePipelines(); void update(float deltaTime, const Camera& camera); void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet); diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 16351936..2b626b86 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -281,6 +281,8 @@ public: double getQueryTimeMs() const { return queryTimeMs; } uint32_t getQueryCallCount() const { return queryCallCount; } + void recreatePipelines(); + // Stats bool isInitialized() const { return initialized_; } uint32_t getModelCount() const { return static_cast(models.size()); } diff --git a/include/rendering/mount_dust.hpp b/include/rendering/mount_dust.hpp index 553cfb56..de0cce60 100644 --- a/include/rendering/mount_dust.hpp +++ b/include/rendering/mount_dust.hpp @@ -18,6 +18,7 @@ public: bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); + void recreatePipelines(); // Spawn dust particles at mount feet when moving on ground void spawnDust(const glm::vec3& position, const glm::vec3& velocity, bool isMoving); diff --git a/include/rendering/quest_marker_renderer.hpp b/include/rendering/quest_marker_renderer.hpp index 8b5ff69e..2d6a73d3 100644 --- a/include/rendering/quest_marker_renderer.hpp +++ b/include/rendering/quest_marker_renderer.hpp @@ -27,6 +27,7 @@ public: bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* assetManager); void shutdown(); + void recreatePipelines(); /** * Add or update a quest marker at a position diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 1fad9f3c..dab7cf2b 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -241,6 +241,7 @@ private: public: void setShadowsEnabled(bool enabled) { shadowsEnabled = enabled; } bool areShadowsEnabled() const { return shadowsEnabled; } + void setMsaaSamples(VkSampleCountFlagBits samples); private: void renderShadowPass(); diff --git a/include/rendering/skybox.hpp b/include/rendering/skybox.hpp index 23a79db9..5e20c28c 100644 --- a/include/rendering/skybox.hpp +++ b/include/rendering/skybox.hpp @@ -24,6 +24,7 @@ public: bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); + void recreatePipelines(); /** * Render the skybox diff --git a/include/rendering/starfield.hpp b/include/rendering/starfield.hpp index 8a8793a9..8ac1e655 100644 --- a/include/rendering/starfield.hpp +++ b/include/rendering/starfield.hpp @@ -23,6 +23,7 @@ public: bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); + void recreatePipelines(); /** * Render the star field diff --git a/include/rendering/swim_effects.hpp b/include/rendering/swim_effects.hpp index 75c7c439..3ff643d4 100644 --- a/include/rendering/swim_effects.hpp +++ b/include/rendering/swim_effects.hpp @@ -20,6 +20,7 @@ public: bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); + void recreatePipelines(); void update(const Camera& camera, const CameraController& cc, const WaterRenderer& water, float deltaTime); void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet); diff --git a/include/rendering/terrain_renderer.hpp b/include/rendering/terrain_renderer.hpp index 34f2b666..0a451290 100644 --- a/include/rendering/terrain_renderer.hpp +++ b/include/rendering/terrain_renderer.hpp @@ -104,6 +104,8 @@ public: void clear(); + void recreatePipelines(); + void setWireframe(bool enabled) { wireframe = enabled; } void setFrustumCulling(bool enabled) { frustumCullingEnabled = enabled; } void setFogEnabled(bool enabled) { fogEnabled = enabled; } @@ -139,7 +141,7 @@ private: // Descriptor pool for material sets VkDescriptorPool materialDescPool = VK_NULL_HANDLE; - static constexpr uint32_t MAX_MATERIAL_SETS = 8192; + static constexpr uint32_t MAX_MATERIAL_SETS = 16384; // Loaded terrain chunks std::vector chunks; diff --git a/include/rendering/vk_context.hpp b/include/rendering/vk_context.hpp index b1ca4b58..625c244a 100644 --- a/include/rendering/vk_context.hpp +++ b/include/rendering/vk_context.hpp @@ -71,6 +71,11 @@ public: bool isSwapchainDirty() const { return swapchainDirty; } + // MSAA + VkSampleCountFlagBits getMsaaSamples() const { return msaaSamples_; } + void setMsaaSamples(VkSampleCountFlagBits samples); + VkSampleCountFlagBits getMaxUsableSampleCount() const; + private: bool createInstance(SDL_Window* window); bool createSurface(SDL_Window* window); @@ -126,6 +131,15 @@ private: bool createDepthBuffer(); void destroyDepthBuffer(); + // MSAA resources + VkSampleCountFlagBits msaaSamples_ = VK_SAMPLE_COUNT_1_BIT; + VkImage msaaColorImage_ = VK_NULL_HANDLE; + VkImageView msaaColorView_ = VK_NULL_HANDLE; + VmaAllocation msaaColorAllocation_ = VK_NULL_HANDLE; + + bool createMsaaColorImage(); + void destroyMsaaColorImage(); + // ImGui resources VkRenderPass imguiRenderPass = VK_NULL_HANDLE; VkDescriptorPool imguiDescriptorPool = VK_NULL_HANDLE; diff --git a/include/rendering/water_renderer.hpp b/include/rendering/water_renderer.hpp index 292b75d7..cbd3b121 100644 --- a/include/rendering/water_renderer.hpp +++ b/include/rendering/water_renderer.hpp @@ -79,6 +79,8 @@ public: void removeTile(int tileX, int tileY); void clear(); + void recreatePipelines(); + void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera, float time); void setEnabled(bool enabled) { renderingEnabled = enabled; } diff --git a/include/rendering/weather.hpp b/include/rendering/weather.hpp index 8423d91b..a971e02c 100644 --- a/include/rendering/weather.hpp +++ b/include/rendering/weather.hpp @@ -40,6 +40,7 @@ public: * @return true if initialization succeeded */ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); + void recreatePipelines(); /** * @brief Update weather particles diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 35dc3b2f..45d6a12c 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -156,6 +156,7 @@ public: /** * Get number of loaded models */ + void recreatePipelines(); bool isInitialized() const { return initialized_; } uint32_t getModelCount() const { return loadedModels.size(); } diff --git a/include/ui/auth_screen.hpp b/include/ui/auth_screen.hpp index 80ada9fa..484797e3 100644 --- a/include/ui/auth_screen.hpp +++ b/include/ui/auth_screen.hpp @@ -1,11 +1,13 @@ #pragma once #include "auth/auth_handler.hpp" -#include "rendering/video_player.hpp" +#include #include #include #include +namespace wowee { namespace rendering { class VkContext; } } + namespace wowee { namespace ui { /** @@ -103,9 +105,18 @@ private: void upsertCurrentServerProfile(bool includePasswordHash); std::string currentExpansionId() const; - // Background video - bool videoInitAttempted = false; - rendering::VideoPlayer backgroundVideo; + // Background image (Vulkan) + bool bgInitAttempted = false; + bool loadBackgroundImage(); + void destroyBackgroundImage(); + rendering::VkContext* bgVkCtx = nullptr; + VkImage bgImage = VK_NULL_HANDLE; + VkDeviceMemory bgMemory = VK_NULL_HANDLE; + VkImageView bgImageView = VK_NULL_HANDLE; + VkSampler bgSampler = VK_NULL_HANDLE; + VkDescriptorSet bgDescriptorSet = VK_NULL_HANDLE; + int bgWidth = 0; + int bgHeight = 0; bool musicInitAttempted = false; bool musicPlaying = false; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 1d939dd4..19eeeb43 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -102,6 +102,7 @@ private: bool pendingAutoLoot = false; bool pendingUseOriginalSoundtrack = true; int pendingGroundClutterDensity = 100; + int pendingAntiAliasing = 0; // 0=Off, 1=2x, 2=4x, 3=8x // UI element transparency (0.0 = fully transparent, 1.0 = fully opaque) float uiOpacity_ = 0.65f; @@ -110,6 +111,7 @@ private: bool minimapNpcDots_ = false; bool minimapSettingsApplied_ = false; bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers + bool msaaSettingsApplied_ = false; // True once saved MSAA setting applied to renderer // Mute state: mute bypasses master volume without touching slider values bool soundMuted_ = false; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ea00aa66..07b3221c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -722,6 +722,15 @@ void GameHandler::update(float deltaTime) { ? 0.25f : (classicLikeCombatSync ? 0.05f : moveHeartbeatInterval_); if (timeSinceLastMoveHeartbeat_ >= heartbeatInterval) { + // Debug: log heartbeat position periodically + static int hbCount = 0; + if (++hbCount <= 5 || hbCount % 60 == 0) { + glm::vec3 serverPos = core::coords::canonicalToServer( + glm::vec3(movementInfo.x, movementInfo.y, movementInfo.z)); + LOG_INFO("Heartbeat #", hbCount, " canonical=(", + movementInfo.x, ",", movementInfo.y, ",", movementInfo.z, + ") server=(", serverPos.x, ",", serverPos.y, ",", serverPos.z, ")"); + } sendMovement(Opcode::MSG_MOVE_HEARTBEAT); timeSinceLastMoveHeartbeat_ = 0.0f; } diff --git a/src/pipeline/adt_loader.cpp b/src/pipeline/adt_loader.cpp index fff4d07c..755d0596 100644 --- a/src/pipeline/adt_loader.cpp +++ b/src/pipeline/adt_loader.cpp @@ -328,10 +328,14 @@ void ADTLoader::parseMCNK(const uint8_t* data, size_t size, int chunkIndex, ADTT " holes=0x", std::hex, chunk.holes, std::dec); } - // Position (stored at offset 0x68 = 104 in MCNK header) - chunk.position[0] = readFloat(data, 104); // X - chunk.position[1] = readFloat(data, 108); // Y - chunk.position[2] = readFloat(data, 112); // Z + // MCNK position is in canonical WoW coordinates (NOT ADT placement space): + // offset 104: wowY (west axis, horizontal — unused, XY computed from tile indices) + // offset 108: wowX (north axis, horizontal — unused, XY computed from tile indices) + // offset 112: wowZ = HEIGHT BASE (MCVT heights are relative to this) + chunk.position[0] = readFloat(data, 104); // wowY (unused) + chunk.position[1] = readFloat(data, 108); // wowX (unused) + chunk.position[2] = readFloat(data, 112); // wowZ = height base + // Parse sub-chunks using offsets from MCNK header // WoW ADT sub-chunks may have their own 8-byte headers (magic+size) @@ -409,7 +413,11 @@ void ADTLoader::parseMCVT(const uint8_t* data, size_t size, MapChunk& chunk) { // Log height range for first chunk only static bool logged = false; if (!logged) { - LOG_DEBUG("MCVT height range: [", minHeight, ", ", maxHeight, "]"); + LOG_INFO("MCVT height range: [", minHeight, ", ", maxHeight, "]", + " (heights[0]=", chunk.heightMap.heights[0], + " heights[8]=", chunk.heightMap.heights[8], + " heights[136]=", chunk.heightMap.heights[136], + " heights[144]=", chunk.heightMap.heights[144], ")"); logged = true; } } diff --git a/src/pipeline/terrain_mesh.cpp b/src/pipeline/terrain_mesh.cpp index 9af14144..efb4f9da 100644 --- a/src/pipeline/terrain_mesh.cpp +++ b/src/pipeline/terrain_mesh.cpp @@ -1,4 +1,5 @@ #include "pipeline/terrain_mesh.hpp" +#include "core/coordinates.hpp" #include "core/logger.hpp" #include @@ -40,6 +41,7 @@ TerrainMesh TerrainMeshGenerator::generate(const ADTTerrain& terrain) { mesh.validChunkCount = validCount; + return mesh; } @@ -49,10 +51,24 @@ ChunkMesh TerrainMeshGenerator::generateChunkMesh(const MapChunk& chunk, int chu mesh.chunkX = chunkX; mesh.chunkY = chunkY; - // World position from chunk data - mesh.worldX = chunk.position[0]; - mesh.worldY = chunk.position[1]; - mesh.worldZ = chunk.position[2]; + // Compute render-space XY from tile/chunk indices (MCNK position fields are unreliable). + // tileX increases southward (renderY axis), tileY increases eastward (renderX axis). + // NW corner of tile: renderX = (32-tileY)*TILE_SIZE, renderY = (32-tileX)*TILE_SIZE + // Each chunk step goes east (–renderX) or south (–renderY). + const float tileNW_renderX = (32.0f - static_cast(tileY)) * core::coords::TILE_SIZE; + const float tileNW_renderY = (32.0f - static_cast(tileX)) * core::coords::TILE_SIZE; + mesh.worldX = tileNW_renderX - static_cast(chunkY) * CHUNK_SIZE; // iy controls renderX (east-west) + mesh.worldY = tileNW_renderY - static_cast(chunkX) * CHUNK_SIZE; // ix controls renderY (north-south) + mesh.worldZ = chunk.position[2]; // height base (wowZ) from MCNK offset 112 + + // Debug: log chunk positions for first tile + static int posLogCount = 0; + if (posLogCount < 5) { + posLogCount++; + LOG_INFO("Terrain chunk: tile(", tileX, ",", tileY, ") ix=", chunkX, " iy=", chunkY, + " worldXY=(", mesh.worldX, ",", mesh.worldY, ",", mesh.worldZ, ")", + " mcnk=(", chunk.position[0], ",", chunk.position[1], ",", chunk.position[2], ")"); + } // Generate vertices from heightmap (pass chunk grid indices and tile coords) mesh.vertices = generateVertices(chunk, chunkX, chunkY, tileX, tileY); @@ -167,19 +183,21 @@ ChunkMesh TerrainMeshGenerator::generateChunkMesh(const MapChunk& chunk, int chu return mesh; } -std::vector TerrainMeshGenerator::generateVertices(const MapChunk& chunk, [[maybe_unused]] int chunkX, [[maybe_unused]] int chunkY, [[maybe_unused]] int tileX, [[maybe_unused]] int tileY) { +std::vector TerrainMeshGenerator::generateVertices(const MapChunk& chunk, int chunkX, int chunkY, int tileX, int tileY) { std::vector vertices; vertices.reserve(145); // 145 vertices total const HeightMap& heightMap = chunk.heightMap; // WoW terrain uses 145 heights stored in a 9x17 row-major grid layout - const float unitSize = CHUNK_SIZE / 8.0f; // 66.67 units per vertex step + const float unitSize = CHUNK_SIZE / 8.0f; // 33.333/8 units per vertex step - // chunk.position contains world coordinates for this chunk's origin - // Both X and Y are at world scale (no scaling needed) - float chunkBaseX = chunk.position[0]; - float chunkBaseY = chunk.position[1]; + // Compute render-space base from tile/chunk indices (same formula as generateChunkMesh). + const float tileNW_renderX = (32.0f - static_cast(tileY)) * core::coords::TILE_SIZE; + const float tileNW_renderY = (32.0f - static_cast(tileX)) * core::coords::TILE_SIZE; + float chunkBaseX = tileNW_renderX - static_cast(chunkY) * CHUNK_SIZE; // iy controls renderX (east-west) + float chunkBaseY = tileNW_renderY - static_cast(chunkX) * CHUNK_SIZE; // ix controls renderY (north-south) + float chunkBaseZ = chunk.position[2]; // height base (wowZ) from MCNK offset 112 for (int index = 0; index < 145; index++) { int y = index / 17; // Row (0-8) @@ -196,11 +214,12 @@ std::vector TerrainMeshGenerator::generateVertices(const MapChunk TerrainVertex vertex; - // Position - match wowee.js coordinate layout (swap X/Y and negate) - // wowee.js: X = -(y * unitSize), Y = -(x * unitSize) - vertex.position[0] = chunkBaseX - (offsetY * unitSize); - vertex.position[1] = chunkBaseY - (offsetX * unitSize); - vertex.position[2] = chunk.position[2] + heightMap.heights[index]; + // Position in render space: + // MCVT rows (offsetY) go west→east = renderX decreasing + // MCVT columns (offsetX) go north→south = renderY decreasing + vertex.position[0] = chunkBaseX - (offsetY * unitSize); // renderX (row = west→east) + vertex.position[1] = chunkBaseY - (offsetX * unitSize); // renderY (col = north→south) + vertex.position[2] = chunkBaseZ + heightMap.heights[index]; // renderZ // Normal if (index * 3 + 2 < static_cast(chunk.normals.size())) { diff --git a/src/rendering/celestial.cpp b/src/rendering/celestial.cpp index 91b64fcd..34090fe5 100644 --- a/src/rendering/celestial.cpp +++ b/src/rendering/celestial.cpp @@ -86,6 +86,7 @@ bool Celestial::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // test on, write off (sky layer) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) @@ -106,6 +107,71 @@ bool Celestial::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) return true; } +void Celestial::recreatePipelines() { + if (!vkCtx_) return; + VkDevice device = vkCtx_->getDevice(); + + if (pipeline_ != VK_NULL_HANDLE) { vkDestroyPipeline(device, pipeline_, nullptr); pipeline_ = VK_NULL_HANDLE; } + + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/celestial.vert.spv")) { + LOG_ERROR("Celestial::recreatePipelines: failed to load vertex shader"); + return; + } + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/celestial.frag.spv")) { + LOG_ERROR("Celestial::recreatePipelines: failed to load fragment shader"); + vertModule.destroy(); + return; + } + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + // Vertex input (same as initialize) + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 5 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32B32_SFLOAT; + posAttr.offset = 0; + + VkVertexInputAttributeDescription uvAttr{}; + uvAttr.location = 1; + uvAttr.binding = 0; + uvAttr.format = VK_FORMAT_R32G32_SFLOAT; + uvAttr.offset = 3 * sizeof(float); + + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + pipeline_ = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr, uvAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(pipelineLayout_) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (pipeline_ == VK_NULL_HANDLE) { + LOG_ERROR("Celestial::recreatePipelines: failed to create pipeline"); + } +} + void Celestial::shutdown() { destroyQuad(); diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 68e37451..63ea6833 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -210,6 +210,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(blendState) + .setMultisample(vkCtx_->getMsaaSamples()) .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) @@ -2564,5 +2565,69 @@ void CharacterRenderer::dumpAnimations(uint32_t instanceId) const { core::Logger::getInstance().info("=== End animation dump ==="); } +void CharacterRenderer::recreatePipelines() { + if (!vkCtx_) return; + VkDevice device = vkCtx_->getDevice(); + vkDeviceWaitIdle(device); + + // Destroy old main-pass pipelines (NOT shadow, NOT pipeline layout) + if (opaquePipeline_) { vkDestroyPipeline(device, opaquePipeline_, nullptr); opaquePipeline_ = VK_NULL_HANDLE; } + if (alphaTestPipeline_) { vkDestroyPipeline(device, alphaTestPipeline_, nullptr); alphaTestPipeline_ = VK_NULL_HANDLE; } + if (alphaPipeline_) { vkDestroyPipeline(device, alphaPipeline_, nullptr); alphaPipeline_ = VK_NULL_HANDLE; } + if (additivePipeline_) { vkDestroyPipeline(device, additivePipeline_, nullptr); additivePipeline_ = VK_NULL_HANDLE; } + + // --- Load shaders --- + rendering::VkShaderModule charVert, charFrag; + charVert.loadFromFile(device, "assets/shaders/character.vert.spv"); + charFrag.loadFromFile(device, "assets/shaders/character.frag.spv"); + + if (!charVert.isValid() || !charFrag.isValid()) { + LOG_ERROR("CharacterRenderer::recreatePipelines: missing required shaders"); + return; + } + + VkRenderPass mainPass = vkCtx_->getImGuiRenderPass(); + + // --- Vertex input --- + VkVertexInputBindingDescription charBinding{}; + charBinding.binding = 0; + charBinding.stride = sizeof(pipeline::M2Vertex); + charBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector charAttrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast(offsetof(pipeline::M2Vertex, position))}, + {1, 0, VK_FORMAT_R8G8B8A8_UNORM, static_cast(offsetof(pipeline::M2Vertex, boneWeights))}, + {2, 0, VK_FORMAT_R8G8B8A8_UINT, static_cast(offsetof(pipeline::M2Vertex, boneIndices))}, + {3, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast(offsetof(pipeline::M2Vertex, normal))}, + {4, 0, VK_FORMAT_R32G32_SFLOAT, static_cast(offsetof(pipeline::M2Vertex, texCoords))}, + }; + + auto buildCharPipeline = [&](VkPipelineColorBlendAttachmentState blendState, bool depthWrite) -> VkPipeline { + return PipelineBuilder() + .setShaders(charVert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + charFrag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({charBinding}, charAttrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(blendState) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(pipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); + }; + + opaquePipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); + alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), true); + alphaPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), false); + additivePipeline_ = buildCharPipeline(PipelineBuilder::blendAdditive(), false); + + charVert.destroy(); + charFrag.destroy(); + + core::Logger::getInstance().info("CharacterRenderer: pipelines recreated"); +} + } // namespace rendering } // namespace wowee diff --git a/src/rendering/charge_effect.cpp b/src/rendering/charge_effect.cpp index 5b2ac217..d6fba4de 100644 --- a/src/rendering/charge_effect.cpp +++ b/src/rendering/charge_effect.cpp @@ -97,6 +97,7 @@ bool ChargeEffect::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayo .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS) .setColorBlendAttachment(PipelineBuilder::blendAdditive()) // Additive blend for fiery glow + .setMultisample(vkCtx_->getMsaaSamples()) .setLayout(ribbonPipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) @@ -160,6 +161,7 @@ bool ChargeEffect::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayo .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) .setLayout(dustPipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) @@ -249,6 +251,122 @@ void ChargeEffect::shutdown() { dustPuffs_.clear(); } +void ChargeEffect::recreatePipelines() { + if (!vkCtx_) return; + VkDevice device = vkCtx_->getDevice(); + + // Destroy old pipelines (NOT layouts) + if (ribbonPipeline_ != VK_NULL_HANDLE) { + vkDestroyPipeline(device, ribbonPipeline_, nullptr); + ribbonPipeline_ = VK_NULL_HANDLE; + } + if (dustPipeline_ != VK_NULL_HANDLE) { + vkDestroyPipeline(device, dustPipeline_, nullptr); + dustPipeline_ = VK_NULL_HANDLE; + } + + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + // ---- Rebuild ribbon trail pipeline (TRIANGLE_STRIP) ---- + { + VkShaderModule vertModule; + vertModule.loadFromFile(device, "assets/shaders/charge_ribbon.vert.spv"); + VkShaderModule fragModule; + fragModule.loadFromFile(device, "assets/shaders/charge_ribbon.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 = 6 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector attrs(4); + attrs[0].location = 0; + attrs[0].binding = 0; + attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT; + attrs[0].offset = 0; + attrs[1].location = 1; + attrs[1].binding = 0; + attrs[1].format = VK_FORMAT_R32_SFLOAT; + attrs[1].offset = 3 * sizeof(float); + attrs[2].location = 2; + attrs[2].binding = 0; + attrs[2].format = VK_FORMAT_R32_SFLOAT; + attrs[2].offset = 4 * sizeof(float); + attrs[3].location = 3; + attrs[3].binding = 0; + attrs[3].format = VK_FORMAT_R32_SFLOAT; + attrs[3].offset = 5 * sizeof(float); + + ribbonPipeline_ = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAdditive()) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(ribbonPipelineLayout_) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + } + + // ---- Rebuild dust puff pipeline (POINT_LIST) ---- + { + VkShaderModule vertModule; + vertModule.loadFromFile(device, "assets/shaders/charge_dust.vert.spv"); + VkShaderModule fragModule; + fragModule.loadFromFile(device, "assets/shaders/charge_dust.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 = 5 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector attrs(3); + attrs[0].location = 0; + attrs[0].binding = 0; + attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT; + attrs[0].offset = 0; + attrs[1].location = 1; + attrs[1].binding = 0; + attrs[1].format = VK_FORMAT_R32_SFLOAT; + attrs[1].offset = 3 * sizeof(float); + attrs[2].location = 2; + attrs[2].binding = 0; + attrs[2].format = VK_FORMAT_R32_SFLOAT; + attrs[2].offset = 4 * sizeof(float); + + dustPipeline_ = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(dustPipelineLayout_) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + } +} + void ChargeEffect::tryLoadM2Models(M2Renderer* m2Renderer, pipeline::AssetManager* assets) { if (!m2Renderer || !assets) return; m2Renderer_ = m2Renderer; diff --git a/src/rendering/clouds.cpp b/src/rendering/clouds.cpp index 9b90d4a0..22dcf342 100644 --- a/src/rendering/clouds.cpp +++ b/src/rendering/clouds.cpp @@ -79,6 +79,7 @@ bool Clouds::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // test on, write off (sky layer) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) @@ -100,6 +101,65 @@ bool Clouds::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { return true; } +void Clouds::recreatePipelines() { + if (!vkCtx_) return; + VkDevice device = vkCtx_->getDevice(); + + if (pipeline_ != VK_NULL_HANDLE) { vkDestroyPipeline(device, pipeline_, nullptr); pipeline_ = VK_NULL_HANDLE; } + + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/clouds.vert.spv")) { + LOG_ERROR("Clouds::recreatePipelines: failed to load vertex shader"); + return; + } + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/clouds.frag.spv")) { + LOG_ERROR("Clouds::recreatePipelines: failed to load fragment shader"); + vertModule.destroy(); + return; + } + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + // Vertex input (same as initialize) + 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; + + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + pipeline_ = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(pipelineLayout_) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (pipeline_ == VK_NULL_HANDLE) { + LOG_ERROR("Clouds::recreatePipelines: failed to create pipeline"); + } +} + void Clouds::shutdown() { destroyBuffers(); diff --git a/src/rendering/lens_flare.cpp b/src/rendering/lens_flare.cpp index c5350a31..07b3b3fd 100644 --- a/src/rendering/lens_flare.cpp +++ b/src/rendering/lens_flare.cpp @@ -105,6 +105,7 @@ bool LensFlare::initialize(VkContext* ctx, VkDescriptorSetLayout /*perFrameLayou .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setNoDepthTest() .setColorBlendAttachment(PipelineBuilder::blendAdditive()) + .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) @@ -146,6 +147,63 @@ void LensFlare::shutdown() { vkCtx = nullptr; } +void LensFlare::recreatePipelines() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + + // Destroy old pipeline (NOT layout) + if (pipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, pipeline, nullptr); + pipeline = VK_NULL_HANDLE; + } + + VkShaderModule vertModule; + vertModule.loadFromFile(device, "assets/shaders/lens_flare.vert.spv"); + VkShaderModule fragModule; + fragModule.loadFromFile(device, "assets/shaders/lens_flare.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 = 4 * 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; + + VkVertexInputAttributeDescription uvAttr{}; + uvAttr.location = 1; + uvAttr.binding = 0; + uvAttr.format = VK_FORMAT_R32G32_SFLOAT; + uvAttr.offset = 2 * sizeof(float); + + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + pipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr, uvAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() + .setColorBlendAttachment(PipelineBuilder::blendAdditive()) + .setMultisample(vkCtx->getMsaaSamples()) + .setLayout(pipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); +} + void LensFlare::generateFlareElements() { flareElements.clear(); diff --git a/src/rendering/lightning.cpp b/src/rendering/lightning.cpp index 3414cf70..9dbd1b95 100644 --- a/src/rendering/lightning.cpp +++ b/src/rendering/lightning.cpp @@ -103,6 +103,7 @@ bool Lightning::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .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) @@ -164,6 +165,7 @@ bool Lightning::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setNoDepthTest() .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) .setLayout(flashPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) @@ -253,6 +255,102 @@ void Lightning::shutdown() { 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 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); + + 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); + + vertModule.destroy(); + fragModule.destroy(); + } +} + void Lightning::update(float deltaTime, const Camera& camera) { if (!enabled) { return; diff --git a/src/rendering/loading_screen.cpp b/src/rendering/loading_screen.cpp index 8c91dc18..f0795cbe 100644 --- a/src/rendering/loading_screen.cpp +++ b/src/rendering/loading_screen.cpp @@ -16,8 +16,7 @@ namespace wowee { namespace rendering { LoadingScreen::LoadingScreen() { - imagePaths.push_back("assets/loading1.jpeg"); - imagePaths.push_back("assets/loading2.jpeg"); + imagePaths.push_back("assets/krayonload.png"); } LoadingScreen::~LoadingScreen() { diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 7c1af188..7514e361 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -468,6 +468,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(blendState) + .setMultisample(vkCtx_->getMsaaSamples()) .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) @@ -502,6 +503,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(blend) + .setMultisample(vkCtx_->getMsaaSamples()) .setLayout(particlePipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) @@ -534,6 +536,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) .setLayout(smokePipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) @@ -3677,5 +3680,144 @@ float M2Renderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3& return closestHit; } +void M2Renderer::recreatePipelines() { + if (!vkCtx_) return; + VkDevice device = vkCtx_->getDevice(); + vkDeviceWaitIdle(device); + + // Destroy old main-pass pipelines (NOT shadow, NOT pipeline layouts) + if (opaquePipeline_) { vkDestroyPipeline(device, opaquePipeline_, nullptr); opaquePipeline_ = VK_NULL_HANDLE; } + if (alphaTestPipeline_) { vkDestroyPipeline(device, alphaTestPipeline_, nullptr); alphaTestPipeline_ = VK_NULL_HANDLE; } + if (alphaPipeline_) { vkDestroyPipeline(device, alphaPipeline_, nullptr); alphaPipeline_ = VK_NULL_HANDLE; } + if (additivePipeline_) { vkDestroyPipeline(device, additivePipeline_, nullptr); additivePipeline_ = VK_NULL_HANDLE; } + if (particlePipeline_) { vkDestroyPipeline(device, particlePipeline_, nullptr); particlePipeline_ = VK_NULL_HANDLE; } + if (particleAdditivePipeline_) { vkDestroyPipeline(device, particleAdditivePipeline_, nullptr); particleAdditivePipeline_ = VK_NULL_HANDLE; } + if (smokePipeline_) { vkDestroyPipeline(device, smokePipeline_, nullptr); smokePipeline_ = VK_NULL_HANDLE; } + + // --- Load shaders --- + rendering::VkShaderModule m2Vert, m2Frag; + rendering::VkShaderModule particleVert, particleFrag; + rendering::VkShaderModule smokeVert, smokeFrag; + + m2Vert.loadFromFile(device, "assets/shaders/m2.vert.spv"); + m2Frag.loadFromFile(device, "assets/shaders/m2.frag.spv"); + particleVert.loadFromFile(device, "assets/shaders/m2_particle.vert.spv"); + particleFrag.loadFromFile(device, "assets/shaders/m2_particle.frag.spv"); + smokeVert.loadFromFile(device, "assets/shaders/m2_smoke.vert.spv"); + smokeFrag.loadFromFile(device, "assets/shaders/m2_smoke.frag.spv"); + + if (!m2Vert.isValid() || !m2Frag.isValid()) { + LOG_ERROR("M2Renderer::recreatePipelines: missing required shaders"); + return; + } + + VkRenderPass mainPass = vkCtx_->getImGuiRenderPass(); + + // --- M2 model vertex input --- + VkVertexInputBindingDescription m2Binding{}; + m2Binding.binding = 0; + m2Binding.stride = 18 * sizeof(float); + m2Binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector m2Attrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // position + {1, 0, VK_FORMAT_R32G32B32_SFLOAT, 3 * sizeof(float)}, // normal + {2, 0, VK_FORMAT_R32G32_SFLOAT, 6 * sizeof(float)}, // texCoord0 + {5, 0, VK_FORMAT_R32G32_SFLOAT, 8 * sizeof(float)}, // texCoord1 + {3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 10 * sizeof(float)}, // boneWeights + {4, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 14 * sizeof(float)}, // boneIndices (float) + }; + + auto buildM2Pipeline = [&](VkPipelineColorBlendAttachmentState blendState, bool depthWrite) -> VkPipeline { + return PipelineBuilder() + .setShaders(m2Vert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + m2Frag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({m2Binding}, m2Attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(blendState) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(pipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); + }; + + opaquePipeline_ = buildM2Pipeline(PipelineBuilder::blendDisabled(), true); + alphaTestPipeline_ = buildM2Pipeline(PipelineBuilder::blendAlpha(), true); + alphaPipeline_ = buildM2Pipeline(PipelineBuilder::blendAlpha(), false); + additivePipeline_ = buildM2Pipeline(PipelineBuilder::blendAdditive(), false); + + // --- Particle pipelines --- + if (particleVert.isValid() && particleFrag.isValid()) { + VkVertexInputBindingDescription pBind{}; + pBind.binding = 0; + pBind.stride = 9 * sizeof(float); // pos3 + color4 + size1 + tile1 + pBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector pAttrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // position + {1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 3 * sizeof(float)}, // color + {2, 0, VK_FORMAT_R32_SFLOAT, 7 * sizeof(float)}, // size + {3, 0, VK_FORMAT_R32_SFLOAT, 8 * sizeof(float)}, // tile + }; + + auto buildParticlePipeline = [&](VkPipelineColorBlendAttachmentState blend) -> VkPipeline { + return PipelineBuilder() + .setShaders(particleVert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + particleFrag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({pBind}, pAttrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(blend) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(particlePipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); + }; + + particlePipeline_ = buildParticlePipeline(PipelineBuilder::blendAlpha()); + particleAdditivePipeline_ = buildParticlePipeline(PipelineBuilder::blendAdditive()); + } + + // --- Smoke pipeline --- + if (smokeVert.isValid() && smokeFrag.isValid()) { + VkVertexInputBindingDescription sBind{}; + sBind.binding = 0; + sBind.stride = 6 * sizeof(float); // pos3 + lifeRatio1 + size1 + isSpark1 + sBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector sAttrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // position + {1, 0, VK_FORMAT_R32_SFLOAT, 3 * sizeof(float)}, // lifeRatio + {2, 0, VK_FORMAT_R32_SFLOAT, 4 * sizeof(float)}, // size + {3, 0, VK_FORMAT_R32_SFLOAT, 5 * sizeof(float)}, // isSpark + }; + + smokePipeline_ = PipelineBuilder() + .setShaders(smokeVert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + smokeFrag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({sBind}, sAttrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(smokePipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); + } + + m2Vert.destroy(); m2Frag.destroy(); + particleVert.destroy(); particleFrag.destroy(); + smokeVert.destroy(); smokeFrag.destroy(); + + core::Logger::getInstance().info("M2Renderer: pipelines recreated"); +} + } // namespace rendering } // namespace wowee diff --git a/src/rendering/minimap.cpp b/src/rendering/minimap.cpp index 0042d803..47c6b619 100644 --- a/src/rendering/minimap.cpp +++ b/src/rendering/minimap.cpp @@ -446,7 +446,7 @@ void Minimap::render(VkCommandBuffer cmd, const Camera& playerCamera, float pixelW = static_cast(mapSize) / screenWidth; float pixelH = static_cast(mapSize) / screenHeight; float x = 1.0f - pixelW - margin / screenWidth; - float y = 1.0f - pixelH - margin / screenHeight; + float y = margin / screenHeight; // top edge in Vulkan (y=0 is top) // Compute player's UV in the composite texture constexpr float TILE_SIZE = core::coords::TILE_SIZE; diff --git a/src/rendering/mount_dust.cpp b/src/rendering/mount_dust.cpp index 98098325..5678f31c 100644 --- a/src/rendering/mount_dust.cpp +++ b/src/rendering/mount_dust.cpp @@ -88,6 +88,7 @@ bool MountDust::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) @@ -145,6 +146,65 @@ void MountDust::shutdown() { particles.clear(); } +void MountDust::recreatePipelines() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + + // Destroy old pipeline (NOT layout) + if (pipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, pipeline, nullptr); + pipeline = VK_NULL_HANDLE; + } + + VkShaderModule vertModule; + vertModule.loadFromFile(device, "assets/shaders/mount_dust.vert.spv"); + VkShaderModule fragModule; + fragModule.loadFromFile(device, "assets/shaders/mount_dust.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 = 5 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector attrs(3); + attrs[0].location = 0; + attrs[0].binding = 0; + attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT; + attrs[0].offset = 0; + attrs[1].location = 1; + attrs[1].binding = 0; + attrs[1].format = VK_FORMAT_R32_SFLOAT; + attrs[1].offset = 3 * sizeof(float); + attrs[2].location = 2; + attrs[2].binding = 0; + attrs[2].format = VK_FORMAT_R32_SFLOAT; + attrs[2].offset = 4 * sizeof(float); + + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + pipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) + .setLayout(pipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); +} + void MountDust::spawnDust(const glm::vec3& position, const glm::vec3& velocity, bool isMoving) { if (!isMoving) { spawnAccum = 0.0f; diff --git a/src/rendering/quest_marker_renderer.cpp b/src/rendering/quest_marker_renderer.cpp index 4a52c09d..d07096e3 100644 --- a/src/rendering/quest_marker_renderer.cpp +++ b/src/rendering/quest_marker_renderer.cpp @@ -108,6 +108,7 @@ bool QuestMarkerRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFr .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS) // depth test on, write off .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) @@ -179,6 +180,63 @@ void QuestMarkerRenderer::shutdown() { vkCtx_ = nullptr; } +void QuestMarkerRenderer::recreatePipelines() { + if (!vkCtx_) return; + VkDevice device = vkCtx_->getDevice(); + + // Destroy old pipeline (NOT layout) + if (pipeline_ != VK_NULL_HANDLE) { + vkDestroyPipeline(device, pipeline_, nullptr); + pipeline_ = VK_NULL_HANDLE; + } + + VkShaderModule vertModule; + vertModule.loadFromFile(device, "assets/shaders/quest_marker.vert.spv"); + VkShaderModule fragModule; + fragModule.loadFromFile(device, "assets/shaders/quest_marker.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 = 5 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32B32_SFLOAT; + posAttr.offset = 0; + + VkVertexInputAttributeDescription uvAttr{}; + uvAttr.location = 1; + uvAttr.binding = 0; + uvAttr.format = VK_FORMAT_R32G32_SFLOAT; + uvAttr.offset = 3 * sizeof(float); + + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + pipeline_ = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr, uvAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(pipelineLayout_) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); +} + void QuestMarkerRenderer::createDescriptorResources() { VkDevice device = vkCtx_->getDevice(); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index fc3418d1..fccef846 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -723,6 +723,66 @@ void Renderer::shutdown() { LOG_INFO("Renderer shutdown"); } +void Renderer::setMsaaSamples(VkSampleCountFlagBits samples) { + if (!vkCtx) return; + + VkSampleCountFlagBits current = vkCtx->getMsaaSamples(); + if (samples == current) return; + + LOG_INFO("Changing MSAA from ", static_cast(current), "x to ", static_cast(samples), "x"); + + vkDeviceWaitIdle(vkCtx->getDevice()); + + // Set new MSAA and recreate swapchain (render pass, depth, MSAA image, framebuffers) + vkCtx->setMsaaSamples(samples); + vkCtx->recreateSwapchain(window->getWidth(), window->getHeight()); + + // Recreate all sub-renderer pipelines (they embed sample count from render pass) + if (terrainRenderer) terrainRenderer->recreatePipelines(); + if (waterRenderer) waterRenderer->recreatePipelines(); + if (wmoRenderer) wmoRenderer->recreatePipelines(); + if (m2Renderer) m2Renderer->recreatePipelines(); + if (characterRenderer) characterRenderer->recreatePipelines(); + if (questMarkerRenderer) questMarkerRenderer->recreatePipelines(); + if (weather) weather->recreatePipelines(); + if (swimEffects) swimEffects->recreatePipelines(); + if (mountDust) mountDust->recreatePipelines(); + if (chargeEffect) chargeEffect->recreatePipelines(); + + // Sky system sub-renderers + if (skySystem) { + if (auto* sb = skySystem->getSkybox()) sb->recreatePipelines(); + if (auto* sf = skySystem->getStarField()) sf->recreatePipelines(); + if (auto* ce = skySystem->getCelestial()) ce->recreatePipelines(); + if (auto* cl = skySystem->getClouds()) cl->recreatePipelines(); + if (auto* lf = skySystem->getLensFlare()) lf->recreatePipelines(); + } + + // Lightning is standalone (not instantiated in Renderer, no action needed) + // Selection circle + overlay use lazy init, just destroy them + VkDevice device = vkCtx->getDevice(); + if (selCirclePipeline) { vkDestroyPipeline(device, selCirclePipeline, nullptr); selCirclePipeline = VK_NULL_HANDLE; } + if (overlayPipeline) { vkDestroyPipeline(device, overlayPipeline, nullptr); overlayPipeline = VK_NULL_HANDLE; } + + // Reinitialize ImGui Vulkan backend with new MSAA sample count + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplVulkan_InitInfo initInfo{}; + initInfo.ApiVersion = VK_API_VERSION_1_1; + initInfo.Instance = vkCtx->getInstance(); + initInfo.PhysicalDevice = vkCtx->getPhysicalDevice(); + initInfo.Device = vkCtx->getDevice(); + initInfo.QueueFamily = vkCtx->getGraphicsQueueFamily(); + initInfo.Queue = vkCtx->getGraphicsQueue(); + initInfo.DescriptorPool = vkCtx->getImGuiDescriptorPool(); + initInfo.MinImageCount = 2; + initInfo.ImageCount = vkCtx->getSwapchainImageCount(); + initInfo.PipelineInfoMain.RenderPass = vkCtx->getImGuiRenderPass(); + initInfo.PipelineInfoMain.MSAASamples = vkCtx->getMsaaSamples(); + ImGui_ImplVulkan_Init(&initInfo); + + LOG_INFO("MSAA change complete"); +} + void Renderer::beginFrame() { if (!vkCtx) return; @@ -2784,6 +2844,7 @@ void Renderer::initSelectionCircle() { .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setNoDepthTest() .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) .setLayout(selCirclePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) @@ -2894,6 +2955,7 @@ void Renderer::initOverlayPipeline() { .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setNoDepthTest() .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) .setLayout(overlayPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) diff --git a/src/rendering/skybox.cpp b/src/rendering/skybox.cpp index a51e2706..e85ce38c 100644 --- a/src/rendering/skybox.cpp +++ b/src/rendering/skybox.cpp @@ -66,6 +66,7 @@ bool Skybox::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // depth test on, write off, LEQUAL for far plane .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) @@ -84,6 +85,53 @@ bool Skybox::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { return true; } +void Skybox::recreatePipelines() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + + if (pipeline != VK_NULL_HANDLE) { vkDestroyPipeline(device, pipeline, nullptr); pipeline = VK_NULL_HANDLE; } + + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/skybox.vert.spv")) { + LOG_ERROR("Skybox::recreatePipelines: failed to load vertex shader"); + return; + } + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/skybox.frag.spv")) { + LOG_ERROR("Skybox::recreatePipelines: failed to load fragment shader"); + vertModule.destroy(); + return; + } + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + pipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({}, {}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setMultisample(vkCtx->getMsaaSamples()) + .setLayout(pipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (pipeline == VK_NULL_HANDLE) { + LOG_ERROR("Skybox::recreatePipelines: failed to create pipeline"); + } +} + void Skybox::shutdown() { if (vkCtx) { VkDevice device = vkCtx->getDevice(); diff --git a/src/rendering/starfield.cpp b/src/rendering/starfield.cpp index 35371e84..e472bc8d 100644 --- a/src/rendering/starfield.cpp +++ b/src/rendering/starfield.cpp @@ -88,6 +88,7 @@ bool StarField::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // depth test, no write (stars behind sky) .setColorBlendAttachment(PipelineBuilder::blendAdditive()) + .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .build(device); @@ -108,6 +109,71 @@ bool StarField::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) return true; } +void StarField::recreatePipelines() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + + if (pipeline != VK_NULL_HANDLE) { vkDestroyPipeline(device, pipeline, nullptr); pipeline = VK_NULL_HANDLE; } + + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/starfield.vert.spv")) { + LOG_ERROR("StarField::recreatePipelines: failed to load vertex shader"); + return; + } + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/starfield.frag.spv")) { + LOG_ERROR("StarField::recreatePipelines: failed to load fragment shader"); + vertModule.destroy(); + return; + } + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + // Vertex input (same as initialize) + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 5 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32B32_SFLOAT; + posAttr.offset = 0; + + VkVertexInputAttributeDescription brightnessAttr{}; + brightnessAttr.location = 1; + brightnessAttr.binding = 0; + brightnessAttr.format = VK_FORMAT_R32_SFLOAT; + brightnessAttr.offset = 3 * sizeof(float); + + VkVertexInputAttributeDescription twinkleAttr{}; + twinkleAttr.location = 2; + twinkleAttr.binding = 0; + twinkleAttr.format = VK_FORMAT_R32_SFLOAT; + twinkleAttr.offset = 4 * sizeof(float); + + pipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr, brightnessAttr, twinkleAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendAdditive()) + .setMultisample(vkCtx->getMsaaSamples()) + .setLayout(pipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (pipeline == VK_NULL_HANDLE) { + LOG_ERROR("StarField::recreatePipelines: failed to create pipeline"); + } +} + void StarField::shutdown() { destroyStarBuffers(); diff --git a/src/rendering/swim_effects.cpp b/src/rendering/swim_effects.cpp index bfcfd2ce..38603be9 100644 --- a/src/rendering/swim_effects.cpp +++ b/src/rendering/swim_effects.cpp @@ -93,6 +93,7 @@ bool SwimEffects::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) .setLayout(ripplePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) @@ -136,6 +137,7 @@ bool SwimEffects::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) .setLayout(bubblePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) @@ -225,6 +227,100 @@ void SwimEffects::shutdown() { bubbles.clear(); } +void SwimEffects::recreatePipelines() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + + // Destroy old pipelines (NOT layouts) + if (ripplePipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, ripplePipeline, nullptr); + ripplePipeline = VK_NULL_HANDLE; + } + if (bubblePipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, bubblePipeline, nullptr); + bubblePipeline = VK_NULL_HANDLE; + } + + // Shared vertex input: pos(vec3) + size(float) + alpha(float) = 5 floats + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 5 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector attrs(3); + attrs[0].location = 0; + attrs[0].binding = 0; + attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT; + attrs[0].offset = 0; + attrs[1].location = 1; + attrs[1].binding = 0; + attrs[1].format = VK_FORMAT_R32_SFLOAT; + attrs[1].offset = 3 * sizeof(float); + attrs[2].location = 2; + attrs[2].binding = 0; + attrs[2].format = VK_FORMAT_R32_SFLOAT; + attrs[2].offset = 4 * sizeof(float); + + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + // ---- Rebuild ripple pipeline ---- + { + VkShaderModule vertModule; + vertModule.loadFromFile(device, "assets/shaders/swim_ripple.vert.spv"); + VkShaderModule fragModule; + fragModule.loadFromFile(device, "assets/shaders/swim_ripple.frag.spv"); + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + ripplePipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) + .setLayout(ripplePipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + } + + // ---- Rebuild bubble pipeline ---- + { + VkShaderModule vertModule; + vertModule.loadFromFile(device, "assets/shaders/swim_bubble.vert.spv"); + VkShaderModule fragModule; + fragModule.loadFromFile(device, "assets/shaders/swim_bubble.frag.spv"); + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + bubblePipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) + .setLayout(bubblePipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + } +} + void SwimEffects::spawnRipple(const glm::vec3& pos, const glm::vec3& moveDir, float waterH) { if (static_cast(ripples.size()) >= MAX_RIPPLE_PARTICLES) return; diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index bdb319cb..8d26cc42 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -138,6 +138,7 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL .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 }) @@ -159,6 +160,7 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL .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 }) @@ -188,6 +190,86 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL 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"); @@ -482,7 +564,39 @@ void TerrainRenderer::writeMaterialDescriptors(VkDescriptorSet set, const Terrai } void TerrainRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) { - if (chunks.empty() || !pipeline) return; + 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); @@ -507,6 +621,13 @@ void TerrainRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, c renderedChunks = 0; culledChunks = 0; + // Periodic culling summary (every ~5s at 60fps) + static int renderCallCount = 0; + if (++renderCallCount % 300 == 1) { + glm::vec3 cam = camera.getPosition(); + LOG_INFO("Terrain render call: total=", chunks.size(), " cam=(", cam.x, ",", cam.y, ",", cam.z, ")"); + } + for (const auto& chunk : chunks) { if (!chunk.isValid() || !chunk.materialSet) continue; @@ -533,6 +654,11 @@ void TerrainRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, c vkCmdDrawIndexed(cmd, chunk.indexCount, 1, 0, 0, 0); renderedChunks++; } + + // Log culling result periodically + if (renderCallCount % 300 == 1) { + LOG_INFO("Terrain culling: rendered=", renderedChunks, " culled=", culledChunks); + } } void TerrainRenderer::renderShadow(VkCommandBuffer /*cmd*/, const glm::vec3& /*shadowCenter*/, float /*halfExtent*/) { @@ -589,6 +715,13 @@ void TerrainRenderer::destroyChunkGPU(TerrainChunkGPU& chunk) { chunk.paramsUBO = VK_NULL_HANDLE; } 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 { diff --git a/src/rendering/vk_context.cpp b/src/rendering/vk_context.cpp index 2da3b41a..5d5a37a6 100644 --- a/src/rendering/vk_context.cpp +++ b/src/rendering/vk_context.cpp @@ -205,7 +205,7 @@ bool VkContext::createSwapchain(int width, int height) { vkb::SwapchainBuilder swapchainBuilder{physicalDevice, device, surface}; auto swapRet = swapchainBuilder - .set_desired_format({VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}) + .set_desired_format({VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}) .set_desired_present_mode(VK_PRESENT_MODE_FIFO_KHR) // VSync .set_desired_extent(static_cast(width), static_cast(height)) .set_desired_min_image_count(2) @@ -331,7 +331,7 @@ bool VkContext::createDepthBuffer() { imgInfo.extent = {swapchainExtent.width, swapchainExtent.height, 1}; imgInfo.mipLevels = 1; imgInfo.arrayLayers = 1; - imgInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imgInfo.samples = msaaSamples_; imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imgInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; @@ -365,87 +365,252 @@ void VkContext::destroyDepthBuffer() { if (depthImage) { vmaDestroyImage(allocator, depthImage, depthAllocation); depthImage = VK_NULL_HANDLE; depthAllocation = VK_NULL_HANDLE; } } +bool VkContext::createMsaaColorImage() { + if (msaaSamples_ == VK_SAMPLE_COUNT_1_BIT) return true; // No MSAA image needed + + VkImageCreateInfo imgInfo{}; + imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imgInfo.imageType = VK_IMAGE_TYPE_2D; + imgInfo.format = swapchainFormat; + imgInfo.extent = {swapchainExtent.width, swapchainExtent.height, 1}; + imgInfo.mipLevels = 1; + imgInfo.arrayLayers = 1; + imgInfo.samples = msaaSamples_; + imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imgInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT; + + VmaAllocationCreateInfo allocInfo{}; + allocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; + allocInfo.preferredFlags = VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT; + + if (vmaCreateImage(allocator, &imgInfo, &allocInfo, &msaaColorImage_, &msaaColorAllocation_, nullptr) != VK_SUCCESS) { + LOG_ERROR("Failed to create MSAA color image"); + return false; + } + + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = msaaColorImage_; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = swapchainFormat; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.layerCount = 1; + + if (vkCreateImageView(device, &viewInfo, nullptr, &msaaColorView_) != VK_SUCCESS) { + LOG_ERROR("Failed to create MSAA color image view"); + return false; + } + + return true; +} + +void VkContext::destroyMsaaColorImage() { + if (msaaColorView_) { vkDestroyImageView(device, msaaColorView_, nullptr); msaaColorView_ = VK_NULL_HANDLE; } + if (msaaColorImage_) { vmaDestroyImage(allocator, msaaColorImage_, msaaColorAllocation_); msaaColorImage_ = VK_NULL_HANDLE; msaaColorAllocation_ = VK_NULL_HANDLE; } +} + +VkSampleCountFlagBits VkContext::getMaxUsableSampleCount() const { + VkPhysicalDeviceProperties props; + vkGetPhysicalDeviceProperties(physicalDevice, &props); + VkSampleCountFlags counts = props.limits.framebufferColorSampleCounts + & props.limits.framebufferDepthSampleCounts; + if (counts & VK_SAMPLE_COUNT_8_BIT) return VK_SAMPLE_COUNT_8_BIT; + if (counts & VK_SAMPLE_COUNT_4_BIT) return VK_SAMPLE_COUNT_4_BIT; + if (counts & VK_SAMPLE_COUNT_2_BIT) return VK_SAMPLE_COUNT_2_BIT; + return VK_SAMPLE_COUNT_1_BIT; +} + +void VkContext::setMsaaSamples(VkSampleCountFlagBits samples) { + // Clamp to max supported + VkSampleCountFlagBits maxSamples = getMaxUsableSampleCount(); + if (samples > maxSamples) samples = maxSamples; + msaaSamples_ = samples; + swapchainDirty = true; +} + bool VkContext::createImGuiResources() { // Create depth buffer first if (!createDepthBuffer()) return false; - // Render pass with color + depth attachments (used by both scene and ImGui) - VkAttachmentDescription attachments[2] = {}; + // Create MSAA color image if needed + if (!createMsaaColorImage()) return false; - // Color attachment (swapchain image) - attachments[0].format = swapchainFormat; - attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; - attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; - attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + bool useMsaa = (msaaSamples_ > VK_SAMPLE_COUNT_1_BIT); - // Depth attachment - attachments[1].format = depthFormat; - attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; - attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + if (useMsaa) { + // MSAA render pass: 3 attachments (MSAA color, depth, resolve/swapchain) + VkAttachmentDescription attachments[3] = {}; - VkAttachmentReference colorRef{}; - colorRef.attachment = 0; - colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + // Attachment 0: MSAA color target + attachments[0].format = swapchainFormat; + attachments[0].samples = msaaSamples_; + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[0].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - VkAttachmentReference depthRef{}; - depthRef.attachment = 1; - depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + // Attachment 1: Depth (multisampled) + attachments[1].format = depthFormat; + attachments[1].samples = msaaSamples_; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - VkSubpassDescription subpass{}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; - subpass.pColorAttachments = &colorRef; - subpass.pDepthStencilAttachment = &depthRef; + // Attachment 2: Resolve target (swapchain image) + attachments[2].format = swapchainFormat; + attachments[2].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[2].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[2].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[2].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[2].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[2].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[2].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - VkSubpassDependency dependency{}; - dependency.srcSubpass = VK_SUBPASS_EXTERNAL; - dependency.dstSubpass = 0; - dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; - dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; - dependency.srcAccessMask = 0; - dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + VkAttachmentReference colorRef{}; + colorRef.attachment = 0; + colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - VkRenderPassCreateInfo rpInfo{}; - rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; - rpInfo.attachmentCount = 2; - rpInfo.pAttachments = attachments; - rpInfo.subpassCount = 1; - rpInfo.pSubpasses = &subpass; - rpInfo.dependencyCount = 1; - rpInfo.pDependencies = &dependency; + VkAttachmentReference depthRef{}; + depthRef.attachment = 1; + depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - if (vkCreateRenderPass(device, &rpInfo, nullptr, &imguiRenderPass) != VK_SUCCESS) { - LOG_ERROR("Failed to create render pass"); - return false; - } + VkAttachmentReference resolveRef{}; + resolveRef.attachment = 2; + resolveRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - // Create framebuffers (color + depth) - swapchainFramebuffers.resize(swapchainImageViews.size()); - for (size_t i = 0; i < swapchainImageViews.size(); i++) { - VkImageView fbAttachments[2] = {swapchainImageViews[i], depthImageView}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + subpass.pDepthStencilAttachment = &depthRef; + subpass.pResolveAttachments = &resolveRef; - VkFramebufferCreateInfo fbInfo{}; - fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fbInfo.renderPass = imguiRenderPass; - fbInfo.attachmentCount = 2; - fbInfo.pAttachments = fbAttachments; - fbInfo.width = swapchainExtent.width; - fbInfo.height = swapchainExtent.height; - fbInfo.layers = 1; + VkSubpassDependency dependency{}; + dependency.srcSubpass = VK_SUBPASS_EXTERNAL; + dependency.dstSubpass = 0; + dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dependency.srcAccessMask = 0; + dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; - if (vkCreateFramebuffer(device, &fbInfo, nullptr, &swapchainFramebuffers[i]) != VK_SUCCESS) { - LOG_ERROR("Failed to create swapchain framebuffer ", i); + VkRenderPassCreateInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rpInfo.attachmentCount = 3; + rpInfo.pAttachments = attachments; + rpInfo.subpassCount = 1; + rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dependency; + + if (vkCreateRenderPass(device, &rpInfo, nullptr, &imguiRenderPass) != VK_SUCCESS) { + LOG_ERROR("Failed to create MSAA render pass"); return false; } + + // Framebuffers: [msaaColorView, depthView, swapchainView] + swapchainFramebuffers.resize(swapchainImageViews.size()); + for (size_t i = 0; i < swapchainImageViews.size(); i++) { + VkImageView fbAttachments[3] = {msaaColorView_, depthImageView, swapchainImageViews[i]}; + + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = imguiRenderPass; + fbInfo.attachmentCount = 3; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = swapchainExtent.width; + fbInfo.height = swapchainExtent.height; + fbInfo.layers = 1; + + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &swapchainFramebuffers[i]) != VK_SUCCESS) { + LOG_ERROR("Failed to create MSAA swapchain framebuffer ", i); + return false; + } + } + } else { + // Non-MSAA render pass: 2 attachments (color + depth) — original path + VkAttachmentDescription attachments[2] = {}; + + // Color attachment (swapchain image) + attachments[0].format = swapchainFormat; + attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + // Depth attachment + attachments[1].format = depthFormat; + attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkAttachmentReference colorRef{}; + colorRef.attachment = 0; + colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkAttachmentReference depthRef{}; + depthRef.attachment = 1; + depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + subpass.pDepthStencilAttachment = &depthRef; + + VkSubpassDependency dependency{}; + dependency.srcSubpass = VK_SUBPASS_EXTERNAL; + dependency.dstSubpass = 0; + dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dependency.srcAccessMask = 0; + dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + + VkRenderPassCreateInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rpInfo.attachmentCount = 2; + rpInfo.pAttachments = attachments; + rpInfo.subpassCount = 1; + rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dependency; + + if (vkCreateRenderPass(device, &rpInfo, nullptr, &imguiRenderPass) != VK_SUCCESS) { + LOG_ERROR("Failed to create render pass"); + return false; + } + + // Framebuffers: [swapchainView, depthView] + swapchainFramebuffers.resize(swapchainImageViews.size()); + for (size_t i = 0; i < swapchainImageViews.size(); i++) { + VkImageView fbAttachments[2] = {swapchainImageViews[i], depthImageView}; + + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = imguiRenderPass; + fbInfo.attachmentCount = 2; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = swapchainExtent.width; + fbInfo.height = swapchainExtent.height; + fbInfo.layers = 1; + + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &swapchainFramebuffers[i]) != VK_SUCCESS) { + LOG_ERROR("Failed to create swapchain framebuffer ", i); + return false; + } + } } // Create descriptor pool for ImGui @@ -473,6 +638,7 @@ void VkContext::destroyImGuiResources() { vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr); imguiDescriptorPool = VK_NULL_HANDLE; } + destroyMsaaColorImage(); destroyDepthBuffer(); // Framebuffers are destroyed in destroySwapchain() if (imguiRenderPass) { @@ -500,7 +666,7 @@ bool VkContext::recreateSwapchain(int width, int height) { vkb::SwapchainBuilder swapchainBuilder{physicalDevice, device, surface}; auto swapRet = swapchainBuilder - .set_desired_format({VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}) + .set_desired_format({VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}) .set_desired_present_mode(VK_PRESENT_MODE_FIFO_KHR) .set_desired_extent(static_cast(width), static_cast(height)) .set_desired_min_image_count(2) @@ -524,28 +690,168 @@ bool VkContext::recreateSwapchain(int width, int height) { swapchainImages = vkbSwap.get_images().value(); swapchainImageViews = vkbSwap.get_image_views().value(); - // Recreate depth buffer + // Recreate depth buffer + MSAA color image + destroyMsaaColorImage(); destroyDepthBuffer(); + + // Destroy old render pass (needs recreation if MSAA changed) + if (imguiRenderPass) { + vkDestroyRenderPass(device, imguiRenderPass, nullptr); + imguiRenderPass = VK_NULL_HANDLE; + } + if (!createDepthBuffer()) return false; + if (!createMsaaColorImage()) return false; - // Recreate framebuffers (color + depth) - swapchainFramebuffers.resize(swapchainImageViews.size()); - for (size_t i = 0; i < swapchainImageViews.size(); i++) { - VkImageView fbAttachments[2] = {swapchainImageViews[i], depthImageView}; + bool useMsaa = (msaaSamples_ > VK_SAMPLE_COUNT_1_BIT); - VkFramebufferCreateInfo fbInfo{}; - fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fbInfo.renderPass = imguiRenderPass; - fbInfo.attachmentCount = 2; - fbInfo.pAttachments = fbAttachments; - fbInfo.width = swapchainExtent.width; - fbInfo.height = swapchainExtent.height; - fbInfo.layers = 1; + if (useMsaa) { + // MSAA render pass: 3 attachments + VkAttachmentDescription attachments[3] = {}; + attachments[0].format = swapchainFormat; + attachments[0].samples = msaaSamples_; + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[0].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - if (vkCreateFramebuffer(device, &fbInfo, nullptr, &swapchainFramebuffers[i]) != VK_SUCCESS) { - LOG_ERROR("Failed to recreate swapchain framebuffer ", i); + attachments[1].format = depthFormat; + attachments[1].samples = msaaSamples_; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + attachments[2].format = swapchainFormat; + attachments[2].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[2].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[2].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[2].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[2].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[2].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[2].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkAttachmentReference depthRef{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; + VkAttachmentReference resolveRef{2, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + subpass.pDepthStencilAttachment = &depthRef; + subpass.pResolveAttachments = &resolveRef; + + VkSubpassDependency dependency{}; + dependency.srcSubpass = VK_SUBPASS_EXTERNAL; + dependency.dstSubpass = 0; + dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dependency.srcAccessMask = 0; + dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + + VkRenderPassCreateInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rpInfo.attachmentCount = 3; + rpInfo.pAttachments = attachments; + rpInfo.subpassCount = 1; + rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dependency; + + if (vkCreateRenderPass(device, &rpInfo, nullptr, &imguiRenderPass) != VK_SUCCESS) { + LOG_ERROR("Failed to recreate MSAA render pass"); return false; } + + swapchainFramebuffers.resize(swapchainImageViews.size()); + for (size_t i = 0; i < swapchainImageViews.size(); i++) { + VkImageView fbAttachments[3] = {msaaColorView_, depthImageView, swapchainImageViews[i]}; + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = imguiRenderPass; + fbInfo.attachmentCount = 3; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = swapchainExtent.width; + fbInfo.height = swapchainExtent.height; + fbInfo.layers = 1; + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &swapchainFramebuffers[i]) != VK_SUCCESS) { + LOG_ERROR("Failed to recreate MSAA swapchain framebuffer ", i); + return false; + } + } + } else { + // Non-MSAA render pass: 2 attachments + VkAttachmentDescription attachments[2] = {}; + attachments[0].format = swapchainFormat; + attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + attachments[1].format = depthFormat; + attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkAttachmentReference depthRef{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; + + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + subpass.pDepthStencilAttachment = &depthRef; + + VkSubpassDependency dependency{}; + dependency.srcSubpass = VK_SUBPASS_EXTERNAL; + dependency.dstSubpass = 0; + dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dependency.srcAccessMask = 0; + dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + + VkRenderPassCreateInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rpInfo.attachmentCount = 2; + rpInfo.pAttachments = attachments; + rpInfo.subpassCount = 1; + rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dependency; + + if (vkCreateRenderPass(device, &rpInfo, nullptr, &imguiRenderPass) != VK_SUCCESS) { + LOG_ERROR("Failed to recreate render pass"); + return false; + } + + swapchainFramebuffers.resize(swapchainImageViews.size()); + for (size_t i = 0; i < swapchainImageViews.size(); i++) { + VkImageView fbAttachments[2] = {swapchainImageViews[i], depthImageView}; + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = imguiRenderPass; + fbInfo.attachmentCount = 2; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = swapchainExtent.width; + fbInfo.height = swapchainExtent.height; + fbInfo.layers = 1; + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &swapchainFramebuffers[i]) != VK_SUCCESS) { + LOG_ERROR("Failed to recreate swapchain framebuffer ", i); + return false; + } + } } swapchainDirty = false; diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index f3dbd4c8..365bd02f 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -125,6 +125,7 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // depth test yes, write no .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) @@ -142,6 +143,60 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay return true; } +void WaterRenderer::recreatePipelines() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + + // Destroy old pipeline (keep layout) + if (waterPipeline) { vkDestroyPipeline(device, waterPipeline, nullptr); waterPipeline = VK_NULL_HANDLE; } + + // Load shaders + VkShaderModule vertShader, fragShader; + if (!vertShader.loadFromFile(device, "assets/shaders/water.vert.spv")) { + LOG_ERROR("WaterRenderer::recreatePipelines: failed to load vertex shader"); + return; + } + if (!fragShader.loadFromFile(device, "assets/shaders/water.frag.spv")) { + LOG_ERROR("WaterRenderer::recreatePipelines: failed to load fragment shader"); + vertShader.destroy(); + return; + } + + // Vertex input (same as initialize) + VkVertexInputBindingDescription vertBinding{}; + vertBinding.binding = 0; + vertBinding.stride = 8 * sizeof(float); + vertBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector vertAttribs = { + { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }, + { 1, 0, VK_FORMAT_R32G32_SFLOAT, 6 * sizeof(float) }, + }; + + VkRenderPass mainPass = vkCtx->getImGuiRenderPass(); + + waterPipeline = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ vertBinding }, vertAttribs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) + .setLayout(pipelineLayout) + .setRenderPass(mainPass) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); + + vertShader.destroy(); + fragShader.destroy(); + + if (!waterPipeline) { + LOG_ERROR("WaterRenderer::recreatePipelines: failed to create pipeline"); + } +} + void WaterRenderer::shutdown() { clear(); diff --git a/src/rendering/weather.cpp b/src/rendering/weather.cpp index ae59fde7..20af6ab7 100644 --- a/src/rendering/weather.cpp +++ b/src/rendering/weather.cpp @@ -81,6 +81,7 @@ bool Weather::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS) // depth test on, write off (transparent particles) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) @@ -115,6 +116,65 @@ bool Weather::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { return true; } +void Weather::recreatePipelines() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + + if (pipeline != VK_NULL_HANDLE) { vkDestroyPipeline(device, pipeline, nullptr); pipeline = VK_NULL_HANDLE; } + + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/weather.vert.spv")) { + LOG_ERROR("Weather::recreatePipelines: failed to load vertex shader"); + return; + } + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/weather.frag.spv")) { + LOG_ERROR("Weather::recreatePipelines: failed to load fragment shader"); + vertModule.destroy(); + return; + } + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + // Vertex input (same as initialize) + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 3 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32B32_SFLOAT; + posAttr.offset = 0; + + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + pipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx->getMsaaSamples()) + .setLayout(pipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (pipeline == VK_NULL_HANDLE) { + LOG_ERROR("Weather::recreatePipelines: failed to create pipeline"); + } +} + void Weather::update(const Camera& camera, float deltaTime) { if (!enabled || weatherType == Type::NONE) { return; diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index f7263095..3e9f0f65 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -154,6 +154,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .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 }) @@ -175,6 +176,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) @@ -193,6 +195,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .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 }) @@ -2878,5 +2881,96 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3 // Occlusion queries stubbed out in Vulkan (were disabled by default anyway) +void WMORenderer::recreatePipelines() { + if (!vkCtx_) return; + VkDevice device = vkCtx_->getDevice(); + vkDeviceWaitIdle(device); + + // Destroy old main-pass pipelines (NOT shadow, NOT pipeline layout) + if (opaquePipeline_) { vkDestroyPipeline(device, opaquePipeline_, nullptr); opaquePipeline_ = VK_NULL_HANDLE; } + if (transparentPipeline_) { vkDestroyPipeline(device, transparentPipeline_, nullptr); transparentPipeline_ = VK_NULL_HANDLE; } + if (wireframePipeline_) { vkDestroyPipeline(device, wireframePipeline_, nullptr); wireframePipeline_ = VK_NULL_HANDLE; } + + // --- Load shaders --- + VkShaderModule vertShader, fragShader; + if (!vertShader.loadFromFile(device, "assets/shaders/wmo.vert.spv") || + !fragShader.loadFromFile(device, "assets/shaders/wmo.frag.spv")) { + core::Logger::getInstance().error("WMORenderer::recreatePipelines: failed to load shaders"); + return; + } + + // --- Vertex input --- + struct WMOVertexData { + glm::vec3 position; + glm::vec3 normal; + glm::vec2 texCoord; + glm::vec4 color; + }; + + VkVertexInputBindingDescription vertexBinding{}; + vertexBinding.binding = 0; + vertexBinding.stride = sizeof(WMOVertexData); + vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector vertexAttribs(4); + vertexAttribs[0] = { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, + static_cast(offsetof(WMOVertexData, position)) }; + vertexAttribs[1] = { 1, 0, VK_FORMAT_R32G32B32_SFLOAT, + static_cast(offsetof(WMOVertexData, normal)) }; + vertexAttribs[2] = { 2, 0, VK_FORMAT_R32G32_SFLOAT, + static_cast(offsetof(WMOVertexData, texCoord)) }; + vertexAttribs[3] = { 3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, + static_cast(offsetof(WMOVertexData, color)) }; + + VkRenderPass mainPass = vkCtx_->getImGuiRenderPass(); + + opaquePipeline_ = 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); + + transparentPipeline_ = 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, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(pipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); + + 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); + + vertShader.destroy(); + fragShader.destroy(); + + core::Logger::getInstance().info("WMORenderer: pipelines recreated"); +} + } // namespace rendering } // namespace wowee diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index 9be570f3..f76b21ba 100644 --- a/src/ui/auth_screen.cpp +++ b/src/ui/auth_screen.cpp @@ -3,10 +3,13 @@ #include "core/application.hpp" #include "core/logger.hpp" #include "rendering/renderer.hpp" +#include "rendering/vk_context.hpp" #include "pipeline/asset_manager.hpp" #include "audio/music_manager.hpp" #include "game/expansion_profile.hpp" #include +#include +#include "stb_image.h" #include #include #include @@ -159,39 +162,34 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { loginInfoLoaded = true; } - if (!videoInitAttempted) { - videoInitAttempted = true; - std::string videoPath = "assets/startscreen.mp4"; - if (!std::filesystem::exists(videoPath)) { - videoPath = (std::filesystem::current_path() / "assets/startscreen.mp4").string(); - } - backgroundVideo.open(videoPath); + if (!bgInitAttempted) { + bgInitAttempted = true; + loadBackgroundImage(); } - backgroundVideo.update(ImGui::GetIO().DeltaTime); - if (backgroundVideo.isReady()) { + if (bgDescriptorSet) { ImVec2 screen = ImGui::GetIO().DisplaySize; float screenW = screen.x; float screenH = screen.y; - float videoW = static_cast(backgroundVideo.getWidth()); - float videoH = static_cast(backgroundVideo.getHeight()); - if (videoW > 0.0f && videoH > 0.0f) { + float imgW = static_cast(bgWidth); + float imgH = static_cast(bgHeight); + if (imgW > 0.0f && imgH > 0.0f) { float screenAspect = screenW / screenH; - float videoAspect = videoW / videoH; + float imgAspect = imgW / imgH; ImVec2 uv0(0.0f, 0.0f); ImVec2 uv1(1.0f, 1.0f); - if (videoAspect > screenAspect) { - float scale = screenAspect / videoAspect; + if (imgAspect > screenAspect) { + float scale = screenAspect / imgAspect; float crop = (1.0f - scale) * 0.5f; uv0.x = crop; uv1.x = 1.0f - crop; - } else if (videoAspect < screenAspect) { - float scale = videoAspect / screenAspect; + } else if (imgAspect < screenAspect) { + float scale = imgAspect / screenAspect; float crop = (1.0f - scale) * 0.5f; uv0.y = crop; uv1.y = 1.0f - crop; } ImDrawList* bg = ImGui::GetBackgroundDrawList(); - bg->AddImage(static_cast(static_cast(backgroundVideo.getTextureId())), + bg->AddImage(reinterpret_cast(bgDescriptorSet), ImVec2(0, 0), ImVec2(screenW, screenH), uv0, uv1); } } @@ -763,4 +761,164 @@ void AuthScreen::loadLoginInfo() { LOG_INFO("Login info loaded from ", path); } +static uint32_t findMemType(VkPhysicalDevice pd, uint32_t filter, VkMemoryPropertyFlags props) { + VkPhysicalDeviceMemoryProperties mp; + vkGetPhysicalDeviceMemoryProperties(pd, &mp); + for (uint32_t i = 0; i < mp.memoryTypeCount; i++) { + if ((filter & (1 << i)) && (mp.memoryTypes[i].propertyFlags & props) == props) return i; + } + return 0; +} + +bool AuthScreen::loadBackgroundImage() { + auto& app = core::Application::getInstance(); + auto* renderer = app.getRenderer(); + if (!renderer) return false; + bgVkCtx = renderer->getVkContext(); + if (!bgVkCtx) return false; + + std::string imgPath = "assets/krayonsignin.png"; + if (!std::filesystem::exists(imgPath)) + imgPath = (std::filesystem::current_path() / imgPath).string(); + + int channels; + stbi_set_flip_vertically_on_load(false); + unsigned char* data = stbi_load(imgPath.c_str(), &bgWidth, &bgHeight, &channels, 4); + if (!data) { + LOG_WARNING("Auth screen: failed to load background image: ", imgPath); + return false; + } + + VkDevice device = bgVkCtx->getDevice(); + VkPhysicalDevice physDevice = bgVkCtx->getPhysicalDevice(); + VkDeviceSize imageSize = static_cast(bgWidth) * bgHeight * 4; + + // Staging buffer + VkBuffer stagingBuffer; + VkDeviceMemory stagingMemory; + { + VkBufferCreateInfo bufInfo{}; + bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufInfo.size = imageSize; + bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + vkCreateBuffer(device, &bufInfo, nullptr, &stagingBuffer); + + VkMemoryRequirements memReqs; + vkGetBufferMemoryRequirements(device, stagingBuffer, &memReqs); + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memReqs.size; + allocInfo.memoryTypeIndex = findMemType(physDevice, memReqs.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory); + vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0); + + void* mapped; + vkMapMemory(device, stagingMemory, 0, imageSize, 0, &mapped); + memcpy(mapped, data, imageSize); + vkUnmapMemory(device, stagingMemory); + } + stbi_image_free(data); + + // Create VkImage + { + VkImageCreateInfo imgInfo{}; + imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imgInfo.imageType = VK_IMAGE_TYPE_2D; + imgInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + imgInfo.extent = {static_cast(bgWidth), static_cast(bgHeight), 1}; + imgInfo.mipLevels = 1; + imgInfo.arrayLayers = 1; + imgInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imgInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + vkCreateImage(device, &imgInfo, nullptr, &bgImage); + + VkMemoryRequirements memReqs; + vkGetImageMemoryRequirements(device, bgImage, &memReqs); + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memReqs.size; + allocInfo.memoryTypeIndex = findMemType(physDevice, memReqs.memoryTypeBits, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + vkAllocateMemory(device, &allocInfo, nullptr, &bgMemory); + vkBindImageMemory(device, bgImage, bgMemory, 0); + } + + // Transfer + bgVkCtx->immediateSubmit([&](VkCommandBuffer cmd) { + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = bgImage; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {static_cast(bgWidth), static_cast(bgHeight), 1}; + vkCmdCopyBufferToImage(cmd, stagingBuffer, bgImage, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + }); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingMemory, nullptr); + + // Image view + { + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = bgImage; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCreateImageView(device, &viewInfo, nullptr, &bgImageView); + } + + // Sampler + { + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.magFilter = VK_FILTER_LINEAR; + samplerInfo.minFilter = VK_FILTER_LINEAR; + 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, &bgSampler); + } + + bgDescriptorSet = ImGui_ImplVulkan_AddTexture(bgSampler, bgImageView, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + LOG_INFO("Auth screen background loaded: ", bgWidth, "x", bgHeight); + return true; +} + +void AuthScreen::destroyBackgroundImage() { + if (!bgVkCtx) return; + VkDevice device = bgVkCtx->getDevice(); + vkDeviceWaitIdle(device); + if (bgDescriptorSet) { ImGui_ImplVulkan_RemoveTexture(bgDescriptorSet); bgDescriptorSet = VK_NULL_HANDLE; } + if (bgSampler) { vkDestroySampler(device, bgSampler, nullptr); bgSampler = VK_NULL_HANDLE; } + if (bgImageView) { vkDestroyImageView(device, bgImageView, nullptr); bgImageView = VK_NULL_HANDLE; } + if (bgImage) { vkDestroyImage(device, bgImage, nullptr); bgImage = VK_NULL_HANDLE; } + if (bgMemory) { vkFreeMemory(device, bgMemory, nullptr); bgMemory = VK_NULL_HANDLE; } +} + }} // namespace wowee::ui diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 0c816556..c9fbd59a 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -261,6 +261,21 @@ void GameScreen::render(game::GameHandler& gameHandler) { } } + // Apply saved MSAA setting once when renderer is available + if (!msaaSettingsApplied_ && pendingAntiAliasing > 0) { + auto* renderer = core::Application::getInstance().getRenderer(); + if (renderer) { + static const VkSampleCountFlagBits aaSamples[] = { + VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT, + VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT + }; + renderer->setMsaaSamples(aaSamples[pendingAntiAliasing]); + msaaSettingsApplied_ = true; + } + } else { + msaaSettingsApplied_ = true; + } + // Apply auto-loot setting to GameHandler every frame (cheap bool sync) gameHandler.setAutoLoot(pendingAutoLoot); @@ -5866,6 +5881,17 @@ void GameScreen::renderSettingsWindow() { if (renderer) renderer->setShadowsEnabled(pendingShadows); saveSettings(); } + { + const char* aaLabels[] = { "Off", "2x MSAA", "4x MSAA", "8x MSAA" }; + if (ImGui::Combo("Anti-Aliasing", &pendingAntiAliasing, aaLabels, 4)) { + static const VkSampleCountFlagBits aaSamples[] = { + VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT, + VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT + }; + if (renderer) renderer->setMsaaSamples(aaSamples[pendingAntiAliasing]); + saveSettings(); + } + } if (ImGui::SliderInt("Ground Clutter Density", &pendingGroundClutterDensity, 0, 150, "%d%%")) { if (renderer) { if (auto* tm = renderer->getTerrainManager()) { @@ -5896,11 +5922,13 @@ void GameScreen::renderSettingsWindow() { pendingVsync = kDefaultVsync; pendingShadows = kDefaultShadows; pendingGroundClutterDensity = kDefaultGroundClutterDensity; + pendingAntiAliasing = 0; pendingResIndex = defaultResIndex; window->setFullscreen(pendingFullscreen); window->setVsync(pendingVsync); window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]); if (renderer) renderer->setShadowsEnabled(pendingShadows); + if (renderer) renderer->setMsaaSamples(VK_SAMPLE_COUNT_1_BIT); if (renderer) { if (auto* tm = renderer->getTerrainManager()) { tm->setGroundClutterDensityScale(static_cast(pendingGroundClutterDensity) / 100.0f); @@ -6831,6 +6859,7 @@ void GameScreen::saveSettings() { // Gameplay out << "auto_loot=" << (pendingAutoLoot ? 1 : 0) << "\n"; out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n"; + out << "antialiasing=" << pendingAntiAliasing << "\n"; // Controls out << "mouse_sensitivity=" << pendingMouseSensitivity << "\n"; @@ -6908,6 +6937,7 @@ void GameScreen::loadSettings() { // Gameplay else if (key == "auto_loot") pendingAutoLoot = (std::stoi(val) != 0); else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150); + else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3); // Controls else if (key == "mouse_sensitivity") pendingMouseSensitivity = std::clamp(std::stof(val), 0.05f, 1.0f); else if (key == "invert_mouse") pendingInvertMouse = (std::stoi(val) != 0); diff --git a/src/ui/ui_manager.cpp b/src/ui/ui_manager.cpp index 91ef2bcc..305bb7de 100644 --- a/src/ui/ui_manager.cpp +++ b/src/ui/ui_manager.cpp @@ -77,7 +77,7 @@ bool UIManager::initialize(core::Window* win) { initInfo.MinImageCount = 2; initInfo.ImageCount = vkCtx->getSwapchainImageCount(); initInfo.PipelineInfoMain.RenderPass = vkCtx->getImGuiRenderPass(); - initInfo.PipelineInfoMain.MSAASamples = VK_SAMPLE_COUNT_1_BIT; + initInfo.PipelineInfoMain.MSAASamples = vkCtx->getMsaaSamples(); ImGui_ImplVulkan_Init(&initInfo);