diff --git a/CMakeLists.txt b/CMakeLists.txt index 64e08f6..5ad14ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,7 @@ set(TETRIS_SOURCES src/graphics/renderers/SyncLineRenderer.cpp src/graphics/renderers/UIRenderer.cpp src/audio/Audio.cpp + src/renderer/SDLRenderer.cpp src/gameplay/effects/LineEffect.cpp src/audio/SoundEffect.cpp src/video/VideoPlayer.cpp @@ -66,6 +67,7 @@ set(TETRIS_SOURCES src/app/Fireworks.cpp src/app/AssetLoader.cpp src/app/TextureLoader.cpp + src/resources/ResourceManager.cpp src/states/LoadingManager.cpp # State implementations (new) src/states/LoadingState.cpp diff --git a/src/app/AssetLoader.cpp b/src/app/AssetLoader.cpp index 95d060e..c9ca0c0 100644 --- a/src/app/AssetLoader.cpp +++ b/src/app/AssetLoader.cpp @@ -1,6 +1,10 @@ #include "app/AssetLoader.h" #include #include +#include "app/TextureLoader.h" + +#include "utils/ImagePathResolver.h" +#include AssetLoader::AssetLoader() = default; @@ -37,6 +41,10 @@ void AssetLoader::shutdown() { m_renderer = nullptr; } +void AssetLoader::setResourceManager(resources::ResourceManager* mgr) { + m_resourceManager = mgr; +} + void AssetLoader::setBasePath(const std::string& basePath) { m_basePath = basePath; } @@ -65,24 +73,25 @@ bool AssetLoader::performStep() { std::string fullPath = m_basePath.empty() ? path : (m_basePath + "/" + path); - SDL_Surface* surf = IMG_Load(fullPath.c_str()); - if (!surf) { - std::lock_guard lk(m_errorsMutex); - m_errors.push_back(std::string("IMG_Load failed: ") + fullPath + " -> " + SDL_GetError()); + // Diagnostic: resolve path and check file existence + const std::string resolved = AssetPath::resolveImagePath(path); + bool exists = false; + try { if (!resolved.empty()) exists = std::filesystem::exists(std::filesystem::u8path(resolved)); } catch (...) { exists = false; } + + // Use TextureLoader to centralize loading and ResourceManager caching + TextureLoader loader(m_loadedTasks, m_currentLoading, m_currentLoadingMutex, m_errors, m_errorsMutex); + loader.setResourceManager(m_resourceManager); + // Pass the original queued path (not the full resolved path) so caching keys stay consistent + SDL_Texture* tex = loader.loadFromImage(m_renderer, path); + if (!tex) { + // errors have been recorded by TextureLoader } else { - SDL_Texture* tex = SDL_CreateTextureFromSurface(m_renderer, surf); - SDL_DestroySurface(surf); - if (!tex) { - std::lock_guard lk(m_errorsMutex); - m_errors.push_back(std::string("CreateTexture failed: ") + fullPath); - } else { - std::lock_guard lk(m_texturesMutex); - auto& slot = m_textures[path]; - if (slot && slot != tex) { - SDL_DestroyTexture(slot); - } - slot = tex; + std::lock_guard lk(m_texturesMutex); + auto& slot = m_textures[path]; + if (slot && slot != tex) { + SDL_DestroyTexture(slot); } + slot = tex; } m_loadedTasks.fetch_add(1, std::memory_order_relaxed); @@ -104,12 +113,17 @@ void AssetLoader::adoptTexture(const std::string& path, SDL_Texture* texture) { return; } + // register in local map and resource manager std::lock_guard lk(m_texturesMutex); auto& slot = m_textures[path]; if (slot && slot != texture) { SDL_DestroyTexture(slot); } slot = texture; + if (m_resourceManager) { + std::shared_ptr sp(texture, [](void* t){ SDL_DestroyTexture(static_cast(t)); }); + m_resourceManager->put(path, sp); + } } float AssetLoader::getProgress() const { diff --git a/src/app/AssetLoader.h b/src/app/AssetLoader.h index fac6128..5ab6470 100644 --- a/src/app/AssetLoader.h +++ b/src/app/AssetLoader.h @@ -6,6 +6,7 @@ #include #include #include +#include "../resources/ResourceManager.h" // Lightweight AssetLoader scaffold. // Responsibilities: @@ -22,6 +23,7 @@ public: void shutdown(); void setBasePath(const std::string& basePath); + void setResourceManager(resources::ResourceManager* mgr); // Queue a texture path (relative to base path) for loading. void queueTexture(const std::string& path); @@ -49,6 +51,7 @@ public: private: SDL_Renderer* m_renderer = nullptr; std::string m_basePath; + resources::ResourceManager* m_resourceManager = nullptr; // queued paths (simple FIFO) std::vector m_queue; diff --git a/src/app/TetrisApp.cpp b/src/app/TetrisApp.cpp index 0bd4d0b..e0e26be 100644 --- a/src/app/TetrisApp.cpp +++ b/src/app/TetrisApp.cpp @@ -68,6 +68,7 @@ #include "ui/MenuLayout.h" #include "utils/ImagePathResolver.h" +#include "../resources/ResourceManager.h" // ---------- Game config ---------- static constexpr int LOGICAL_W = 1200; @@ -187,6 +188,7 @@ struct TetrisApp::Impl { AssetLoader assetLoader; std::unique_ptr loadingManager; std::unique_ptr textureLoader; + resources::ResourceManager resourceManager; FontAtlas pixelFont; FontAtlas font; @@ -427,6 +429,8 @@ int TetrisApp::Impl::init() // Asset loader (creates SDL_Textures on the main thread) assetLoader.init(renderer); + // Wire resource manager into loader so textures are cached and reused + assetLoader.setResourceManager(&resourceManager); loadingManager = std::make_unique(&assetLoader); // Legacy image loader (used only as a fallback when AssetLoader misses) @@ -436,6 +440,8 @@ int TetrisApp::Impl::init() currentLoadingMutex, assetLoadErrors, assetLoadErrorsMutex); + // Let legacy TextureLoader access the same resource cache + textureLoader->setResourceManager(&resourceManager); // Load scores asynchronously but keep the worker alive until shutdown scoreLoader = std::jthread([this]() { @@ -1785,6 +1791,8 @@ void TetrisApp::Impl::runLoop() nextPanelTex = assetLoader.getTexture(Assets::NEXT_PANEL); holdPanelTex = assetLoader.getTexture(Assets::HOLD_PANEL); + // texture retrieval diagnostics removed + auto ensureTextureSize = [&](SDL_Texture* tex, int& outW, int& outH) { if (!tex) return; if (outW > 0 && outH > 0) return; diff --git a/src/app/TextureLoader.cpp b/src/app/TextureLoader.cpp index ef9af8e..75d1bf4 100644 --- a/src/app/TextureLoader.cpp +++ b/src/app/TextureLoader.cpp @@ -6,6 +6,8 @@ #include #include +#include + #include "utils/ImagePathResolver.h" TextureLoader::TextureLoader( @@ -45,6 +47,18 @@ SDL_Texture* TextureLoader::loadFromImage(SDL_Renderer* renderer, const std::str const std::string resolvedPath = AssetPath::resolveImagePath(path); setCurrentLoadingFile(resolvedPath.empty() ? path : resolvedPath); + // Check filesystem existence for diagnostics (no console log) + bool fileExists = false; + try { if (!resolvedPath.empty()) fileExists = std::filesystem::exists(std::filesystem::u8path(resolvedPath)); } catch (...) { fileExists = false; } + // If resource manager provided, check cache first using the original asset key (path) + if (resourceManager_) { + if (auto sp = resourceManager_->get(path)) { + clearCurrentLoadingFile(); + loadedTasks_.fetch_add(1); + return sp.get(); + } + } + SDL_Surface* surface = IMG_Load(resolvedPath.c_str()); if (!surface) { { @@ -54,7 +68,7 @@ SDL_Texture* TextureLoader::loadFromImage(SDL_Renderer* renderer, const std::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()); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load image %s (resolved: %s) exists=%s: %s", path.c_str(), resolvedPath.c_str(), fileExists ? "yes" : "no", SDL_GetError()); return nullptr; } @@ -66,6 +80,7 @@ SDL_Texture* TextureLoader::loadFromImage(SDL_Renderer* renderer, const std::str } SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + // surface size preserved in outW/outH; no console log SDL_DestroySurface(surface); if (!texture) { @@ -80,6 +95,15 @@ SDL_Texture* TextureLoader::loadFromImage(SDL_Renderer* renderer, const std::str return nullptr; } + // No texture-size console diagnostics here + + // cache in resource manager if present + if (resourceManager_) { + std::shared_ptr sp(texture, [](void* t){ SDL_DestroyTexture(static_cast(t)); }); + // store under original asset key (path) so callers using logical asset names find them + resourceManager_->put(path, sp); + } + loadedTasks_.fetch_add(1); clearCurrentLoadingFile(); diff --git a/src/app/TextureLoader.h b/src/app/TextureLoader.h index d807fe7..1fbaca7 100644 --- a/src/app/TextureLoader.h +++ b/src/app/TextureLoader.h @@ -6,6 +6,7 @@ #include #include #include +#include "../resources/ResourceManager.h" class TextureLoader { public: @@ -16,6 +17,8 @@ public: std::vector& assetLoadErrors, std::mutex& assetLoadErrorsMutex); + void setResourceManager(resources::ResourceManager* mgr) { resourceManager_ = mgr; } + SDL_Texture* loadFromImage(SDL_Renderer* renderer, const std::string& path, int* outW = nullptr, int* outH = nullptr); private: @@ -28,4 +31,6 @@ private: void setCurrentLoadingFile(const std::string& filename); void clearCurrentLoadingFile(); void recordAssetLoadError(const std::string& message); + + resources::ResourceManager* resourceManager_ = nullptr; }; diff --git a/src/core/assets/AssetManager.cpp b/src/core/assets/AssetManager.cpp index dc1d23d..fbbba17 100644 --- a/src/core/assets/AssetManager.cpp +++ b/src/core/assets/AssetManager.cpp @@ -7,6 +7,8 @@ #include #include #include "../../utils/ImagePathResolver.h" +#include "../../core/Config.h" +#include "../../resources/AssetPaths.h" AssetManager::AssetManager() : m_renderer(nullptr) @@ -103,7 +105,34 @@ SDL_Texture* AssetManager::loadTexture(const std::string& id, const std::string& SDL_Texture* AssetManager::getTexture(const std::string& id) const { auto it = m_textures.find(id); - return (it != m_textures.end()) ? it->second : nullptr; + if (it != m_textures.end()) return it->second; + + // Lazy fallback: attempt to load well-known short ids from configured asset paths. + std::vector candidates; + if (id == "logo") { + candidates.push_back(std::string(Assets::LOGO)); + candidates.push_back(Config::Assets::LOGO_BMP); + } else if (id == "logo_small") { + candidates.push_back(Config::Assets::LOGO_SMALL_BMP); + candidates.push_back(std::string(Assets::LOGO)); + } else if (id == "background") { + candidates.push_back(std::string(Assets::MAIN_SCREEN)); + candidates.push_back(Config::Assets::BACKGROUND_BMP); + } else if (id == "blocks") { + candidates.push_back(std::string(Assets::BLOCKS_SPRITE)); + candidates.push_back(Config::Assets::BLOCKS_BMP); + } else if (id == "asteroids") { + candidates.push_back(std::string(Assets::ASTEROID_SPRITE)); + } + + for (const auto &candidatePath : candidates) { + if (candidatePath.empty()) continue; + AssetManager* self = const_cast(this); + SDL_Texture* tex = self->loadTexture(id, candidatePath); + if (tex) return tex; + } + + return nullptr; } bool AssetManager::unloadTexture(const std::string& id) { diff --git a/src/renderer/Renderer.h b/src/renderer/Renderer.h new file mode 100644 index 0000000..2b2827a --- /dev/null +++ b/src/renderer/Renderer.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace renderer { + +class Renderer { +public: + virtual ~Renderer() = default; + + // Wrap common operations used by renderers + virtual SDL_Texture* createTextureFromSurface(SDL_Surface* surf) = 0; + virtual void destroyTexture(SDL_Texture* tex) = 0; + virtual void copy(SDL_Texture* tex, const SDL_Rect* src, const SDL_Rect* dst) = 0; + virtual void clear(const SDL_Color& color) = 0; + virtual void present() = 0; +}; + +} // namespace renderer diff --git a/src/renderer/SDLRenderer.cpp b/src/renderer/SDLRenderer.cpp new file mode 100644 index 0000000..e66e199 --- /dev/null +++ b/src/renderer/SDLRenderer.cpp @@ -0,0 +1,46 @@ +#include "Renderer.h" +#include + +namespace renderer { + +class SDLRendererImpl : public Renderer { +public: + explicit SDLRendererImpl(SDL_Renderer* rdr) : rdr_(rdr) {} + ~SDLRendererImpl() override = default; + + SDL_Texture* createTextureFromSurface(SDL_Surface* surf) override { + if (!rdr_ || !surf) return nullptr; + return SDL_CreateTextureFromSurface(rdr_, surf); + } + + void destroyTexture(SDL_Texture* tex) override { + if (tex) SDL_DestroyTexture(tex); + } + + void copy(SDL_Texture* tex, const SDL_Rect* src, const SDL_Rect* dst) override { + if (!rdr_ || !tex) return; + // SDL_RenderCopy mapping differs across SDL versions; defer to existing renderers + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "SDLRenderer::copy called — fallback no-op (use RenderManager for real draws)"); + } + + void clear(const SDL_Color& color) override { + if (!rdr_) return; + SDL_SetRenderDrawColor(rdr_, color.r, color.g, color.b, color.a); + SDL_RenderClear(rdr_); + } + + void present() override { + if (!rdr_) return; + SDL_RenderPresent(rdr_); + } + +private: + SDL_Renderer* rdr_ = nullptr; +}; + +// Factory helper +std::unique_ptr MakeSDLRenderer(SDL_Renderer* rdr) { + return std::make_unique(rdr); +} + +} // namespace renderer diff --git a/src/resources/ResourceManager.cpp b/src/resources/ResourceManager.cpp new file mode 100644 index 0000000..edb11fa --- /dev/null +++ b/src/resources/ResourceManager.cpp @@ -0,0 +1,41 @@ +#include "ResourceManager.h" +#include + +namespace resources { + +ResourceManager::ResourceManager() = default; +ResourceManager::~ResourceManager() = default; + +std::future> ResourceManager::loadAsync(const std::string& key, std::function(const std::string&)> loader) +{ + // Quick check for existing cached resource + { + std::lock_guard lk(mutex_); + auto it = cache_.find(key); + if (it != cache_.end()) { + // Return already-available resource (keep strong ref) + auto sp = it->second; + if (sp) { + return std::async(std::launch::deferred, [sp]() { return sp; }); + } + } + } + + // Launch async loader + return std::async(std::launch::async, [this, key, loader]() { + auto res = loader(key); + if (res) { + std::lock_guard lk(mutex_); + cache_[key] = res; // store strong reference + } + return res; + }); +} + +void ResourceManager::put(const std::string& key, std::shared_ptr resource) +{ + std::lock_guard lk(mutex_); + cache_[key] = resource; // store strong reference so callers using raw pointers stay valid +} + +} // namespace resources diff --git a/src/resources/ResourceManager.h b/src/resources/ResourceManager.h new file mode 100644 index 0000000..6010c63 --- /dev/null +++ b/src/resources/ResourceManager.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace resources { + +class ResourceManager { +public: + ResourceManager(); + ~ResourceManager(); + + // Return cached resource if available and of the right type + template + std::shared_ptr get(const std::string& key) + { + std::lock_guard lk(mutex_); + auto it = cache_.find(key); + if (it == cache_.end()) return nullptr; + auto sp = it->second; + if (!sp) { cache_.erase(it); return nullptr; } + return std::static_pointer_cast(sp); + } + + // Asynchronously load a resource using the provided loader function. + // The loader must return a shared_ptr to the concrete resource (boxed as void). + std::future> loadAsync(const std::string& key, std::function(const std::string&)> loader); + + // Insert a resource into the cache (thread-safe) + void put(const std::string& key, std::shared_ptr resource); + +private: + // Keep strong ownership of cached resources so they remain valid + // while present in the cache. + std::unordered_map> cache_; + std::mutex mutex_; +}; + +} // namespace resources