fixed main screen

This commit is contained in:
2025-12-17 18:13:59 +01:00
parent 3264672be0
commit cecf5cf68e
6 changed files with 479 additions and 147 deletions

122
src/app/AssetLoader.cpp Normal file
View File

@ -0,0 +1,122 @@
#include "app/AssetLoader.h"
#include <SDL3_image/SDL_image.h>
#include <algorithm>
AssetLoader::AssetLoader() = default;
AssetLoader::~AssetLoader() {
shutdown();
}
void AssetLoader::init(SDL_Renderer* renderer) {
m_renderer = renderer;
}
void AssetLoader::shutdown() {
// Destroy textures
{
std::lock_guard<std::mutex> 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<std::mutex> lk(m_queueMutex);
m_queue.clear();
}
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lk(m_queueMutex);
if (m_queue.empty()) return true;
path = m_queue.front();
m_queue.erase(m_queue.begin());
}
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lk(m_errorsMutex);
m_errors.push_back(std::string("CreateTexture failed: ") + fullPath);
} else {
std::lock_guard<std::mutex> lk(m_texturesMutex);
m_textures[path] = tex;
}
}
m_loadedTasks.fetch_add(1, std::memory_order_relaxed);
{
std::lock_guard<std::mutex> lk(m_currentLoadingMutex);
m_currentLoading.clear();
}
// Return true when no more queued tasks
{
std::lock_guard<std::mutex> 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<float>(loaded) / static_cast<float>(total);
}
std::vector<std::string> AssetLoader::getAndClearErrors() {
std::lock_guard<std::mutex> lk(m_errorsMutex);
std::vector<std::string> out = m_errors;
m_errors.clear();
return out;
}
SDL_Texture* AssetLoader::getTexture(const std::string& path) const {
std::lock_guard<std::mutex> 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<std::mutex> lk(m_currentLoadingMutex);
return m_currentLoading;
}

64
src/app/AssetLoader.h Normal file
View File

@ -0,0 +1,64 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include <vector>
#include <mutex>
#include <atomic>
#include <unordered_map>
// 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<std::string> 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<std::string> m_queue;
mutable std::mutex m_queueMutex;
std::unordered_map<std::string, SDL_Texture*> m_textures;
mutable std::mutex m_texturesMutex;
std::vector<std::string> m_errors;
mutable std::mutex m_errorsMutex;
std::atomic<int> m_totalTasks{0};
std::atomic<int> m_loadedTasks{0};
std::string m_currentLoading;
mutable std::mutex m_currentLoadingMutex;
};

View File

@ -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<size_t> 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<std::mutex> lk(g_assetLoadErrorsMutex);
g_assetLoadErrors.clear();
}
{
std::lock_guard<std::mutex> 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<std::string> 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<std::mutex> 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<std::mutex> 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<std::string> 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<std::mutex> lk(g_assetLoadErrorsMutex);
g_assetLoadErrors.clear();
}
{
std::lock_guard<std::mutex> 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<int>(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<std::string> 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<std::mutex> 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<std::mutex> 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<int>(std::lround(w));
outH = static_cast<int>(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<int>(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<float>(mainScreenW) : 0.0f;
float texH = mainScreenH > 0 ? static_cast<float>(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<float>(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<float>(mainScreenW) : 0.0f;
float texH = mainScreenH > 0 ? static_cast<float>(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<float>(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);
}

View File

@ -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<std::string> LoadingManager::getAndClearErrors() {
if (!m_loader) return {};
return m_loader->getAndClearErrors();
}
std::string LoadingManager::getCurrentLoading() const {
if (!m_loader) return {};
return m_loader->getCurrentLoading();
}

View File

@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
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<std::string> getAndClearErrors();
// Current path being loaded (or empty)
std::string getCurrentLoading() const;
private:
AssetLoader* m_loader = nullptr;
bool m_started = false;
};