From b1f20338809f8dc99eefda69a990ade3a1e29cd0 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Thu, 25 Dec 2025 10:15:23 +0100 Subject: [PATCH 01/10] Scaffold the pure game model - Added a pure, SDL-free Board model implementing grid access and clearFullLines(). - Added a small standalone test at test_board.cpp (simple assert-based; not yet wired into CMake). --- src/logic/Board.cpp | 59 ++++++++++++++++++++++++++++++++++++++++++++ src/logic/Board.h | 32 ++++++++++++++++++++++++ tests/test_board.cpp | 31 +++++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 src/logic/Board.cpp create mode 100644 src/logic/Board.h create mode 100644 tests/test_board.cpp diff --git a/src/logic/Board.cpp b/src/logic/Board.cpp new file mode 100644 index 0000000..ca6ef59 --- /dev/null +++ b/src/logic/Board.cpp @@ -0,0 +1,59 @@ +#include "Board.h" +#include + +namespace logic { + +Board::Board() + : grid_(Width * Height, Cell::Empty) +{ +} + +void Board::clear() +{ + std::fill(grid_.begin(), grid_.end(), Cell::Empty); +} + +bool Board::inBounds(int x, int y) const +{ + return x >= 0 && x < Width && y >= 0 && y < Height; +} + +Board::Cell Board::at(int x, int y) const +{ + if (!inBounds(x, y)) return Cell::Empty; + return grid_[y * Width + x]; +} + +void Board::set(int x, int y, Cell c) +{ + if (!inBounds(x, y)) return; + grid_[y * Width + x] = c; +} + +int Board::clearFullLines() +{ + int cleared = 0; + // scan from bottom to top + for (int y = Height - 1; y >= 0; --y) { + bool full = true; + for (int x = 0; x < Width; ++x) { + if (at(x, y) == Cell::Empty) { full = false; break; } + } + if (full) { + // remove row y: move all rows above down by one + for (int yy = y; yy > 0; --yy) { + for (int x = 0; x < Width; ++x) { + grid_[yy * Width + x] = grid_[(yy - 1) * Width + x]; + } + } + // clear top row + for (int x = 0; x < Width; ++x) grid_[x] = Cell::Empty; + ++cleared; + // stay on same y to re-check the row that fell into place + ++y; // because next iteration decrements y + } + } + return cleared; +} + +} // namespace logic diff --git a/src/logic/Board.h b/src/logic/Board.h new file mode 100644 index 0000000..6ed5a27 --- /dev/null +++ b/src/logic/Board.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +namespace logic { + +class Board { +public: + static constexpr int Width = 10; + static constexpr int Height = 20; + + enum class Cell : uint8_t { Empty = 0, Filled = 1 }; + + Board(); + + void clear(); + + Cell at(int x, int y) const; + void set(int x, int y, Cell c); + bool inBounds(int x, int y) const; + + // Remove and return number of full lines cleared. Rows above fall down. + int clearFullLines(); + + const std::vector& data() const { return grid_; } + +private: + std::vector grid_; // row-major: y*Width + x +}; + +} // namespace logic diff --git a/tests/test_board.cpp b/tests/test_board.cpp new file mode 100644 index 0000000..babc08a --- /dev/null +++ b/tests/test_board.cpp @@ -0,0 +1,31 @@ +#include "../src/logic/Board.h" +#include +#include + +int main() +{ + using logic::Board; + Board b; + // board starts empty + for (int y = 0; y < Board::Height; ++y) + for (int x = 0; x < Board::Width; ++x) + assert(b.at(x,y) == Board::Cell::Empty); + + // fill a single full row at bottom + int y = Board::Height - 1; + for (int x = 0; x < Board::Width; ++x) b.set(x, y, Board::Cell::Filled); + int cleared = b.clearFullLines(); + assert(cleared == 1); + // bottom row should be empty after clear + for (int x = 0; x < Board::Width; ++x) assert(b.at(x, Board::Height - 1) == Board::Cell::Empty); + + // fill two non-adjacent rows and verify both clear + int y1 = Board::Height - 1; + int y2 = Board::Height - 3; + for (int x = 0; x < Board::Width; ++x) { b.set(x, y1, Board::Cell::Filled); b.set(x, y2, Board::Cell::Filled); } + cleared = b.clearFullLines(); + assert(cleared == 2); + + std::cout << "tests/test_board: all assertions passed\n"; + return 0; +} From 45086e58d8dcb6ce20948251274b86355071dd4f Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Thu, 25 Dec 2025 10:27:35 +0100 Subject: [PATCH 02/10] Add pure game model + GTest board tests and scaffolding Add SDL-free Board model: Board.h, Board.cpp Add unit tests for Board using Google Test: test_board.cpp Integrate test_board into CMake and register with CTest: update CMakeLists.txt Add gtest to vcpkg.json so CMake can find GTest Add high-level refactor plan: plan-spacetrisRefactor.prompt.md Update internal TODOs to mark logic extraction complete This scaffolds deterministic, testable game logic and CI-friendly tests without changing existing runtime behavior. --- CMakeLists.txt | 14 ++++++++++++++ tests/test_board.cpp | 39 +++++++++++++++++++++++---------------- vcpkg.json | 1 + 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e64436a..64e08f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -201,6 +201,20 @@ if(EXISTS "${CMAKE_SOURCE_DIR}/vcpkg_installed/x64-windows/include") target_include_directories(spacetris_tests PRIVATE "${CMAKE_SOURCE_DIR}/vcpkg_installed/x64-windows/include") endif() +# GoogleTest-based board unit tests +find_package(GTest CONFIG REQUIRED) +add_executable(test_board + tests/test_board.cpp + src/logic/Board.cpp +) +target_include_directories(test_board PRIVATE ${CMAKE_SOURCE_DIR}/src) +target_link_libraries(test_board PRIVATE GTest::gtest_main) +add_test(NAME BoardTests COMMAND test_board) + +if(EXISTS "${CMAKE_SOURCE_DIR}/vcpkg_installed/x64-windows/include") + target_include_directories(test_board PRIVATE "${CMAKE_SOURCE_DIR}/vcpkg_installed/x64-windows/include") +endif() + # Add new src subfolders to include path so old #includes continue to work target_include_directories(spacetris PRIVATE ${CMAKE_SOURCE_DIR}/src diff --git a/tests/test_board.cpp b/tests/test_board.cpp index babc08a..3eb941c 100644 --- a/tests/test_board.cpp +++ b/tests/test_board.cpp @@ -1,31 +1,38 @@ #include "../src/logic/Board.h" -#include -#include +#include -int main() +using logic::Board; + +TEST(BoardTests, InitiallyEmpty) { - using logic::Board; Board b; - // board starts empty for (int y = 0; y < Board::Height; ++y) for (int x = 0; x < Board::Width; ++x) - assert(b.at(x,y) == Board::Cell::Empty); + EXPECT_EQ(b.at(x, y), Board::Cell::Empty); +} - // fill a single full row at bottom +TEST(BoardTests, ClearSingleFullLine) +{ + Board b; int y = Board::Height - 1; for (int x = 0; x < Board::Width; ++x) b.set(x, y, Board::Cell::Filled); int cleared = b.clearFullLines(); - assert(cleared == 1); - // bottom row should be empty after clear - for (int x = 0; x < Board::Width; ++x) assert(b.at(x, Board::Height - 1) == Board::Cell::Empty); + EXPECT_EQ(cleared, 1); + for (int x = 0; x < Board::Width; ++x) EXPECT_EQ(b.at(x, Board::Height - 1), Board::Cell::Empty); +} - // fill two non-adjacent rows and verify both clear +TEST(BoardTests, ClearTwoNonAdjacentLines) +{ + Board b; int y1 = Board::Height - 1; int y2 = Board::Height - 3; for (int x = 0; x < Board::Width; ++x) { b.set(x, y1, Board::Cell::Filled); b.set(x, y2, Board::Cell::Filled); } - cleared = b.clearFullLines(); - assert(cleared == 2); - - std::cout << "tests/test_board: all assertions passed\n"; - return 0; + int cleared = b.clearFullLines(); + EXPECT_EQ(cleared, 2); +} + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/vcpkg.json b/vcpkg.json index f93b924..b4a5264 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -8,6 +8,7 @@ }, "enet", "catch2", + "gtest", "cpr", "nlohmann-json", "ffmpeg" From 0b546ce25c2f6efddf1d8e4da0c31f8c1f3117d7 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Thu, 25 Dec 2025 14:23:17 +0100 Subject: [PATCH 03/10] Fixed resource loader --- CMakeLists.txt | 2 ++ src/app/AssetLoader.cpp | 46 ++++++++++++++++++++----------- src/app/AssetLoader.h | 3 ++ src/app/TetrisApp.cpp | 8 ++++++ src/app/TextureLoader.cpp | 26 ++++++++++++++++- src/app/TextureLoader.h | 5 ++++ src/core/assets/AssetManager.cpp | 31 ++++++++++++++++++++- src/renderer/Renderer.h | 20 ++++++++++++++ src/renderer/SDLRenderer.cpp | 46 +++++++++++++++++++++++++++++++ src/resources/ResourceManager.cpp | 41 +++++++++++++++++++++++++++ src/resources/ResourceManager.h | 43 +++++++++++++++++++++++++++++ 11 files changed, 253 insertions(+), 18 deletions(-) create mode 100644 src/renderer/Renderer.h create mode 100644 src/renderer/SDLRenderer.cpp create mode 100644 src/resources/ResourceManager.cpp create mode 100644 src/resources/ResourceManager.h 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 From e2dd768fafb04ed58b7ff74947c05963c3517f1b Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Thu, 25 Dec 2025 14:24:04 +0100 Subject: [PATCH 04/10] fixed gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8bba2aa..e367bca 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,6 @@ dist_package/ .env # Ignore local settings file -settings.ini +/settings.ini # End of .gitignore From 6ef93e4c9c3d4a2ccdca92210f3b7e6d25731995 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Thu, 25 Dec 2025 14:24:46 +0100 Subject: [PATCH 05/10] fixed gitignore --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index e367bca..a9acd58 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ CMakeCache.txt cmake_install.cmake Makefile +settings.ini # vcpkg /vcpkg_installed/ @@ -70,7 +71,4 @@ dist_package/ # Local environment files (if any) .env -# Ignore local settings file -/settings.ini - # End of .gitignore From 17cb64c9d4a003947cffcc2a67e69483df68f8a5 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Thu, 25 Dec 2025 14:39:56 +0100 Subject: [PATCH 06/10] fixed game renderer --- src/renderer/Renderer.h | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/renderer/Renderer.h b/src/renderer/Renderer.h index 2b2827a..e23e0a5 100644 --- a/src/renderer/Renderer.h +++ b/src/renderer/Renderer.h @@ -1,7 +1,8 @@ +// Renderer abstraction (minimal scaffold) #pragma once -#include #include +#include namespace renderer { @@ -9,12 +10,18 @@ class Renderer { public: virtual ~Renderer() = default; - // Wrap common operations used by renderers + // Create/destroy textures virtual SDL_Texture* createTextureFromSurface(SDL_Surface* surf) = 0; virtual void destroyTexture(SDL_Texture* tex) = 0; + + // Draw operations (minimal) 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; }; +// Factory helper implemented by SDL-specific backend +std::unique_ptr MakeSDLRenderer(SDL_Renderer* rdr); + } // namespace renderer + From 03bdc82dc11c25aed78c46145bf099b9cf3819ac Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Thu, 25 Dec 2025 17:26:55 +0100 Subject: [PATCH 07/10] Updated renderer Added Renderer_iface.h as a clean interface. Replaced usages of old/ambiguous SDL calls in SDLRenderer.cpp to call SDL3 APIs: SDL_RenderTexture, SDL_RenderFillRect, SDL_RenderRect, SDL_RenderLine. Converted copy() to call SDL_RenderTexture by converting integer rects to float rects. Updated GameRenderer.cpp to include the new clean interface. --- src/graphics/renderers/GameRenderer.cpp | 116 +++++++++++++----------- src/renderer/Renderer.h | 11 +++ src/renderer/Renderer_iface.h | 27 ++++++ src/renderer/SDLRenderer.cpp | 42 ++++++++- 4 files changed, 141 insertions(+), 55 deletions(-) create mode 100644 src/renderer/Renderer_iface.h diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index ddd4c94..575947b 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -1,4 +1,5 @@ #include "GameRenderer.h" +#include "../../renderer/Renderer_iface.h" #include "SyncLineRenderer.h" #include "../../gameplay/core/Game.h" @@ -248,23 +249,25 @@ static void updateAndDrawTransport(SDL_Renderer* renderer, SDL_Texture* blocksTe Uint8 gridAlpha = static_cast(std::lround(255.0f * t)); Uint8 nextAlpha = gridAlpha; // fade new NEXT preview in at same rate as grid - // Draw preview fade-out - if (previewAlpha > 0) { - if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, previewAlpha); - for (int cy = 0; cy < 4; ++cy) { - for (int cx = 0; cx < 4; ++cx) { - if (!Game::cellFilled(s_transport.piece, cx, cy)) continue; - float px = s_transport.startX + static_cast(cx) * s_transport.tileSize; - float py = s_transport.startY + static_cast(cy) * s_transport.tileSize; - GameRenderer::drawBlockTexturePublic(renderer, blocksTex, px, py, s_transport.tileSize, s_transport.piece.type); + // Create renderer wrapper + auto rwrap = renderer::MakeSDLRenderer(renderer); + // Draw preview fade-out + if (previewAlpha > 0) { + if (blocksTex) rwrap->setTextureAlphaMod(blocksTex, previewAlpha); + for (int cy = 0; cy < 4; ++cy) { + for (int cx = 0; cx < 4; ++cx) { + if (!Game::cellFilled(s_transport.piece, cx, cy)) continue; + float px = s_transport.startX + static_cast(cx) * s_transport.tileSize; + float py = s_transport.startY + static_cast(cy) * s_transport.tileSize; + GameRenderer::drawBlockTexturePublic(renderer, blocksTex, px, py, s_transport.tileSize, s_transport.piece.type); + } } + if (blocksTex) rwrap->setTextureAlphaMod(blocksTex, 255); } - if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, 255); - } // Draw grid fade-in (same intensity as next preview fade-in) if (gridAlpha > 0) { - if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, gridAlpha); + if (blocksTex) rwrap->setTextureAlphaMod(blocksTex, gridAlpha); for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (!Game::cellFilled(s_transport.piece, cx, cy)) continue; @@ -273,12 +276,12 @@ static void updateAndDrawTransport(SDL_Renderer* renderer, SDL_Texture* blocksTe GameRenderer::drawBlockTexturePublic(renderer, blocksTex, gx, gy, s_transport.tileSize, s_transport.piece.type); } } - if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, 255); + if (blocksTex) rwrap->setTextureAlphaMod(blocksTex, 255); } // Draw new NEXT preview fade-in (simultaneous) if (nextAlpha > 0) { - if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, nextAlpha); + if (blocksTex) rwrap->setTextureAlphaMod(blocksTex, nextAlpha); for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (!Game::cellFilled(s_transport.nextPiece, cx, cy)) continue; @@ -287,7 +290,7 @@ static void updateAndDrawTransport(SDL_Renderer* renderer, SDL_Texture* blocksTe GameRenderer::drawBlockTexturePublic(renderer, blocksTex, nx, ny, s_transport.tileSize, s_transport.nextPiece.type); } } - if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, 255); + if (blocksTex) rwrap->setTextureAlphaMod(blocksTex, 255); } if (t >= 1.0f) { @@ -308,16 +311,18 @@ static const SDL_Color COLORS[] = { }; void GameRenderer::drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color c) { - SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); + auto rwrap = renderer::MakeSDLRenderer(renderer); + rwrap->setDrawColor(c); SDL_FRect fr{x, y, w, h}; - SDL_RenderFillRect(renderer, &fr); + rwrap->fillRectF(&fr); } static void drawAsteroid(SDL_Renderer* renderer, SDL_Texture* asteroidTex, float x, float y, float size, const AsteroidCell& cell) { auto outlineGravity = [&](float inset, SDL_Color color) { - SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); + auto rwrap = renderer::MakeSDLRenderer(renderer); + rwrap->setDrawColor(color); SDL_FRect glow{ x + inset, y + inset, size - inset * 2.0f, size - inset * 2.0f }; - SDL_RenderRect(renderer, &glow); + rwrap->drawRectF(&glow); }; if (asteroidTex) { @@ -330,9 +335,10 @@ static void drawAsteroid(SDL_Renderer* renderer, SDL_Texture* asteroidTex, float case AsteroidType::Core: col = 3; break; } int row = std::clamp(cell.visualState, 0, 2); + auto rwrap = renderer::MakeSDLRenderer(renderer); SDL_FRect src{ col * SPRITE_SIZE, row * SPRITE_SIZE, SPRITE_SIZE, SPRITE_SIZE }; SDL_FRect dst{ x, y, size, size }; - SDL_RenderTexture(renderer, asteroidTex, &src, &dst); + rwrap->renderTexture(asteroidTex, &src, &dst); if (cell.gravityEnabled) { outlineGravity(2.0f, SDL_Color{255, 230, 120, 180}); @@ -355,15 +361,16 @@ static void drawAsteroid(SDL_Renderer* renderer, SDL_Texture* asteroidTex, float static_cast(base.b * hpScale + 40 * (1.0f - hpScale)), 255 }; - SDL_SetRenderDrawColor(renderer, fill.r, fill.g, fill.b, fill.a); + auto rwrap = renderer::MakeSDLRenderer(renderer); + rwrap->setDrawColor(fill); SDL_FRect body{x, y, size - 1.0f, size - 1.0f}; - SDL_RenderFillRect(renderer, &body); + rwrap->fillRectF(&body); SDL_Color outline = base; outline.a = 220; SDL_FRect border{x + 1.0f, y + 1.0f, size - 2.0f, size - 2.0f}; - SDL_SetRenderDrawColor(renderer, outline.r, outline.g, outline.b, outline.a); - SDL_RenderRect(renderer, &border); + rwrap->setDrawColor(outline); + rwrap->drawRectF(&border); if (cell.gravityEnabled) { outlineGravity(2.0f, SDL_Color{255, 230, 120, 180}); } @@ -387,7 +394,8 @@ void GameRenderer::drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksT SDL_FRect srcRect = {srcX, srcY, srcW, srcH}; SDL_FRect dstRect = {x, y, size, size}; - SDL_RenderTexture(renderer, blocksTex, &srcRect, &dstRect); + auto rwrap = renderer::MakeSDLRenderer(renderer); + rwrap->renderTexture(blocksTex, &srcRect, &dstRect); } void GameRenderer::drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost, float pixelOffsetX, float pixelOffsetY) { @@ -403,14 +411,17 @@ void GameRenderer::drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, con SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); // Draw ghost piece as barely visible gray outline - SDL_SetRenderDrawColor(renderer, 180, 180, 180, 20); // Very faint gray + auto rwrap = renderer::MakeSDLRenderer(renderer); + // Draw ghost fill + SDL_Color ghostFill{180,180,180,20}; + rwrap->setDrawColor(ghostFill); SDL_FRect rect = {px + 2, py + 2, tileSize - 4, tileSize - 4}; - SDL_RenderFillRect(renderer, &rect); - + rwrap->fillRectF(&rect); // Draw thin gray border - SDL_SetRenderDrawColor(renderer, 180, 180, 180, 30); + SDL_Color ghostBorder{180,180,180,30}; + rwrap->setDrawColor(ghostBorder); SDL_FRect border = {px + 1, py + 1, tileSize - 2, tileSize - 2}; - SDL_RenderRect(renderer, &border); + rwrap->drawRectF(&border); } else { drawBlockTexture(renderer, blocksTex, px, py, tileSize, piece.type); } @@ -426,6 +437,7 @@ void GameRenderer::drawBlockTexturePublic(SDL_Renderer* renderer, SDL_Texture* b void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize) { if (pieceType >= PIECE_COUNT) return; + auto rwrap = renderer::MakeSDLRenderer(renderer); // Use the first rotation (index 0) for preview Game::Piece previewPiece; @@ -461,7 +473,7 @@ void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex // Use semi-transparent alpha for preview blocks Uint8 previewAlpha = 180; if (blocksTex) { - SDL_SetTextureAlphaMod(blocksTex, previewAlpha); + rwrap->setTextureAlphaMod(blocksTex, previewAlpha); } for (int cy = 0; cy < 4; ++cy) { @@ -476,7 +488,7 @@ void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex // Reset alpha if (blocksTex) { - SDL_SetTextureAlphaMod(blocksTex, 255); + rwrap->setTextureAlphaMod(blocksTex, 255); } } @@ -496,6 +508,8 @@ void GameRenderer::renderNextPanel( return; } + auto rwrap = renderer::MakeSDLRenderer(renderer); + const SDL_Color gridBorderColor{60, 80, 160, 255}; // matches main grid outline const SDL_Color bayColor{8, 12, 24, 235}; const SDL_Color bayOutline{25, 62, 86, 220}; @@ -505,25 +519,24 @@ void GameRenderer::renderNextPanel( // the panel rectangle and skip the custom background/frame drawing. if (nextPanelTex) { SDL_FRect dst{panelX, panelY, panelW, panelH}; - SDL_RenderTexture(renderer, nextPanelTex, nullptr, &dst); - // Draw the panel label over the texture — user requested visible label + rwrap->renderTexture(nextPanelTex, nullptr, &dst); const float labelPad = tileSize * 0.25f; pixelFont->draw(renderer, panelX + labelPad, panelY + labelPad * 0.5f, "NEXT", 0.9f, labelColor); } else { SDL_FRect bayRect{panelX, panelY, panelW, panelH}; - SDL_SetRenderDrawColor(renderer, bayColor.r, bayColor.g, bayColor.b, bayColor.a); - SDL_RenderFillRect(renderer, &bayRect); + rwrap->setDrawColor(bayColor); + rwrap->fillRectF(&bayRect); SDL_FRect thinOutline{panelX - 1.0f, panelY - 1.0f, panelW + 2.0f, panelH + 2.0f}; auto drawOutlineNoBottom = [&](const SDL_FRect& rect, SDL_Color color) { - SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); + rwrap->setDrawColor(color); const float left = rect.x; const float top = rect.y; const float right = rect.x + rect.w; const float bottom = rect.y + rect.h; - SDL_RenderLine(renderer, left, top, right, top); // top edge - SDL_RenderLine(renderer, left, top, left, bottom); // left edge - SDL_RenderLine(renderer, right, top, right, bottom); // right edge + rwrap->renderLine(left, top, right, top); // top edge + rwrap->renderLine(left, top, left, bottom); // left edge + rwrap->renderLine(right, top, right, bottom); // right edge }; drawOutlineNoBottom(thinOutline, gridBorderColor); @@ -641,11 +654,12 @@ void GameRenderer::renderPlayingState( float contentOffsetX = (winW - contentW) * 0.5f / contentScale; float contentOffsetY = (winH - contentH) * 0.5f / contentScale; - // Helper lambda for drawing rectangles with content offset + // Renderer wrapper and helper lambda for drawing rectangles with content offset + auto rwrap = renderer::MakeSDLRenderer(renderer); auto drawRectWithOffset = [&](float x, float y, float w, float h, SDL_Color c) { - SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); + rwrap->setDrawColor(c); SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h}; - SDL_RenderFillRect(renderer, &fr); + rwrap->fillRectF(&fr); }; // Responsive layout that scales with window size while maintaining margins @@ -747,28 +761,28 @@ void GameRenderer::renderPlayingState( scaledW, scaledH }; - SDL_RenderTexture(renderer, statisticsPanelTex, nullptr, &dstF); + rwrap->renderTexture(statisticsPanelTex, nullptr, &dstF); } } else { // Fallback: render entire texture stretched to panel - SDL_RenderTexture(renderer, statisticsPanelTex, nullptr, &blocksPanelBg); + rwrap->renderTexture(statisticsPanelTex, nullptr, &blocksPanelBg); } } else if (scorePanelTex) { - SDL_RenderTexture(renderer, scorePanelTex, nullptr, &blocksPanelBg); + rwrap->renderTexture(scorePanelTex, nullptr, &blocksPanelBg); } else { - SDL_SetRenderDrawColor(renderer, 12, 18, 32, 205); - SDL_RenderFillRect(renderer, &blocksPanelBg); + rwrap->setDrawColor(SDL_Color{12, 18, 32, 205}); + rwrap->fillRectF(&blocksPanelBg); } // Draw grid lines - SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255); + rwrap->setDrawColor(SDL_Color{40, 45, 60, 255}); for (int x = 1; x < Game::COLS; ++x) { float lineX = gridX + x * finalBlockSize; - SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H); + rwrap->renderLine(lineX, gridY, lineX, gridY + GRID_H); } for (int y = 1; y < Game::ROWS; ++y) { float lineY = gridY + y * finalBlockSize; - SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY); + rwrap->renderLine(gridX, lineY, gridX + GRID_W, lineY); } if (!s_starfieldInitialized) { diff --git a/src/renderer/Renderer.h b/src/renderer/Renderer.h index e23e0a5..5a4f5e2 100644 --- a/src/renderer/Renderer.h +++ b/src/renderer/Renderer.h @@ -15,8 +15,19 @@ public: virtual void destroyTexture(SDL_Texture* tex) = 0; // Draw operations (minimal) + // Copy a texture (integer rects) virtual void copy(SDL_Texture* tex, const SDL_Rect* src, const SDL_Rect* dst) = 0; + // Copy a texture using floating-point rects (SDL_FRect) + virtual void renderTexture(SDL_Texture* tex, const SDL_FRect* src, const SDL_FRect* dst) = 0; + // Set alpha modulation on a texture + virtual void setTextureAlphaMod(SDL_Texture* tex, Uint8 a) = 0; + // Draw a line (floating-point coordinates) + virtual void renderLine(float x1, float y1, float x2, float y2) = 0; + // Set draw color and draw filled/floating rects virtual void clear(const SDL_Color& color) = 0; + virtual void setDrawColor(const SDL_Color& color) = 0; + virtual void fillRectF(const SDL_FRect* rect) = 0; + virtual void drawRectF(const SDL_FRect* rect) = 0; virtual void present() = 0; }; diff --git a/src/renderer/Renderer_iface.h b/src/renderer/Renderer_iface.h new file mode 100644 index 0000000..8aa98dd --- /dev/null +++ b/src/renderer/Renderer_iface.h @@ -0,0 +1,27 @@ +// Clean renderer interface for local use +#pragma once + +#include +#include + +namespace renderer { + +class Renderer { +public: + virtual ~Renderer() = default; + 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 renderTexture(SDL_Texture* tex, const SDL_FRect* src, const SDL_FRect* dst) = 0; + virtual void setTextureAlphaMod(SDL_Texture* tex, Uint8 a) = 0; + virtual void renderLine(float x1, float y1, float x2, float y2) = 0; + virtual void clear(const SDL_Color& color) = 0; + virtual void setDrawColor(const SDL_Color& color) = 0; + virtual void fillRectF(const SDL_FRect* rect) = 0; + virtual void drawRectF(const SDL_FRect* rect) = 0; + virtual void present() = 0; +}; + +std::unique_ptr MakeSDLRenderer(SDL_Renderer* rdr); + +} // namespace renderer diff --git a/src/renderer/SDLRenderer.cpp b/src/renderer/SDLRenderer.cpp index e66e199..accd957 100644 --- a/src/renderer/SDLRenderer.cpp +++ b/src/renderer/SDLRenderer.cpp @@ -1,4 +1,4 @@ -#include "Renderer.h" +#include "Renderer_iface.h" #include namespace renderer { @@ -16,11 +16,25 @@ public: 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)"); + // Convert integer rects to float rects and call SDL_RenderTexture (SDL3 API) + SDL_FRect fs{}; SDL_FRect fd{}; + const SDL_FRect* ps = nullptr; + const SDL_FRect* pd = nullptr; + if (src) { fs.x = static_cast(src->x); fs.y = static_cast(src->y); fs.w = static_cast(src->w); fs.h = static_cast(src->h); ps = &fs; } + if (dst) { fd.x = static_cast(dst->x); fd.y = static_cast(dst->y); fd.w = static_cast(dst->w); fd.h = static_cast(dst->h); pd = &fd; } + SDL_RenderTexture(rdr_, tex, ps, pd); + } + + void renderTexture(SDL_Texture* tex, const SDL_FRect* src, const SDL_FRect* dst) override { + if (!rdr_ || !tex) return; + SDL_RenderTexture(rdr_, tex, src, dst); + } + + void setTextureAlphaMod(SDL_Texture* tex, Uint8 a) override { + if (!tex) return; + SDL_SetTextureAlphaMod(tex, a); } void clear(const SDL_Color& color) override { @@ -29,6 +43,26 @@ public: SDL_RenderClear(rdr_); } + void setDrawColor(const SDL_Color& color) override { + if (!rdr_) return; + SDL_SetRenderDrawColor(rdr_, color.r, color.g, color.b, color.a); + } + + void fillRectF(const SDL_FRect* rect) override { + if (!rdr_ || !rect) return; + SDL_RenderFillRect(rdr_, rect); + } + + void drawRectF(const SDL_FRect* rect) override { + if (!rdr_ || !rect) return; + SDL_RenderRect(rdr_, rect); + } + + void renderLine(float x1, float y1, float x2, float y2) override { + if (!rdr_) return; + SDL_RenderLine(rdr_, x1, y1, x2, y2); + } + void present() override { if (!rdr_) return; SDL_RenderPresent(rdr_); From 938988c876610b99aa14ccb53cac41613090e9f1 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Thu, 25 Dec 2025 18:23:19 +0100 Subject: [PATCH 08/10] fixed --- src/graphics/renderers/GameRenderer.cpp | 161 ++++++++++++------------ 1 file changed, 83 insertions(+), 78 deletions(-) diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index 575947b..99c5553 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -831,6 +831,7 @@ void GameRenderer::renderPlayingState( SDL_BlendMode oldBlend = SDL_BLENDMODE_NONE; SDL_GetRenderDrawBlendMode(renderer, &oldBlend); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + // rwrap already declared near function start; reuse it here. // Add a small, smooth sub-pixel jitter to the starfield origin so the // brightest star doesn't permanently sit exactly at the visual center. { @@ -947,10 +948,10 @@ void GameRenderer::renderPlayingState( float pulse = 0.5f + 0.5f * std::sin(sp.pulse); Uint8 alpha = static_cast(std::clamp(lifeRatio * pulse, 0.0f, 1.0f) * 255.0f); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); - SDL_SetRenderDrawColor(renderer, sp.color.r, sp.color.g, sp.color.b, alpha); + rwrap->setDrawColor(SDL_Color{sp.color.r, sp.color.g, sp.color.b, alpha}); float half = sp.size * 0.5f; SDL_FRect fr{gridX + sp.x - half, gridY + sp.y - half, sp.size, sp.size}; - SDL_RenderFillRect(renderer, &fr); + rwrap->fillRectF(&fr); ++it; } @@ -963,11 +964,11 @@ void GameRenderer::renderPlayingState( // Draw a small filled connector to visually merge NEXT panel and grid border // If an external NEXT panel texture is used, skip the connector to avoid // drawing a visible seam under the image/artwork. - if (!nextPanelTex) { - SDL_SetRenderDrawColor(renderer, 60, 80, 160, 255); // same as grid border + if (!nextPanelTex) { + rwrap->setDrawColor(SDL_Color{60, 80, 160, 255}); // same as grid border float connectorY = NEXT_PANEL_Y + NEXT_PANEL_HEIGHT; // bottom of next panel (near grid top) SDL_FRect connRect{ NEXT_PANEL_X, connectorY - 1.0f, NEXT_PANEL_WIDTH, 2.0f }; - SDL_RenderFillRect(renderer, &connRect); + rwrap->fillRectF(&connRect); } // Draw transport effect if active (renders the moving piece and trail) @@ -1178,27 +1179,27 @@ void GameRenderer::renderPlayingState( } if (asteroidsTex && spawnAlpha < 1.0f) { - SDL_SetTextureAlphaMod(asteroidsTex, static_cast(std::clamp(spawnAlpha, 0.0f, 1.0f) * 255.0f)); + rwrap->setTextureAlphaMod(asteroidsTex, static_cast(std::clamp(spawnAlpha, 0.0f, 1.0f) * 255.0f)); } float size = finalBlockSize * spawnScale * clearScale; float offset = (finalBlockSize - size) * 0.5f; if (asteroidsTex && clearAlpha < 1.0f) { Uint8 alpha = static_cast(std::clamp(spawnAlpha * clearAlpha, 0.0f, 1.0f) * 255.0f); - SDL_SetTextureAlphaMod(asteroidsTex, alpha); + rwrap->setTextureAlphaMod(asteroidsTex, alpha); } drawAsteroid(renderer, asteroidsTex, bx + offset, by + offset, size, cell); if (asteroidsTex && (spawnAlpha < 1.0f || clearAlpha < 1.0f)) { - SDL_SetTextureAlphaMod(asteroidsTex, 255); + rwrap->setTextureAlphaMod(asteroidsTex, 255); } } else { if (blocksTex && clearAlpha < 1.0f) { - SDL_SetTextureAlphaMod(blocksTex, static_cast(std::clamp(clearAlpha, 0.0f, 1.0f) * 255.0f)); + rwrap->setTextureAlphaMod(blocksTex, static_cast(std::clamp(clearAlpha, 0.0f, 1.0f) * 255.0f)); } drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize * clearScale, v - 1); if (blocksTex && clearAlpha < 1.0f) { - SDL_SetTextureAlphaMod(blocksTex, 255); + rwrap->setTextureAlphaMod(blocksTex, 255); } } } @@ -1223,7 +1224,7 @@ void GameRenderer::renderPlayingState( s.y += s.vy * sparkDeltaMs; float lifeRatio = std::clamp(static_cast(s.lifeMs / s.maxLifeMs), 0.0f, 1.0f); Uint8 alpha = static_cast(lifeRatio * 200.0f); - SDL_SetRenderDrawColor(renderer, s.color.r, s.color.g, s.color.b, alpha); + rwrap->setDrawColor(SDL_Color{s.color.r, s.color.g, s.color.b, alpha}); float size = s.size * (0.7f + (1.0f - lifeRatio) * 0.8f); SDL_FRect shardRect{ s.x - size * 0.5f, @@ -1231,7 +1232,7 @@ void GameRenderer::renderPlayingState( size, size * 1.4f }; - SDL_RenderFillRect(renderer, &shardRect); + rwrap->fillRectF(&shardRect); ++shardIt; } @@ -1252,14 +1253,14 @@ void GameRenderer::renderPlayingState( SDL_Color c = b.color; Uint8 a = static_cast(alpha * 220.0f); - SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, a); + rwrap->setDrawColor(SDL_Color{c.r, c.g, c.b, a}); SDL_FRect outer{ b.x - radius + jitter, b.y - radius + jitter, radius * 2.0f, radius * 2.0f }; - SDL_RenderRect(renderer, &outer); + rwrap->drawRectF(&outer); SDL_FRect inner{ b.x - (radius - thickness), @@ -1267,8 +1268,8 @@ void GameRenderer::renderPlayingState( (radius - thickness) * 2.0f, (radius - thickness) * 2.0f }; - SDL_SetRenderDrawColor(renderer, 255, 255, 255, static_cast(a * 0.9f)); - SDL_RenderRect(renderer, &inner); + rwrap->setDrawColor(SDL_Color{255, 255, 255, static_cast(a * 0.9f)}); + rwrap->drawRectF(&inner); ++it; } SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); @@ -1289,14 +1290,14 @@ void GameRenderer::renderPlayingState( } float lifeRatio = spark.lifeMs / spark.maxLifeMs; Uint8 alpha = static_cast(std::clamp(lifeRatio, 0.0f, 1.0f) * 160.0f); - SDL_SetRenderDrawColor(renderer, spark.color.r, spark.color.g, spark.color.b, alpha); + rwrap->setDrawColor(SDL_Color{spark.color.r, spark.color.g, spark.color.b, alpha}); SDL_FRect sparkRect{ spark.x - spark.size * 0.5f, spark.y - spark.size * 0.5f, spark.size, spark.size * 1.4f }; - SDL_RenderFillRect(renderer, &sparkRect); + rwrap->fillRectF(&sparkRect); ++it; } } @@ -1540,9 +1541,9 @@ void GameRenderer::renderPlayingState( float barW = numbersW; float barY = numbersY + numbersH + 8.0f; - SDL_SetRenderDrawColor(renderer, 24, 80, 120, 220); + rwrap->setDrawColor(SDL_Color{24, 80, 120, 220}); SDL_FRect track{barX, barY, barW, barHeight}; - SDL_RenderFillRect(renderer, &track); + rwrap->fillRectF(&track); // Fill color brightness based on usage and highlight for top piece float strength = (totalBlocks > 0) ? (float(blockCounts[i]) / float(totalBlocks)) : 0.0f; @@ -1556,9 +1557,9 @@ void GameRenderer::renderPlayingState( }; float fillW = barW * std::clamp(strength, 0.0f, 1.0f); - SDL_SetRenderDrawColor(renderer, fillC.r, fillC.g, fillC.b, fillC.a); + rwrap->setDrawColor(SDL_Color{fillC.r, fillC.g, fillC.b, fillC.a}); SDL_FRect fill{barX, barY, fillW, barHeight}; - SDL_RenderFillRect(renderer, &fill); + rwrap->fillRectF(&fill); // Advance cursor to next row: after bar + gap (leave more space between blocks) yCursor = barY + barHeight + rowGap + 6.0f; @@ -1733,10 +1734,10 @@ void GameRenderer::renderPlayingState( SDL_FRect statsBg{statsPanelLeft, statsPanelTop, statsPanelWidth, statsPanelHeight}; if (scorePanelTex) { - SDL_RenderTexture(renderer, scorePanelTex, nullptr, &statsBg); + rwrap->renderTexture(scorePanelTex, nullptr, &statsBg); } else { - SDL_SetRenderDrawColor(renderer, 12, 18, 32, 205); - SDL_RenderFillRect(renderer, &statsBg); + rwrap->setDrawColor(SDL_Color{12, 18, 32, 205}); + rwrap->fillRectF(&statsBg); } scorePanelMetricsValid = true; @@ -1824,12 +1825,12 @@ void GameRenderer::renderPlayingState( SDL_FRect panelDst{panelX, panelY, panelW, panelH}; SDL_SetTextureBlendMode(holdPanelTex, SDL_BLENDMODE_BLEND); SDL_SetTextureScaleMode(holdPanelTex, SDL_SCALEMODE_LINEAR); - SDL_RenderTexture(renderer, holdPanelTex, nullptr, &panelDst); + rwrap->renderTexture(holdPanelTex, nullptr, &panelDst); } else { // fallback: draw a dark panel rect so UI is visible even without texture - SDL_SetRenderDrawColor(renderer, 12, 18, 32, 220); + rwrap->setDrawColor(SDL_Color{12, 18, 32, 220}); SDL_FRect panelDst{panelX, panelY, panelW, panelH}; - SDL_RenderFillRect(renderer, &panelDst); + rwrap->fillRectF(&panelDst); } // Display "HOLD" label on right side @@ -1868,6 +1869,8 @@ void GameRenderer::renderCoopPlayingState( ) { if (!renderer || !game || !pixelFont) return; + auto rwrap = renderer::MakeSDLRenderer(renderer); + static SyncLineRenderer s_syncLine; static bool s_lastHadCompletedLines = false; @@ -1906,9 +1909,9 @@ void GameRenderer::renderCoopPlayingState( float contentOffsetY = (winH - contentH) * 0.5f / contentScale; auto drawRectWithOffset = [&](float x, float y, float w, float h, SDL_Color c) { - SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); + rwrap->setDrawColor(c); SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h}; - SDL_RenderFillRect(renderer, &fr); + rwrap->fillRectF(&fr); }; static constexpr float COOP_GAP_PX = 20.0f; @@ -1981,19 +1984,19 @@ void GameRenderer::renderCoopPlayingState( }; // Grid lines (draw per-half so the gap is clean) - SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255); + rwrap->setDrawColor(SDL_Color{40, 45, 60, 255}); for (int x = 1; x < 10; ++x) { float lineX = gridX + x * finalBlockSize; - SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H); + rwrap->renderLine(lineX, gridY, lineX, gridY + GRID_H); } for (int x = 1; x < 10; ++x) { float lineX = gridX + HALF_W + COOP_GAP_PX + x * finalBlockSize; - SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H); + rwrap->renderLine(lineX, gridY, lineX, gridY + GRID_H); } for (int y = 1; y < CoopGame::ROWS; ++y) { float lineY = gridY + y * finalBlockSize; - SDL_RenderLine(renderer, gridX, lineY, gridX + HALF_W, lineY); - SDL_RenderLine(renderer, gridX + HALF_W + COOP_GAP_PX, lineY, gridX + HALF_W + COOP_GAP_PX + HALF_W, lineY); + rwrap->renderLine(gridX, lineY, gridX + HALF_W, lineY); + rwrap->renderLine(gridX + HALF_W + COOP_GAP_PX, lineY, gridX + HALF_W + COOP_GAP_PX + HALF_W, lineY); } // In-grid 3D starfield + ambient sparkles (match classic feel, per-half) @@ -2178,10 +2181,10 @@ void GameRenderer::renderCoopPlayingState( float pulse = 0.5f + 0.5f * std::sin(sp.pulse); Uint8 alpha = static_cast(std::clamp(lifeRatio * pulse, 0.0f, 1.0f) * 255.0f); - SDL_SetRenderDrawColor(renderer, sp.color.r, sp.color.g, sp.color.b, alpha); + rwrap->setDrawColor(SDL_Color{sp.color.r, sp.color.g, sp.color.b, alpha}); float half = sp.size * 0.5f; SDL_FRect fr{ originX + sp.x - half, gridY + sp.y - half, sp.size, sp.size }; - SDL_RenderFillRect(renderer, &fr); + rwrap->fillRectF(&fr); ++it; } } @@ -2200,14 +2203,14 @@ void GameRenderer::renderCoopPlayingState( } float lifeRatio = spark.lifeMs / spark.maxLifeMs; Uint8 alpha = static_cast(std::clamp(lifeRatio, 0.0f, 1.0f) * 160.0f); - SDL_SetRenderDrawColor(renderer, spark.color.r, spark.color.g, spark.color.b, alpha); + rwrap->setDrawColor(SDL_Color{spark.color.r, spark.color.g, spark.color.b, alpha}); SDL_FRect sparkRect{ spark.x - spark.size * 0.5f, spark.y - spark.size * 0.5f, spark.size, spark.size * 1.4f }; - SDL_RenderFillRect(renderer, &sparkRect); + rwrap->fillRectF(&sparkRect); ++it; } } @@ -2239,17 +2242,17 @@ void GameRenderer::renderCoopPlayingState( } if (rs.leftFull && rs.rightFull) { - SDL_SetRenderDrawColor(renderer, 140, 210, 255, 45); + rwrap->setDrawColor(SDL_Color{140, 210, 255, 45}); SDL_FRect frL{gridX, rowY, HALF_W, finalBlockSize}; - SDL_RenderFillRect(renderer, &frL); + rwrap->fillRectF(&frL); SDL_FRect frR{gridX + HALF_W + COOP_GAP_PX, rowY, HALF_W, finalBlockSize}; - SDL_RenderFillRect(renderer, &frR); + rwrap->fillRectF(&frR); } else if (rs.leftFull ^ rs.rightFull) { - SDL_SetRenderDrawColor(renderer, 90, 140, 220, 35); + rwrap->setDrawColor(SDL_Color{90, 140, 220, 35}); float w = HALF_W; float x = rs.leftFull ? gridX : (gridX + HALF_W + COOP_GAP_PX); SDL_FRect fr{x, rowY, w, finalBlockSize}; - SDL_RenderFillRect(renderer, &fr); + rwrap->fillRectF(&fr); } } SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); @@ -2446,7 +2449,7 @@ void GameRenderer::renderCoopPlayingState( float elapsed = static_cast(nowTicks - sf.startTick); float t = sf.durationMs <= 0.0f ? 1.0f : std::clamp(elapsed / sf.durationMs, 0.0f, 1.0f); Uint8 alpha = static_cast(std::lround(255.0f * t)); - if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, alpha); + if (blocksTex) rwrap->setTextureAlphaMod(blocksTex, alpha); int minCy = 4; int maxCy = -1; @@ -2493,7 +2496,7 @@ void GameRenderer::renderCoopPlayingState( drawBlockTexturePublic(renderer, blocksTex, px, py, sf.tileSize, livePiece.type); } } - if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, 255); + if (blocksTex) rwrap->setTextureAlphaMod(blocksTex, 255); // End fade after duration, but never stop while we are pinning (otherwise // I can briefly disappear until it becomes visible in the real grid). @@ -2513,12 +2516,12 @@ void GameRenderer::renderCoopPlayingState( float py = gridY + (float)pyIdx * finalBlockSize + offsets.second; if (isGhost) { SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); - SDL_SetRenderDrawColor(renderer, 180, 180, 180, 20); + rwrap->setDrawColor(SDL_Color{180, 180, 180, 20}); SDL_FRect rect = {px + 2.0f, py + 2.0f, finalBlockSize - 4.0f, finalBlockSize - 4.0f}; - SDL_RenderFillRect(renderer, &rect); - SDL_SetRenderDrawColor(renderer, 180, 180, 180, 30); + rwrap->fillRectF(&rect); + rwrap->setDrawColor(SDL_Color{180, 180, 180, 30}); SDL_FRect border = {px + 1.0f, py + 1.0f, finalBlockSize - 2.0f, finalBlockSize - 2.0f}; - SDL_RenderRect(renderer, &border); + rwrap->drawRectF(&border); } else { drawBlockTexturePublic(renderer, blocksTex, px, py, finalBlockSize, p.type); } @@ -2593,7 +2596,7 @@ void GameRenderer::renderCoopPlayingState( auto drawNextPanel = [&](float panelX, float panelY, const CoopGame::Piece& piece) { SDL_FRect panel{ panelX, panelY, nextPanelW, nextPanelH }; if (nextPanelTex) { - SDL_RenderTexture(renderer, nextPanelTex, nullptr, &panel); + rwrap->renderTexture(nextPanelTex, nullptr, &panel); } else { drawRectWithOffset(panel.x - contentOffsetX, panel.y - contentOffsetY, panel.w, panel.h, SDL_Color{18,22,30,200}); } @@ -2721,10 +2724,10 @@ void GameRenderer::renderCoopPlayingState( float panelX = (side == CoopGame::PlayerSide::Right) ? (columnRightX - panelW) : columnLeftX; SDL_FRect panelBg{ panelX, panelY, panelW, panelH }; if (scorePanelTex) { - SDL_RenderTexture(renderer, scorePanelTex, nullptr, &panelBg); + rwrap->renderTexture(scorePanelTex, nullptr, &panelBg); } else { - SDL_SetRenderDrawColor(renderer, 12, 18, 32, 205); - SDL_RenderFillRect(renderer, &panelBg); + rwrap->setDrawColor(SDL_Color{12, 18, 32, 205}); + rwrap->fillRectF(&panelBg); } float textDrawX = panelX + statsPanelPadLeft; @@ -2791,9 +2794,10 @@ void GameRenderer::renderExitPopup( SDL_SetRenderViewport(renderer, nullptr); SDL_SetRenderScale(renderer, 1.0f, 1.0f); - SDL_SetRenderDrawColor(renderer, 2, 4, 12, 210); + auto rwrap = renderer::MakeSDLRenderer(renderer); + rwrap->setDrawColor(SDL_Color{2, 4, 12, 210}); SDL_FRect fullWin{0.0f, 0.0f, winW, winH}; - SDL_RenderFillRect(renderer, &fullWin); + rwrap->fillRectF(&fullWin); const float scale = std::max(0.8f, logicalScale); const float panelW = 740.0f * scale; @@ -2811,8 +2815,8 @@ void GameRenderer::renderExitPopup( panel.w + 4.0f * scale, panel.h + 4.0f * scale }; - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140); - SDL_RenderFillRect(renderer, &shadow); + rwrap->setDrawColor(SDL_Color{0, 0, 0, 140}); + rwrap->fillRectF(&shadow); const std::array panelLayers{ SDL_Color{7, 10, 22, 255}, @@ -2828,12 +2832,12 @@ void GameRenderer::renderExitPopup( panel.h - inset * 2.0f }; SDL_Color c = panelLayers[i]; - SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); - SDL_RenderFillRect(renderer, &layer); + rwrap->setDrawColor(c); + rwrap->fillRectF(&layer); } - SDL_SetRenderDrawColor(renderer, 60, 90, 150, 255); - SDL_RenderRect(renderer, &panel); + rwrap->setDrawColor(SDL_Color{60, 90, 150, 255}); + rwrap->drawRectF(&panel); SDL_FRect insetFrame{ panel.x + 10.0f * scale, @@ -2841,8 +2845,8 @@ void GameRenderer::renderExitPopup( panel.w - 20.0f * scale, panel.h - 20.0f * scale }; - SDL_SetRenderDrawColor(renderer, 24, 45, 84, 255); - SDL_RenderRect(renderer, &insetFrame); + rwrap->setDrawColor(SDL_Color{24, 45, 84, 255}); + rwrap->drawRectF(&insetFrame); const float contentPad = 44.0f * scale; float textX = panel.x + contentPad; @@ -2856,9 +2860,9 @@ void GameRenderer::renderExitPopup( pixelFont->draw(renderer, textX, cursorY, title, titleScale, SDL_Color{255, 224, 130, 255}); cursorY += titleH + 18.0f * scale; - SDL_SetRenderDrawColor(renderer, 32, 64, 110, 210); + rwrap->setDrawColor(SDL_Color{32, 64, 110, 210}); SDL_FRect divider{textX, cursorY, contentWidth, 2.0f * scale}; - SDL_RenderFillRect(renderer, ÷r); + rwrap->fillRectF(÷r); cursorY += 26.0f * scale; const std::array lines{ @@ -2899,29 +2903,29 @@ void GameRenderer::renderExitPopup( SDL_Color border = selected ? SDL_Color{255, 225, 150, 255} : SDL_Color{90, 120, 170, 255}; SDL_Color topEdge = SDL_Color{Uint8(std::min(255, body.r + 20)), Uint8(std::min(255, body.g + 20)), Uint8(std::min(255, body.b + 20)), 255}; - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 110); + rwrap->setDrawColor(SDL_Color{0, 0, 0, 110}); SDL_FRect btnShadow{btn.x + 6.0f * scale, btn.y + 8.0f * scale, btn.w, btn.h}; - SDL_RenderFillRect(renderer, &btnShadow); + rwrap->fillRectF(&btnShadow); - SDL_SetRenderDrawColor(renderer, body.r, body.g, body.b, body.a); - SDL_RenderFillRect(renderer, &btn); + rwrap->setDrawColor(body); + rwrap->fillRectF(&btn); SDL_FRect topStrip{btn.x, btn.y, btn.w, 6.0f * scale}; - SDL_SetRenderDrawColor(renderer, topEdge.r, topEdge.g, topEdge.b, topEdge.a); - SDL_RenderFillRect(renderer, &topStrip); + rwrap->setDrawColor(topEdge); + rwrap->fillRectF(&topStrip); - SDL_SetRenderDrawColor(renderer, border.r, border.g, border.b, border.a); - SDL_RenderRect(renderer, &btn); + rwrap->setDrawColor(border); + rwrap->drawRectF(&btn); if (selected) { - SDL_SetRenderDrawColor(renderer, 255, 230, 160, 90); + rwrap->setDrawColor(SDL_Color{255, 230, 160, 90}); SDL_FRect glow{ btn.x - 6.0f * scale, btn.y - 6.0f * scale, btn.w + 12.0f * scale, btn.h + 12.0f * scale }; - SDL_RenderRect(renderer, &glow); + rwrap->drawRectF(&glow); } const float labelScale = 1.35f * scale; @@ -2962,9 +2966,10 @@ void GameRenderer::renderPauseOverlay( SDL_SetRenderScale(renderer, 1.0f, 1.0f); // Draw full screen overlay (darken) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); + auto rwrap = renderer::MakeSDLRenderer(renderer); + rwrap->setDrawColor(SDL_Color{0, 0, 0, 180}); SDL_FRect pauseOverlay{0, 0, winW, winH}; - SDL_RenderFillRect(renderer, &pauseOverlay); + rwrap->fillRectF(&pauseOverlay); // Draw centered text const char* pausedText = "PAUSED"; From 68b35ea57b32a2522c767de0a55fcc02b2b297d0 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Thu, 25 Dec 2025 19:17:36 +0100 Subject: [PATCH 09/10] Audio update --- CMakeLists.txt | 1 + src/audio/Audio.cpp | 3 ++ src/audio/Audio.h | 42 +++++++-------- src/audio/AudioManager.cpp | 15 ++++++ src/audio/AudioManager.h | 11 ++++ src/audio/SoundEffect.cpp | 5 +- src/core/application/ApplicationManager.cpp | 59 ++++++++++----------- src/core/assets/AssetManager.cpp | 3 +- src/core/assets/AssetManager.h | 4 +- src/core/interfaces/IAudioSystem.h | 26 +++++++++ src/gameplay/LineEffect.cpp | 3 +- src/gameplay/effects/LineEffect.cpp | 3 +- src/states/MenuState.cpp | 3 +- src/states/OptionsState.cpp | 3 +- src/states/VideoState.cpp | 3 +- 15 files changed, 122 insertions(+), 62 deletions(-) create mode 100644 src/audio/AudioManager.cpp create mode 100644 src/audio/AudioManager.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ad14ec..b776fa0 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/audio/AudioManager.cpp src/renderer/SDLRenderer.cpp src/gameplay/effects/LineEffect.cpp src/audio/SoundEffect.cpp diff --git a/src/audio/Audio.cpp b/src/audio/Audio.cpp index 582f7b5..15570b5 100644 --- a/src/audio/Audio.cpp +++ b/src/audio/Audio.cpp @@ -118,6 +118,7 @@ static bool decodeMP3(const std::string& path, std::vector& outPCM, int outCh = static_cast(clientFormat.mChannelsPerFrame); return !outPCM.empty(); } + #else static bool decodeMP3(const std::string& path, std::vector& outPCM, int& outRate, int& outCh){ (void)outPCM; (void)outRate; (void)outCh; (void)path; @@ -184,6 +185,8 @@ void Audio::skipToNextTrack(){ void Audio::toggleMute(){ muted=!muted; } void Audio::setMuted(bool m){ muted=m; } +bool Audio::isMuted() const { return muted; } + void Audio::nextTrack(){ if(tracks.empty()) { current = -1; return; } // Try every track once to find a decodable one diff --git a/src/audio/Audio.h b/src/audio/Audio.h index 35f520a..2e1a176 100644 --- a/src/audio/Audio.h +++ b/src/audio/Audio.h @@ -32,29 +32,27 @@ public: void setSoundVolume(float volume) override; bool isMusicPlaying() const override; - // Existing Audio class methods - bool init(); // initialize backend (MF on Windows) - void addTrack(const std::string& path); // decode MP3 -> PCM16 stereo 44100 - void addTrackAsync(const std::string& path); // add track for background loading - void startBackgroundLoading(); // start background thread for loading - void waitForLoadingComplete(); // wait for all tracks to finish loading - bool isLoadingComplete() const; // check if background loading is done - int getLoadedTrackCount() const; // get number of tracks loaded so far - void shuffle(); // randomize order - void start(); // begin playback - void skipToNextTrack(); // advance to the next music track - void toggleMute(); + // Additional IAudioSystem methods (forwarded to concrete implementation) + bool init() override; + void shutdown() override; + void addTrack(const std::string& path) override; + void addTrackAsync(const std::string& path) override; + void startBackgroundLoading() override; + bool isLoadingComplete() const override; + int getLoadedTrackCount() const override; + void start() override; + void skipToNextTrack() override; + void shuffle() override; + void toggleMute() override; + bool isMuted() const override; void setMuted(bool m); - bool isMuted() const { return muted; } - - // Menu music support - void setMenuTrack(const std::string& path); - void playMenuMusic(); - void playGameMusic(); - - // Queue a sound effect to mix over the music (pcm can be mono/stereo, any rate; will be converted) - void playSfx(const std::vector& pcm, int channels, int rate, float volume); - void shutdown(); + void setMenuTrack(const std::string& path) override; + void playMenuMusic() override; + void playGameMusic() override; + void playSfx(const std::vector& pcm, int channels, int rate, float volume) override; + + // Existing Audio class helper methods + void waitForLoadingComplete(); // wait for all tracks to finish loading private: Audio()=default; ~Audio()=default; Audio(const Audio&)=delete; Audio& operator=(const Audio&)=delete; static void SDLCALL streamCallback(void* userdata, SDL_AudioStream* stream, int additional, int total); diff --git a/src/audio/AudioManager.cpp b/src/audio/AudioManager.cpp new file mode 100644 index 0000000..775a4d2 --- /dev/null +++ b/src/audio/AudioManager.cpp @@ -0,0 +1,15 @@ +#include "AudioManager.h" +#include "Audio.h" + +static IAudioSystem* g_audioSystem = nullptr; + +IAudioSystem* AudioManager::get() { + if (!g_audioSystem) { + g_audioSystem = &Audio::instance(); + } + return g_audioSystem; +} + +void AudioManager::set(IAudioSystem* sys) { + g_audioSystem = sys; +} diff --git a/src/audio/AudioManager.h b/src/audio/AudioManager.h new file mode 100644 index 0000000..ebf078b --- /dev/null +++ b/src/audio/AudioManager.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../core/interfaces/IAudioSystem.h" + +class AudioManager { +public: + // Get the currently registered audio system (may return Audio::instance()) + static IAudioSystem* get(); + // Replace the audio system (for tests or different backends) + static void set(IAudioSystem* sys); +}; diff --git a/src/audio/SoundEffect.cpp b/src/audio/SoundEffect.cpp index ecc34ee..e48393e 100644 --- a/src/audio/SoundEffect.cpp +++ b/src/audio/SoundEffect.cpp @@ -2,6 +2,7 @@ #include "SoundEffect.h" #include #include "audio/Audio.h" +#include "audio/AudioManager.h" #include #include #include @@ -93,7 +94,9 @@ void SimpleAudioPlayer::playSound(const std::vector& pcmData, int chann return; } // Route through shared Audio mixer so SFX always play over music - Audio::instance().playSfx(pcmData, channels, sampleRate, volume); + if (auto sys = AudioManager::get()) { + sys->playSfx(pcmData, channels, sampleRate, volume); + } } bool SoundEffect::loadWAV(const std::string& filePath) { diff --git a/src/core/application/ApplicationManager.cpp b/src/core/application/ApplicationManager.cpp index 9e4da60..e7c13e2 100644 --- a/src/core/application/ApplicationManager.cpp +++ b/src/core/application/ApplicationManager.cpp @@ -7,6 +7,7 @@ #include "../interfaces/IInputHandler.h" #include #include "../../audio/Audio.h" +#include "../../audio/AudioManager.h" #include "../../audio/SoundEffect.h" #include "../../persistence/Scores.h" #include "../../states/State.h" @@ -267,7 +268,7 @@ void ApplicationManager::shutdown() { m_running = false; // Stop audio systems before tearing down SDL to avoid aborts/asserts - Audio::instance().shutdown(); + if (auto sys = ::AudioManager::get()) sys->shutdown(); SoundEffectManager::instance().shutdown(); // Cleanup in reverse order of initialization @@ -381,11 +382,11 @@ bool ApplicationManager::initializeManagers() { // M: Toggle/mute music; start playback if unmuting and not started yet if (!consume && sc == SDL_SCANCODE_M) { - Audio::instance().toggleMute(); + if (auto sys = ::AudioManager::get()) sys->toggleMute(); m_musicEnabled = !m_musicEnabled; - if (m_musicEnabled && !m_musicStarted && Audio::instance().getLoadedTrackCount() > 0) { - Audio::instance().shuffle(); - Audio::instance().start(); + if (m_musicEnabled && !m_musicStarted && ::AudioManager::get() && ::AudioManager::get()->getLoadedTrackCount() > 0) { + ::AudioManager::get()->shuffle(); + ::AudioManager::get()->start(); m_musicStarted = true; } consume = true; @@ -393,11 +394,7 @@ bool ApplicationManager::initializeManagers() { // N: Skip to next song in the playlist (or restart menu track) if (!consume && sc == SDL_SCANCODE_N) { - Audio::instance().skipToNextTrack(); - if (!m_musicStarted && Audio::instance().getLoadedTrackCount() > 0) { - m_musicStarted = true; - m_musicEnabled = true; - } + if (auto sys = ::AudioManager::get()) { sys->skipToNextTrack(); if (!m_musicStarted && sys->getLoadedTrackCount() > 0) { m_musicStarted = true; m_musicEnabled = true; } } consume = true; } @@ -515,13 +512,13 @@ void ApplicationManager::registerServices() { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IInputHandler service"); } - // Register Audio system singleton - auto& audioInstance = Audio::instance(); - auto audioPtr = std::shared_ptr