diff --git a/CMakeLists.txt b/CMakeLists.txt index 4dcfd7f..af802f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,8 @@ set(TETRIS_SOURCES src/ui/MenuLayout.cpp src/app/BackgroundManager.cpp src/app/Fireworks.cpp + src/app/AssetLoader.cpp + src/states/LoadingManager.cpp # State implementations (new) src/states/LoadingState.cpp src/states/MenuState.cpp diff --git a/src/app/AssetLoader.cpp b/src/app/AssetLoader.cpp new file mode 100644 index 0000000..fdec338 --- /dev/null +++ b/src/app/AssetLoader.cpp @@ -0,0 +1,122 @@ +#include "app/AssetLoader.h" +#include +#include + +AssetLoader::AssetLoader() = default; + +AssetLoader::~AssetLoader() { + shutdown(); +} + +void AssetLoader::init(SDL_Renderer* renderer) { + m_renderer = renderer; +} + +void AssetLoader::shutdown() { + // Destroy textures + { + std::lock_guard lk(m_texturesMutex); + for (auto &p : m_textures) { + if (p.second) SDL_DestroyTexture(p.second); + } + m_textures.clear(); + } + + // Clear queue and errors + { + std::lock_guard lk(m_queueMutex); + m_queue.clear(); + } + { + std::lock_guard lk(m_errorsMutex); + m_errors.clear(); + } + + m_totalTasks = 0; + m_loadedTasks = 0; + m_renderer = nullptr; +} + +void AssetLoader::setBasePath(const std::string& basePath) { + m_basePath = basePath; +} + +void AssetLoader::queueTexture(const std::string& path) { + { + std::lock_guard lk(m_queueMutex); + m_queue.push_back(path); + } + m_totalTasks.fetch_add(1, std::memory_order_relaxed); +} + +bool AssetLoader::performStep() { + std::string path; + { + std::lock_guard lk(m_queueMutex); + if (m_queue.empty()) return true; + path = m_queue.front(); + m_queue.erase(m_queue.begin()); + } + + { + std::lock_guard lk(m_currentLoadingMutex); + m_currentLoading = path; + } + + 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()); + } 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); + m_textures[path] = tex; + } + } + + m_loadedTasks.fetch_add(1, std::memory_order_relaxed); + + { + std::lock_guard lk(m_currentLoadingMutex); + m_currentLoading.clear(); + } + + // Return true when no more queued tasks + { + std::lock_guard lk(m_queueMutex); + return m_queue.empty(); + } +} + +float AssetLoader::getProgress() const { + int total = m_totalTasks.load(std::memory_order_relaxed); + if (total <= 0) return 1.0f; + int loaded = m_loadedTasks.load(std::memory_order_relaxed); + return static_cast(loaded) / static_cast(total); +} + +std::vector AssetLoader::getAndClearErrors() { + std::lock_guard lk(m_errorsMutex); + std::vector out = m_errors; + m_errors.clear(); + return out; +} + +SDL_Texture* AssetLoader::getTexture(const std::string& path) const { + std::lock_guard lk(m_texturesMutex); + auto it = m_textures.find(path); + if (it == m_textures.end()) return nullptr; + return it->second; +} + +std::string AssetLoader::getCurrentLoading() const { + std::lock_guard lk(m_currentLoadingMutex); + return m_currentLoading; +} diff --git a/src/app/AssetLoader.h b/src/app/AssetLoader.h new file mode 100644 index 0000000..0d871e5 --- /dev/null +++ b/src/app/AssetLoader.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +// Lightweight AssetLoader scaffold. +// Responsibilities: +// - Queue textures to load (main thread) and perform incremental loads via performStep(). +// - Store loaded SDL_Texture* instances and provide accessors. +// - Collect loading errors thread-safely. +// NOTE: All SDL texture creation MUST happen on the thread that owns the SDL_Renderer. +class AssetLoader { +public: + AssetLoader(); + ~AssetLoader(); + + void init(SDL_Renderer* renderer); + void shutdown(); + + void setBasePath(const std::string& basePath); + + // Queue a texture path (relative to base path) for loading. + void queueTexture(const std::string& path); + + // Perform a single loading step (load one queued asset). + // Returns true when all queued tasks are complete, false otherwise. + bool performStep(); + + // Progress in [0,1]. If no tasks, returns 1.0f. + float getProgress() const; + + // Retrieve and clear accumulated error messages. + std::vector getAndClearErrors(); + + // Get a loaded texture (or nullptr if not loaded). + SDL_Texture* getTexture(const std::string& path) const; + + // Return currently-loading path (empty when idle). + std::string getCurrentLoading() const; + +private: + SDL_Renderer* m_renderer = nullptr; + std::string m_basePath; + + // queued paths (simple FIFO) + std::vector m_queue; + mutable std::mutex m_queueMutex; + + std::unordered_map m_textures; + mutable std::mutex m_texturesMutex; + + std::vector m_errors; + mutable std::mutex m_errorsMutex; + + std::atomic m_totalTasks{0}; + std::atomic m_loadedTasks{0}; + + std::string m_currentLoading; + mutable std::mutex m_currentLoadingMutex; +}; diff --git a/src/main.cpp b/src/main.cpp index 2e91f71..718b40c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -38,6 +38,8 @@ #include "states/LevelSelectorState.h" #include "states/PlayingState.h" #include "audio/MenuWrappers.h" +#include "app/AssetLoader.h" +#include "states/LoadingManager.h" #include "utils/ImagePathResolver.h" #include "graphics/renderers/GameRenderer.h" #include "core/Config.h" @@ -486,6 +488,11 @@ int main(int, char **) SDL_GetError()); } + // Asset loader (creates SDL_Textures on the main thread) + AssetLoader assetLoader; + assetLoader.init(renderer); + LoadingManager loadingManager(&assetLoader); + // Font and UI asset handles (actual loading deferred until Loading state) FontAtlas pixelFont; FontAtlas font; @@ -544,108 +551,10 @@ int main(int, char **) std::atomic_bool g_loadingComplete{false}; std::atomic g_loadingStep{0}; - // Define performLoadingStep to execute one load operation per frame on main thread - auto performLoadingStep = [&]() -> bool { - size_t step = g_loadingStep.fetch_add(1); - - // Initialize counters on first step - if (step == 0) { - constexpr int baseTasks = 25; // 2 fonts + 2 logos + 1 main + 1 blocks + 3 panels + 16 SFX - g_totalLoadingTasks.store(baseTasks); - g_loadedTasks.store(0); - { - std::lock_guard lk(g_assetLoadErrorsMutex); - g_assetLoadErrors.clear(); - } - { - std::lock_guard lk(g_currentLoadingMutex); - g_currentLoadingFile.clear(); - } - - // Initialize background music loading - Audio::instance().init(); - for (int i = 1; i <= 100; ++i) { - char base[128]; - std::snprintf(base, sizeof(base), "assets/music/music%03d", i); - std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" }); - if (path.empty()) break; - Audio::instance().addTrackAsync(path); - totalTracks++; - } - // Expand task budget to account for music tracks so the loading bar waits for them - g_totalLoadingTasks.store(baseTasks + totalTracks); - if (totalTracks > 0) { - Audio::instance().startBackgroundLoading(); - musicLoadingStarted = true; - } else { - // No music files found, mark as loaded so game can continue - musicLoaded = true; - } - } - - // Execute one load operation per step - switch (step) { - case 0: return false; // Init step - case 1: pixelFont.init(AssetPath::resolveWithBase("assets/fonts/Orbitron.ttf"), 22); g_loadedTasks.fetch_add(1); break; - case 2: font.init(AssetPath::resolveWithBase("assets/fonts/Exo2.ttf"), 20); g_loadedTasks.fetch_add(1); break; - case 3: logoTex = loadTextureFromImage(renderer, "assets/images/spacetris.png"); break; - case 4: logoSmallTex = loadTextureFromImage(renderer, "assets/images/spacetris.png", &logoSmallW, &logoSmallH); break; - case 5: mainScreenTex = loadTextureFromImage(renderer, "assets/images/main_screen.png", &mainScreenW, &mainScreenH); - if (mainScreenTex) SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND); break; - case 6: - blocksTex = loadTextureFromImage(renderer, "assets/images/blocks90px_001.bmp"); - if (!blocksTex) { - blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90); - SDL_SetRenderTarget(renderer, blocksTex); - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); - SDL_RenderClear(renderer); - for (int i = 0; i < PIECE_COUNT; ++i) { - SDL_Color c = COLORS[i + 1]; - SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); - SDL_FRect rect{(float)(i * 90), 0, 90, 90}; - SDL_RenderFillRect(renderer, &rect); - } - SDL_SetRenderTarget(renderer, nullptr); - g_loadedTasks.fetch_add(1); - } - break; - case 7: scorePanelTex = loadTextureFromImage(renderer, "assets/images/panel_score.png"); - if (scorePanelTex) SDL_SetTextureBlendMode(scorePanelTex, SDL_BLENDMODE_BLEND); break; - case 8: statisticsPanelTex = loadTextureFromImage(renderer, "assets/images/statistics_panel.png"); - if (statisticsPanelTex) SDL_SetTextureBlendMode(statisticsPanelTex, SDL_BLENDMODE_BLEND); break; - case 9: nextPanelTex = loadTextureFromImage(renderer, "assets/images/next_panel.png"); - if (nextPanelTex) SDL_SetTextureBlendMode(nextPanelTex, SDL_BLENDMODE_BLEND); break; - case 10: SoundEffectManager::instance().init(); g_loadedTasks.fetch_add(1); break; - - // Audio loading steps - default: { - const std::vector audioIds = {"clear_line","nice_combo","you_fire","well_played","keep_that_ryhtm","great_move","smooth_clear","impressive","triple_strike","amazing","you_re_unstoppable","boom_tetris","wonderful","lets_go","hard_drop","new_level"}; - size_t audioIdx = step - 11; - if (audioIdx < audioIds.size()) { - std::string id = audioIds[audioIdx]; - std::string basePath = "assets/music/" + (id == "hard_drop" ? "hard_drop_001" : id); - { - std::lock_guard lk(g_currentLoadingMutex); - g_currentLoadingFile = basePath; - } - std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" }); - if (!resolved.empty()) { - SoundEffectManager::instance().loadSound(id, resolved); - } - g_loadedTasks.fetch_add(1); - { - std::lock_guard lk(g_currentLoadingMutex); - g_currentLoadingFile.clear(); - } - } else { - // All done - return true; - } - break; - } - } - return false; // More steps remaining - }; + // Loading is now handled by AssetLoader + LoadingManager. + // Old incremental lambda removed; use LoadingManager to queue texture loads and + // perform a single step per frame. Non-texture initialization (fonts, SFX) + // is performed on the first loading frame below when the loader is started. Game game(startLevelSelection); // Apply global gravity speed multiplier from config @@ -1261,9 +1170,10 @@ int main(int, char **) currentTrackLoading = Audio::instance().getLoadedTrackCount(); if (Audio::instance().isLoadingComplete() || (totalTracks > 0 && currentTrackLoading >= totalTracks)) { Audio::instance().shuffle(); - if (musicEnabled) { - Audio::instance().start(); - } + // Defer starting playback until the app has entered the Menu/Playing state. + // Actual playback is started below when `musicLoaded` is observed and + // the state is Menu or Playing (so the user doesn't hear music while + // still on the Loading screen). musicLoaded = true; } } @@ -1300,10 +1210,154 @@ int main(int, char **) } else if (state == AppState::Loading) { - // Execute one loading step per frame on main thread + static int queuedTextureCount = 0; + // Execute one loading step per frame on main thread via LoadingManager if (g_loadingStarted.load() && !g_loadingComplete.load()) { - if (performLoadingStep()) { - g_loadingComplete.store(true); + static bool queuedTextures = false; + static std::vector queuedPaths; + if (!queuedTextures) { + queuedTextures = true; + // Initialize counters and clear previous errors + constexpr int baseTasks = 25; // keep same budget as before + g_totalLoadingTasks.store(baseTasks); + g_loadedTasks.store(0); + { + std::lock_guard lk(g_assetLoadErrorsMutex); + g_assetLoadErrors.clear(); + } + { + std::lock_guard lk(g_currentLoadingMutex); + g_currentLoadingFile.clear(); + } + + // Initialize background music loading + Audio::instance().init(); + totalTracks = 0; + for (int i = 1; i <= 100; ++i) { + char base[128]; + std::snprintf(base, sizeof(base), "assets/music/music%03d", i); + std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" }); + if (path.empty()) break; + Audio::instance().addTrackAsync(path); + totalTracks++; + } + g_totalLoadingTasks.store(baseTasks + totalTracks); + if (totalTracks > 0) { + Audio::instance().startBackgroundLoading(); + musicLoadingStarted = true; + } else { + musicLoaded = true; + } + + // Initialize fonts (synchronous, cheap) + pixelFont.init(AssetPath::resolveWithBase("assets/fonts/Orbitron.ttf"), 22); + g_loadedTasks.fetch_add(1); + font.init(AssetPath::resolveWithBase("assets/fonts/Exo2.ttf"), 20); + g_loadedTasks.fetch_add(1); + + // Queue UI textures for incremental loading + queuedPaths = { + "assets/images/spacetris.png", + "assets/images/spacetris.png", // small logo uses same source + "assets/images/main_screen.png", + "assets/images/blocks90px_001.bmp", + "assets/images/panel_score.png", + "assets/images/statistics_panel.png", + "assets/images/next_panel.png" + }; + for (auto &p : queuedPaths) { + loadingManager.queueTexture(p); + } + queuedTextureCount = static_cast(queuedPaths.size()); + + // Initialize sound effects manager (counts as a loaded task) + SoundEffectManager::instance().init(); + g_loadedTasks.fetch_add(1); + + // Load small set of voice/audio SFX synchronously for now (keeps behavior) + const std::vector audioIds = {"clear_line","nice_combo","you_fire","well_played","keep_that_ryhtm","great_move","smooth_clear","impressive","triple_strike","amazing","you_re_unstoppable","boom_tetris","wonderful","lets_go","hard_drop","new_level"}; + for (const auto &id : audioIds) { + std::string basePath = "assets/music/" + (id == "hard_drop" ? "hard_drop_001" : id); + { + std::lock_guard lk(g_currentLoadingMutex); + g_currentLoadingFile = basePath; + } + std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" }); + if (!resolved.empty()) { + SoundEffectManager::instance().loadSound(id, resolved); + } + g_loadedTasks.fetch_add(1); + { + std::lock_guard lk(g_currentLoadingMutex); + g_currentLoadingFile.clear(); + } + } + } + + // Perform a single texture loading step via LoadingManager + bool texturesDone = loadingManager.update(); + if (texturesDone) { + // Bind loaded textures into the runtime context + logoTex = assetLoader.getTexture("assets/images/spacetris.png"); + logoSmallTex = assetLoader.getTexture("assets/images/spacetris.png"); + mainScreenTex = assetLoader.getTexture("assets/images/main_screen.png"); + blocksTex = assetLoader.getTexture("assets/images/blocks90px_001.bmp"); + scorePanelTex = assetLoader.getTexture("assets/images/panel_score.png"); + statisticsPanelTex = assetLoader.getTexture("assets/images/statistics_panel.png"); + nextPanelTex = assetLoader.getTexture("assets/images/next_panel.png"); + + auto ensureTextureSize = [&](SDL_Texture* tex, int& outW, int& outH) { + if (!tex) return; + if (outW > 0 && outH > 0) return; + float w = 0.0f, h = 0.0f; + if (SDL_GetTextureSize(tex, &w, &h)) { + outW = static_cast(std::lround(w)); + outH = static_cast(std::lround(h)); + } + }; + + // If a texture was created by AssetLoader (not legacy IMG_Load), + // its stored width/height may still be 0. Query the real size. + ensureTextureSize(logoSmallTex, logoSmallW, logoSmallH); + ensureTextureSize(mainScreenTex, mainScreenW, mainScreenH); + + // Fallback: if any critical UI texture failed to load via AssetLoader, + // 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); + } + }; + + legacyLoad("assets/images/spacetris.png", logoTex); + legacyLoad("assets/images/spacetris.png", logoSmallTex, &logoSmallW, &logoSmallH); + legacyLoad("assets/images/main_screen.png", mainScreenTex, &mainScreenW, &mainScreenH); + legacyLoad("assets/images/blocks90px_001.bmp", blocksTex); + legacyLoad("assets/images/panel_score.png", scorePanelTex); + legacyLoad("assets/images/statistics_panel.png", statisticsPanelTex); + legacyLoad("assets/images/next_panel.png", nextPanelTex); + + // If blocks texture failed, create fallback and count it as loaded + if (!blocksTex) { + blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90); + SDL_SetRenderTarget(renderer, blocksTex); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); + SDL_RenderClear(renderer); + for (int i = 0; i < PIECE_COUNT; ++i) { + SDL_Color c = COLORS[i + 1]; + SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); + SDL_FRect rect{(float)(i * 90), 0, 90, 90}; + SDL_RenderFillRect(renderer, &rect); + } + SDL_SetRenderTarget(renderer, nullptr); + // Do not update global task counter here; textures are accounted + // for via the LoadingManager/AssetLoader progress below. + } + + // Mark loading complete when music also loaded + if (musicLoaded) { + g_loadingComplete.store(true); + } } } @@ -1311,6 +1365,13 @@ int main(int, char **) const int totalTasks = g_totalLoadingTasks.load(std::memory_order_acquire); const int musicDone = std::min(totalTracks, currentTrackLoading); int doneTasks = g_loadedTasks.load(std::memory_order_acquire) + musicDone; + // Include texture progress reported by the LoadingManager/AssetLoader + if (queuedTextureCount > 0) { + float texProg = loadingManager.getProgress(); + int texDone = static_cast(std::floor(texProg * queuedTextureCount + 0.5f)); + if (texDone > queuedTextureCount) texDone = queuedTextureCount; + doneTasks += texDone; + } if (doneTasks > totalTasks) doneTasks = totalTasks; if (totalTasks > 0) { loadingProgress = std::min(1.0, double(doneTasks) / double(totalTasks)); @@ -1696,8 +1757,53 @@ int main(int, char **) } break; case AppState::Menu: - // Delegate full menu rendering to MenuState object now - menuState->render(renderer, logicalScale, logicalVP); + // 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); + } + + // Render menu content that should appear *behind* the overlay (highscores/logo). + // Bottom buttons are drawn separately on top. + if (menuState) { + menuState->drawMainButtonNormally = false; + menuState->render(renderer, logicalScale, logicalVP); + } + + // Draw main screen overlay above highscores + if (mainScreenTex) { + SDL_SetRenderViewport(renderer, nullptr); + SDL_SetRenderScale(renderer, 1.f, 1.f); + float texW = mainScreenW > 0 ? static_cast(mainScreenW) : 0.0f; + float texH = mainScreenH > 0 ? static_cast(mainScreenH) : 0.0f; + if (texW <= 0.0f || texH <= 0.0f) { + float iwf = 0.0f, ihf = 0.0f; + if (!SDL_GetTextureSize(mainScreenTex, &iwf, &ihf)) { + iwf = ihf = 0.0f; + } + texW = iwf; + texH = ihf; + } + if (texW > 0.0f && texH > 0.0f) { + const float drawH = static_cast(winH); + const float scale = drawH / texH; + const float drawW = texW * scale; + SDL_FRect dst{ + (winW - drawW) * 0.5f, + 0.0f, + drawW, + drawH + }; + SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND); + SDL_RenderTexture(renderer, mainScreenTex, nullptr, &dst); + } + SDL_SetRenderViewport(renderer, &logicalVP); + SDL_SetRenderScale(renderer, logicalScale, logicalScale); + } + + // Draw bottom menu buttons above the overlay + if (menuState) { + menuState->renderMainButtonTop(renderer, logicalScale, logicalVP); + } break; case AppState::Options: optionsState->render(renderer, logicalScale, logicalVP); @@ -1924,43 +2030,6 @@ int main(int, char **) HelpOverlay::Render(renderer, pixelFont, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY); } - // Top-layer overlay: render `mainScreenTex` above all other layers when in Menu - if (state == AppState::Menu && mainScreenTex) { - SDL_SetRenderViewport(renderer, nullptr); - SDL_SetRenderScale(renderer, 1.f, 1.f); - float texW = mainScreenW > 0 ? static_cast(mainScreenW) : 0.0f; - float texH = mainScreenH > 0 ? static_cast(mainScreenH) : 0.0f; - if (texW <= 0.0f || texH <= 0.0f) { - float iwf = 0.0f, ihf = 0.0f; - if (SDL_GetTextureSize(mainScreenTex, &iwf, &ihf) != 0) { - iwf = ihf = 0.0f; - } - texW = iwf; - texH = ihf; - } - if (texW > 0.0f && texH > 0.0f) { - const float drawH = static_cast(winH); - const float scale = drawH / texH; - const float drawW = texW * scale; - SDL_FRect dst{ - (winW - drawW) * 0.5f, - 0.0f, - drawW, - drawH - }; - SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND); - SDL_RenderTexture(renderer, mainScreenTex, nullptr, &dst); - } - // Restore logical viewport/scale and draw the main PLAY button above the overlay - SDL_SetRenderViewport(renderer, &logicalVP); - SDL_SetRenderScale(renderer, logicalScale, logicalScale); - if (menuState) { - menuState->drawMainButtonNormally = false; // ensure it isn't double-drawn - menuState->renderMainButtonTop(renderer, logicalScale, logicalVP); - menuState->drawMainButtonNormally = true; - } - } - SDL_RenderPresent(renderer); SDL_SetRenderScale(renderer, 1.f, 1.f); } diff --git a/src/states/LoadingManager.cpp b/src/states/LoadingManager.cpp new file mode 100644 index 0000000..d87f9ce --- /dev/null +++ b/src/states/LoadingManager.cpp @@ -0,0 +1,39 @@ +#include "states/LoadingManager.h" +#include "app/AssetLoader.h" + +LoadingManager::LoadingManager(AssetLoader* loader) + : m_loader(loader) +{ +} + +void LoadingManager::queueTexture(const std::string& path) { + if (!m_loader) return; + m_loader->queueTexture(path); +} + +void LoadingManager::start() { + m_started = true; +} + +bool LoadingManager::update() { + if (!m_loader) return true; + // perform a single step on the loader; AssetLoader::performStep returns true when + // there are no more queued tasks. + bool done = m_loader->performStep(); + return done; +} + +float LoadingManager::getProgress() const { + if (!m_loader) return 1.0f; + return m_loader->getProgress(); +} + +std::vector LoadingManager::getAndClearErrors() { + if (!m_loader) return {}; + return m_loader->getAndClearErrors(); +} + +std::string LoadingManager::getCurrentLoading() const { + if (!m_loader) return {}; + return m_loader->getCurrentLoading(); +} diff --git a/src/states/LoadingManager.h b/src/states/LoadingManager.h new file mode 100644 index 0000000..94eb0bf --- /dev/null +++ b/src/states/LoadingManager.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +class AssetLoader; + +// LoadingManager: thin facade over AssetLoader for incremental loading. +// Main thread only. Call update() once per frame to perform a single step. +class LoadingManager { +public: + explicit LoadingManager(AssetLoader* loader); + + // Queue a texture path (relative to base path) for loading. + void queueTexture(const std::string& path); + + // Start loading (idempotent). + void start(); + + // Perform a single loading step. Returns true when loading complete. + bool update(); + + // Progress in [0,1] + float getProgress() const; + + // Return and clear any accumulated loading errors. + std::vector getAndClearErrors(); + + // Current path being loaded (or empty) + std::string getCurrentLoading() const; + +private: + AssetLoader* m_loader = nullptr; + bool m_started = false; +};