diff --git a/CMakeLists.txt b/CMakeLists.txt index 8eba3f1..cd046df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,7 @@ set(TETRIS_SOURCES src/app/BackgroundManager.cpp src/app/Fireworks.cpp src/app/AssetLoader.cpp + src/app/TextureLoader.cpp src/states/LoadingManager.cpp # State implementations (new) src/states/LoadingState.cpp diff --git a/src/app/TextureLoader.cpp b/src/app/TextureLoader.cpp new file mode 100644 index 0000000..ef9af8e --- /dev/null +++ b/src/app/TextureLoader.cpp @@ -0,0 +1,91 @@ +#include "app/TextureLoader.h" + +#include + +#include +#include +#include + +#include "utils/ImagePathResolver.h" + +TextureLoader::TextureLoader( + std::atomic& loadedTasks, + std::string& currentLoadingFile, + std::mutex& currentLoadingMutex, + std::vector& assetLoadErrors, + std::mutex& assetLoadErrorsMutex) + : loadedTasks_(loadedTasks) + , currentLoadingFile_(currentLoadingFile) + , currentLoadingMutex_(currentLoadingMutex) + , assetLoadErrors_(assetLoadErrors) + , assetLoadErrorsMutex_(assetLoadErrorsMutex) +{ +} + +void TextureLoader::setCurrentLoadingFile(const std::string& filename) { + std::lock_guard lk(currentLoadingMutex_); + currentLoadingFile_ = filename; +} + +void TextureLoader::clearCurrentLoadingFile() { + std::lock_guard lk(currentLoadingMutex_); + currentLoadingFile_.clear(); +} + +void TextureLoader::recordAssetLoadError(const std::string& message) { + std::lock_guard lk(assetLoadErrorsMutex_); + assetLoadErrors_.emplace_back(message); +} + +SDL_Texture* TextureLoader::loadFromImage(SDL_Renderer* renderer, const std::string& path, int* outW, int* outH) { + if (!renderer) { + return nullptr; + } + + const std::string resolvedPath = AssetPath::resolveImagePath(path); + setCurrentLoadingFile(resolvedPath.empty() ? path : resolvedPath); + + SDL_Surface* surface = IMG_Load(resolvedPath.c_str()); + if (!surface) { + { + std::ostringstream ss; + ss << "Image load failed: " << path << " (" << resolvedPath << "): " << SDL_GetError(); + recordAssetLoadError(ss.str()); + } + loadedTasks_.fetch_add(1); + clearCurrentLoadingFile(); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load image %s (resolved: %s): %s", path.c_str(), resolvedPath.c_str(), SDL_GetError()); + return nullptr; + } + + if (outW) { + *outW = surface->w; + } + if (outH) { + *outH = surface->h; + } + + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + SDL_DestroySurface(surface); + + if (!texture) { + { + std::ostringstream ss; + ss << "Texture create failed: " << resolvedPath << ": " << SDL_GetError(); + recordAssetLoadError(ss.str()); + } + loadedTasks_.fetch_add(1); + clearCurrentLoadingFile(); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create texture from %s: %s", resolvedPath.c_str(), SDL_GetError()); + return nullptr; + } + + loadedTasks_.fetch_add(1); + clearCurrentLoadingFile(); + + if (resolvedPath != path) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded %s via %s", path.c_str(), resolvedPath.c_str()); + } + + return texture; +} diff --git a/src/app/TextureLoader.h b/src/app/TextureLoader.h new file mode 100644 index 0000000..d807fe7 --- /dev/null +++ b/src/app/TextureLoader.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include +#include +#include +#include + +class TextureLoader { +public: + TextureLoader( + std::atomic& loadedTasks, + std::string& currentLoadingFile, + std::mutex& currentLoadingMutex, + std::vector& assetLoadErrors, + std::mutex& assetLoadErrorsMutex); + + SDL_Texture* loadFromImage(SDL_Renderer* renderer, const std::string& path, int* outW = nullptr, int* outH = nullptr); + +private: + std::atomic& loadedTasks_; + std::string& currentLoadingFile_; + std::mutex& currentLoadingMutex_; + std::vector& assetLoadErrors_; + std::mutex& assetLoadErrorsMutex_; + + void setCurrentLoadingFile(const std::string& filename); + void clearCurrentLoadingFile(); + void recordAssetLoadError(const std::string& message); +}; diff --git a/src/main.cpp b/src/main.cpp index b273636..4651f3d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -39,6 +38,7 @@ #include "states/PlayingState.h" #include "audio/MenuWrappers.h" #include "app/AssetLoader.h" +#include "app/TextureLoader.h" #include "states/LoadingManager.h" #include "utils/ImagePathResolver.h" #include "graphics/renderers/GameRenderer.h" @@ -47,8 +47,6 @@ #include "ui/MenuLayout.h" #include "ui/BottomMenu.h" -// Debug logging removed: no-op in this build (previously LOG_DEBUG) - // Font rendering now handled by FontAtlas // ---------- Game config ---------- @@ -99,289 +97,6 @@ static void drawRect(SDL_Renderer *r, float x, float y, float w, float h, SDL_Co SDL_RenderFillRect(r, &fr); } -static SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer, const std::string& path, int* outW = nullptr, int* outH = nullptr) { - if (!renderer) { - return nullptr; - } - - const std::string resolvedPath = AssetPath::resolveImagePath(path); - { - std::lock_guard lk(g_currentLoadingMutex); - g_currentLoadingFile = resolvedPath.empty() ? path : resolvedPath; - } - SDL_Surface* surface = IMG_Load(resolvedPath.c_str()); - if (!surface) { - // Record the error for display on the loading screen - { - std::lock_guard lk(g_assetLoadErrorsMutex); - std::ostringstream ss; - ss << "Image load failed: " << path << " (" << resolvedPath << "): " << SDL_GetError(); - g_assetLoadErrors.emplace_back(ss.str()); - } - g_loadedTasks.fetch_add(1); - { - std::lock_guard lk(g_currentLoadingMutex); - g_currentLoadingFile.clear(); - } - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load image %s (resolved: %s): %s", path.c_str(), resolvedPath.c_str(), SDL_GetError()); - return nullptr; - } - - if (outW) { *outW = surface->w; } - if (outH) { *outH = surface->h; } - - SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); - SDL_DestroySurface(surface); - - if (!texture) { - { - std::lock_guard lk(g_assetLoadErrorsMutex); - std::ostringstream ss; - ss << "Texture create failed: " << resolvedPath << ": " << SDL_GetError(); - g_assetLoadErrors.emplace_back(ss.str()); - } - g_loadedTasks.fetch_add(1); - { - std::lock_guard lk(g_currentLoadingMutex); - g_currentLoadingFile.clear(); - } - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create texture from %s: %s", resolvedPath.c_str(), SDL_GetError()); - return nullptr; - } - // Mark this task as completed - g_loadedTasks.fetch_add(1); - { - std::lock_guard lk(g_currentLoadingMutex); - g_currentLoadingFile.clear(); - } - if (resolvedPath != path) { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded %s via %s", path.c_str(), resolvedPath.c_str()); - } - - return texture; -} - -enum class LevelBackgroundPhase { Idle, ZoomOut, ZoomIn }; - -struct LevelBackgroundFader { - SDL_Texture* currentTex = nullptr; - SDL_Texture* nextTex = nullptr; - int currentLevel = -1; - int queuedLevel = -1; - float phaseElapsedMs = 0.0f; - float phaseDurationMs = 0.0f; - float fadeDurationMs = Config::Gameplay::LEVEL_FADE_DURATION; - LevelBackgroundPhase phase = LevelBackgroundPhase::Idle; -}; - -static float getPhaseDurationMs(const LevelBackgroundFader& fader, LevelBackgroundPhase phase) { - const float total = std::max(1200.0f, fader.fadeDurationMs); - switch (phase) { - case LevelBackgroundPhase::ZoomOut: return total * 0.45f; - case LevelBackgroundPhase::ZoomIn: return total * 0.45f; - case LevelBackgroundPhase::Idle: - default: return 0.0f; - } -} - -static void setPhase(LevelBackgroundFader& fader, LevelBackgroundPhase nextPhase) { - fader.phase = nextPhase; - fader.phaseDurationMs = getPhaseDurationMs(fader, nextPhase); - fader.phaseElapsedMs = 0.0f; -} - -static void destroyTexture(SDL_Texture*& tex) { - if (tex) { - SDL_DestroyTexture(tex); - tex = nullptr; - } -} - -static bool queueLevelBackground(LevelBackgroundFader& fader, SDL_Renderer* renderer, int level) { - if (!renderer) { - return false; - } - - level = std::clamp(level, 0, 32); - if (fader.currentLevel == level || fader.queuedLevel == level) { - return true; - } - - char bgPath[256]; - std::snprintf(bgPath, sizeof(bgPath), "assets/images/levels/level%d.jpg", level); - - SDL_Texture* newTexture = loadTextureFromImage(renderer, bgPath); - if (!newTexture) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to queue background for level %d: %s", level, bgPath); - return false; - } - - destroyTexture(fader.nextTex); - fader.nextTex = newTexture; - fader.queuedLevel = level; - - if (!fader.currentTex) { - // First background load happens instantly. - fader.currentTex = fader.nextTex; - fader.currentLevel = fader.queuedLevel; - fader.nextTex = nullptr; - fader.queuedLevel = -1; - fader.phase = LevelBackgroundPhase::Idle; - fader.phaseElapsedMs = 0.0f; - fader.phaseDurationMs = 0.0f; - } else if (fader.phase == LevelBackgroundPhase::Idle) { - // Kick off fancy transition. - setPhase(fader, LevelBackgroundPhase::ZoomOut); - } - - return true; -} - -static void updateLevelBackgroundFade(LevelBackgroundFader& fader, float frameMs) { - if (fader.phase == LevelBackgroundPhase::Idle) { - return; - } - - // Guard against missing textures - if (!fader.currentTex && !fader.nextTex) { - fader.phase = LevelBackgroundPhase::Idle; - return; - } - - fader.phaseElapsedMs += frameMs; - if (fader.phaseElapsedMs < std::max(1.0f, fader.phaseDurationMs)) { - return; - } - - switch (fader.phase) { - case LevelBackgroundPhase::ZoomOut: - // After zoom-out, swap textures then start zoom-in. - if (fader.nextTex) { - destroyTexture(fader.currentTex); - fader.currentTex = fader.nextTex; - fader.currentLevel = fader.queuedLevel; - fader.nextTex = nullptr; - fader.queuedLevel = -1; - } - setPhase(fader, LevelBackgroundPhase::ZoomIn); - break; - case LevelBackgroundPhase::ZoomIn: - fader.phase = LevelBackgroundPhase::Idle; - fader.phaseElapsedMs = 0.0f; - fader.phaseDurationMs = 0.0f; - break; - case LevelBackgroundPhase::Idle: - default: - fader.phase = LevelBackgroundPhase::Idle; - break; - } -} - -static void renderScaledBackground(SDL_Renderer* renderer, SDL_Texture* tex, int winW, int winH, float scale, Uint8 alpha = 255) { - if (!renderer || !tex) { - return; - } - - scale = std::max(0.5f, scale); - SDL_FRect dest{ - (winW - winW * scale) * 0.5f, - (winH - winH * scale) * 0.5f, - winW * scale, - winH * scale - }; - - SDL_SetTextureAlphaMod(tex, alpha); - SDL_RenderTexture(renderer, tex, nullptr, &dest); - SDL_SetTextureAlphaMod(tex, 255); -} - -static void renderDynamicBackground(SDL_Renderer* renderer, SDL_Texture* tex, int winW, int winH, float baseScale, float motionClockMs, float alphaMul = 1.0f) { - if (!renderer || !tex) { - return; - } - - const float seconds = motionClockMs * 0.001f; - const float wobble = std::max(0.4f, baseScale + std::sin(seconds * 0.07f) * 0.02f + std::sin(seconds * 0.23f) * 0.01f); - const float rotation = std::sin(seconds * 0.035f) * 1.25f; - const float panX = std::sin(seconds * 0.11f) * winW * 0.02f; - const float panY = std::cos(seconds * 0.09f) * winH * 0.015f; - - SDL_FRect dest{ - (winW - winW * wobble) * 0.5f + panX, - (winH - winH * wobble) * 0.5f + panY, - winW * wobble, - winH * wobble - }; - SDL_FPoint center{dest.w * 0.5f, dest.h * 0.5f}; - - Uint8 alpha = static_cast(std::clamp(alphaMul, 0.0f, 1.0f) * 255.0f); - SDL_SetTextureAlphaMod(tex, alpha); - SDL_RenderTextureRotated(renderer, tex, nullptr, &dest, rotation, ¢er, SDL_FLIP_NONE); - SDL_SetTextureAlphaMod(tex, 255); -} - -static void drawOverlay(SDL_Renderer* renderer, const SDL_FRect& rect, SDL_Color color, Uint8 alpha) { - if (!renderer || alpha == 0) { - return; - } - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); - SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, alpha); - SDL_RenderFillRect(renderer, &rect); - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); -} - -static void renderLevelBackgrounds(const LevelBackgroundFader& fader, SDL_Renderer* renderer, int winW, int winH, float motionClockMs) { - if (!renderer) { - return; - } - - SDL_FRect fullRect{0.f, 0.f, static_cast(winW), static_cast(winH)}; - const float duration = std::max(1.0f, fader.phaseDurationMs); - const float progress = (fader.phase == LevelBackgroundPhase::Idle) ? 0.0f : std::clamp(fader.phaseElapsedMs / duration, 0.0f, 1.0f); - const float seconds = motionClockMs * 0.001f; - - switch (fader.phase) { - case LevelBackgroundPhase::ZoomOut: { - const float scale = 1.0f + progress * 0.15f; - if (fader.currentTex) { - renderDynamicBackground(renderer, fader.currentTex, winW, winH, scale, motionClockMs, (1.0f - progress * 0.4f)); - drawOverlay(renderer, fullRect, SDL_Color{0, 0, 0, 255}, Uint8(progress * 200.0f)); - } - break; - } - case LevelBackgroundPhase::ZoomIn: { - const float scale = 1.10f - progress * 0.10f; - const Uint8 alpha = Uint8((0.4f + progress * 0.6f) * 255.0f); - if (fader.currentTex) { - renderDynamicBackground(renderer, fader.currentTex, winW, winH, scale, motionClockMs, alpha / 255.0f); - } - break; - } - case LevelBackgroundPhase::Idle: - default: - if (fader.currentTex) { - renderDynamicBackground(renderer, fader.currentTex, winW, winH, 1.02f, motionClockMs, 1.0f); - float pulse = 0.35f + 0.25f * (0.5f + 0.5f * std::sin(seconds * 0.5f)); - drawOverlay(renderer, fullRect, SDL_Color{5, 12, 28, 255}, Uint8(pulse * 90.0f)); - } else if (fader.nextTex) { - renderDynamicBackground(renderer, fader.nextTex, winW, winH, 1.02f, motionClockMs, 1.0f); - } else { - drawOverlay(renderer, fullRect, SDL_Color{0, 0, 0, 255}, 255); - } - break; - } -} - -static void resetLevelBackgrounds(LevelBackgroundFader& fader) { - destroyTexture(fader.currentTex); - destroyTexture(fader.nextTex); - fader.currentLevel = -1; - fader.queuedLevel = -1; - fader.phaseElapsedMs = 0.0f; - fader.phaseDurationMs = 0.0f; - fader.phase = LevelBackgroundPhase::Idle; -} - // Hover state for level popup ( -1 = none, 0..19 = hovered level ) // Now managed by LevelSelectorState @@ -494,6 +209,9 @@ int main(int, char **) assetLoader.init(renderer); LoadingManager loadingManager(&assetLoader); + // Legacy image loader (used only as a fallback when AssetLoader misses) + TextureLoader textureLoader(g_loadedTasks, g_currentLoadingFile, g_currentLoadingMutex, g_assetLoadErrors, g_assetLoadErrorsMutex); + // Font and UI asset handles (actual loading deferred until Loading state) FontAtlas pixelFont; FontAtlas font; @@ -1319,7 +1037,7 @@ int main(int, char **) // load synchronously using the legacy helper so the Menu can render. auto legacyLoad = [&](const std::string& p, SDL_Texture*& outTex, int* outW = nullptr, int* outH = nullptr) { if (!outTex) { - outTex = loadTextureFromImage(renderer, p, outW, outH); + outTex = textureLoader.loadFromImage(renderer, p, outW, outH); } }; @@ -1753,7 +1471,7 @@ int main(int, char **) case AppState::Menu: // Ensure overlay is loaded (drawn after highscores so it sits above that layer) if (!mainScreenTex) { - mainScreenTex = loadTextureFromImage(renderer, "assets/images/main_screen.png", &mainScreenW, &mainScreenH); + mainScreenTex = textureLoader.loadFromImage(renderer, "assets/images/main_screen.png", &mainScreenW, &mainScreenH); } // Render menu content that should appear *behind* the overlay (highscores/logo).