Compare commits

...

11 Commits

Author SHA1 Message Date
516aa16737 Merge branch 'feature/OptimizeGame' into develop 2025-12-25 19:50:42 +01:00
735e966608 Updated and fixed audio 2025-12-25 19:41:19 +01:00
68b35ea57b Audio update 2025-12-25 19:17:36 +01:00
938988c876 fixed 2025-12-25 18:23:19 +01:00
03bdc82dc1 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.
2025-12-25 17:26:55 +01:00
17cb64c9d4 fixed game renderer 2025-12-25 14:39:56 +01:00
6ef93e4c9c fixed gitignore 2025-12-25 14:24:46 +01:00
e2dd768faf fixed gitignore 2025-12-25 14:24:04 +01:00
0b546ce25c Fixed resource loader 2025-12-25 14:23:17 +01:00
45086e58d8 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.
2025-12-25 10:27:35 +01:00
b1f2033880 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).
2025-12-25 10:15:23 +01:00
31 changed files with 804 additions and 246 deletions

4
.gitignore vendored
View File

@ -18,6 +18,7 @@
CMakeCache.txt CMakeCache.txt
cmake_install.cmake cmake_install.cmake
Makefile Makefile
settings.ini
# vcpkg # vcpkg
/vcpkg_installed/ /vcpkg_installed/
@ -70,7 +71,4 @@ dist_package/
# Local environment files (if any) # Local environment files (if any)
.env .env
# Ignore local settings file
settings.ini
# End of .gitignore # End of .gitignore

View File

@ -57,6 +57,8 @@ set(TETRIS_SOURCES
src/graphics/renderers/SyncLineRenderer.cpp src/graphics/renderers/SyncLineRenderer.cpp
src/graphics/renderers/UIRenderer.cpp src/graphics/renderers/UIRenderer.cpp
src/audio/Audio.cpp src/audio/Audio.cpp
src/audio/AudioManager.cpp
src/renderer/SDLRenderer.cpp
src/gameplay/effects/LineEffect.cpp src/gameplay/effects/LineEffect.cpp
src/audio/SoundEffect.cpp src/audio/SoundEffect.cpp
src/video/VideoPlayer.cpp src/video/VideoPlayer.cpp
@ -66,6 +68,7 @@ set(TETRIS_SOURCES
src/app/Fireworks.cpp src/app/Fireworks.cpp
src/app/AssetLoader.cpp src/app/AssetLoader.cpp
src/app/TextureLoader.cpp src/app/TextureLoader.cpp
src/resources/ResourceManager.cpp
src/states/LoadingManager.cpp src/states/LoadingManager.cpp
# State implementations (new) # State implementations (new)
src/states/LoadingState.cpp src/states/LoadingState.cpp
@ -201,6 +204,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") target_include_directories(spacetris_tests PRIVATE "${CMAKE_SOURCE_DIR}/vcpkg_installed/x64-windows/include")
endif() 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 # Add new src subfolders to include path so old #includes continue to work
target_include_directories(spacetris PRIVATE target_include_directories(spacetris PRIVATE
${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/src

View File

@ -1,6 +1,10 @@
#include "app/AssetLoader.h" #include "app/AssetLoader.h"
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <algorithm> #include <algorithm>
#include "app/TextureLoader.h"
#include "utils/ImagePathResolver.h"
#include <filesystem>
AssetLoader::AssetLoader() = default; AssetLoader::AssetLoader() = default;
@ -37,6 +41,10 @@ void AssetLoader::shutdown() {
m_renderer = nullptr; m_renderer = nullptr;
} }
void AssetLoader::setResourceManager(resources::ResourceManager* mgr) {
m_resourceManager = mgr;
}
void AssetLoader::setBasePath(const std::string& basePath) { void AssetLoader::setBasePath(const std::string& basePath) {
m_basePath = basePath; m_basePath = basePath;
} }
@ -65,24 +73,25 @@ bool AssetLoader::performStep() {
std::string fullPath = m_basePath.empty() ? path : (m_basePath + "/" + path); std::string fullPath = m_basePath.empty() ? path : (m_basePath + "/" + path);
SDL_Surface* surf = IMG_Load(fullPath.c_str()); // Diagnostic: resolve path and check file existence
if (!surf) { const std::string resolved = AssetPath::resolveImagePath(path);
std::lock_guard<std::mutex> lk(m_errorsMutex); bool exists = false;
m_errors.push_back(std::string("IMG_Load failed: ") + fullPath + " -> " + SDL_GetError()); 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 { } else {
SDL_Texture* tex = SDL_CreateTextureFromSurface(m_renderer, surf); std::lock_guard<std::mutex> lk(m_texturesMutex);
SDL_DestroySurface(surf); auto& slot = m_textures[path];
if (!tex) { if (slot && slot != tex) {
std::lock_guard<std::mutex> lk(m_errorsMutex); SDL_DestroyTexture(slot);
m_errors.push_back(std::string("CreateTexture failed: ") + fullPath);
} else {
std::lock_guard<std::mutex> lk(m_texturesMutex);
auto& slot = m_textures[path];
if (slot && slot != tex) {
SDL_DestroyTexture(slot);
}
slot = tex;
} }
slot = tex;
} }
m_loadedTasks.fetch_add(1, std::memory_order_relaxed); m_loadedTasks.fetch_add(1, std::memory_order_relaxed);
@ -104,12 +113,17 @@ void AssetLoader::adoptTexture(const std::string& path, SDL_Texture* texture) {
return; return;
} }
// register in local map and resource manager
std::lock_guard<std::mutex> lk(m_texturesMutex); std::lock_guard<std::mutex> lk(m_texturesMutex);
auto& slot = m_textures[path]; auto& slot = m_textures[path];
if (slot && slot != texture) { if (slot && slot != texture) {
SDL_DestroyTexture(slot); SDL_DestroyTexture(slot);
} }
slot = texture; slot = texture;
if (m_resourceManager) {
std::shared_ptr<void> sp(texture, [](void* t){ SDL_DestroyTexture(static_cast<SDL_Texture*>(t)); });
m_resourceManager->put(path, sp);
}
} }
float AssetLoader::getProgress() const { float AssetLoader::getProgress() const {

View File

@ -6,6 +6,7 @@
#include <mutex> #include <mutex>
#include <atomic> #include <atomic>
#include <unordered_map> #include <unordered_map>
#include "../resources/ResourceManager.h"
// Lightweight AssetLoader scaffold. // Lightweight AssetLoader scaffold.
// Responsibilities: // Responsibilities:
@ -22,6 +23,7 @@ public:
void shutdown(); void shutdown();
void setBasePath(const std::string& basePath); void setBasePath(const std::string& basePath);
void setResourceManager(resources::ResourceManager* mgr);
// Queue a texture path (relative to base path) for loading. // Queue a texture path (relative to base path) for loading.
void queueTexture(const std::string& path); void queueTexture(const std::string& path);
@ -49,6 +51,7 @@ public:
private: private:
SDL_Renderer* m_renderer = nullptr; SDL_Renderer* m_renderer = nullptr;
std::string m_basePath; std::string m_basePath;
resources::ResourceManager* m_resourceManager = nullptr;
// queued paths (simple FIFO) // queued paths (simple FIFO)
std::vector<std::string> m_queue; std::vector<std::string> m_queue;

View File

@ -31,6 +31,7 @@
#include "audio/Audio.h" #include "audio/Audio.h"
#include "audio/MenuWrappers.h" #include "audio/MenuWrappers.h"
#include "audio/SoundEffect.h" #include "audio/SoundEffect.h"
#include "audio/AudioManager.h"
#include "core/Config.h" #include "core/Config.h"
#include "core/Settings.h" #include "core/Settings.h"
@ -68,6 +69,7 @@
#include "ui/MenuLayout.h" #include "ui/MenuLayout.h"
#include "utils/ImagePathResolver.h" #include "utils/ImagePathResolver.h"
#include "../resources/ResourceManager.h"
// ---------- Game config ---------- // ---------- Game config ----------
static constexpr int LOGICAL_W = 1200; static constexpr int LOGICAL_W = 1200;
@ -187,6 +189,7 @@ struct TetrisApp::Impl {
AssetLoader assetLoader; AssetLoader assetLoader;
std::unique_ptr<LoadingManager> loadingManager; std::unique_ptr<LoadingManager> loadingManager;
std::unique_ptr<TextureLoader> textureLoader; std::unique_ptr<TextureLoader> textureLoader;
resources::ResourceManager resourceManager;
FontAtlas pixelFont; FontAtlas pixelFont;
FontAtlas font; FontAtlas font;
@ -427,6 +430,8 @@ int TetrisApp::Impl::init()
// Asset loader (creates SDL_Textures on the main thread) // Asset loader (creates SDL_Textures on the main thread)
assetLoader.init(renderer); assetLoader.init(renderer);
// Wire resource manager into loader so textures are cached and reused
assetLoader.setResourceManager(&resourceManager);
loadingManager = std::make_unique<LoadingManager>(&assetLoader); loadingManager = std::make_unique<LoadingManager>(&assetLoader);
// Legacy image loader (used only as a fallback when AssetLoader misses) // Legacy image loader (used only as a fallback when AssetLoader misses)
@ -436,6 +441,8 @@ int TetrisApp::Impl::init()
currentLoadingMutex, currentLoadingMutex,
assetLoadErrors, assetLoadErrors,
assetLoadErrorsMutex); assetLoadErrorsMutex);
// Let legacy TextureLoader access the same resource cache
textureLoader->setResourceManager(&resourceManager);
// Load scores asynchronously but keep the worker alive until shutdown // Load scores asynchronously but keep the worker alive until shutdown
scoreLoader = std::jthread([this]() { scoreLoader = std::jthread([this]() {
@ -841,17 +848,19 @@ void TetrisApp::Impl::runLoop()
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
if (e.key.scancode == SDL_SCANCODE_M) if (e.key.scancode == SDL_SCANCODE_M)
{ {
Audio::instance().toggleMute(); if (auto sys = AudioManager::get()) sys->toggleMute();
musicEnabled = !musicEnabled; musicEnabled = !musicEnabled;
Settings::instance().setMusicEnabled(musicEnabled); Settings::instance().setMusicEnabled(musicEnabled);
} }
if (e.key.scancode == SDL_SCANCODE_N) if (e.key.scancode == SDL_SCANCODE_N)
{ {
Audio::instance().skipToNextTrack(); if (auto sys = AudioManager::get()) {
if (!musicStarted && Audio::instance().getLoadedTrackCount() > 0) { sys->skipToNextTrack();
musicStarted = true; if (!musicStarted && sys->getLoadedTrackCount() > 0) {
musicEnabled = true; musicStarted = true;
Settings::instance().setMusicEnabled(true); musicEnabled = true;
Settings::instance().setMusicEnabled(true);
}
} }
} }
// K: Toggle sound effects (S is reserved for co-op movement) // K: Toggle sound effects (S is reserved for co-op movement)
@ -1316,10 +1325,14 @@ void TetrisApp::Impl::runLoop()
game->softDropBoost(frameMs); game->softDropBoost(frameMs);
if (musicLoadingStarted && !musicLoaded) { if (musicLoadingStarted && !musicLoaded) {
currentTrackLoading = Audio::instance().getLoadedTrackCount(); if (auto sys = AudioManager::get()) {
if (Audio::instance().isLoadingComplete() || (totalTracks > 0 && currentTrackLoading >= totalTracks)) { currentTrackLoading = sys->getLoadedTrackCount();
Audio::instance().shuffle(); if (sys->isLoadingComplete() || (totalTracks > 0 && currentTrackLoading >= totalTracks)) {
musicLoaded = true; sys->shuffle();
musicLoaded = true;
}
} else {
currentTrackLoading = 0;
} }
} }
@ -1706,21 +1719,27 @@ void TetrisApp::Impl::runLoop()
currentLoadingFile.clear(); currentLoadingFile.clear();
} }
Audio::instance().init(); if (auto sys = AudioManager::get()) {
totalTracks = 0; sys->init();
for (int i = 1; i <= 100; ++i) { totalTracks = 0;
char base[128]; for (int i = 1; i <= 100; ++i) {
std::snprintf(base, sizeof(base), "assets/music/music%03d", i); char base[128];
std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" }); std::snprintf(base, sizeof(base), "assets/music/music%03d", i);
if (path.empty()) break; std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" });
Audio::instance().addTrackAsync(path); if (path.empty()) break;
totalTracks++; sys->addTrackAsync(path);
} totalTracks++;
totalLoadingTasks.store(baseTasks + totalTracks); }
if (totalTracks > 0) { totalLoadingTasks.store(baseTasks + totalTracks);
Audio::instance().startBackgroundLoading(); if (totalTracks > 0) {
musicLoadingStarted = true; sys->startBackgroundLoading();
musicLoadingStarted = true;
} else {
musicLoaded = true;
}
} else { } else {
totalTracks = 0;
totalLoadingTasks.store(baseTasks + totalTracks);
musicLoaded = true; musicLoaded = true;
} }
@ -1785,6 +1804,8 @@ void TetrisApp::Impl::runLoop()
nextPanelTex = assetLoader.getTexture(Assets::NEXT_PANEL); nextPanelTex = assetLoader.getTexture(Assets::NEXT_PANEL);
holdPanelTex = assetLoader.getTexture(Assets::HOLD_PANEL); holdPanelTex = assetLoader.getTexture(Assets::HOLD_PANEL);
// texture retrieval diagnostics removed
auto ensureTextureSize = [&](SDL_Texture* tex, int& outW, int& outH) { auto ensureTextureSize = [&](SDL_Texture* tex, int& outW, int& outH) {
if (!tex) return; if (!tex) return;
if (outW > 0 && outH > 0) return; if (outW > 0 && outH > 0) return;
@ -1871,10 +1892,20 @@ void TetrisApp::Impl::runLoop()
if (totalTracks > 0) { if (totalTracks > 0) {
musicProgress = musicLoaded ? 0.7 : std::min(0.7, (double)currentTrackLoading / totalTracks * 0.7); musicProgress = musicLoaded ? 0.7 : std::min(0.7, (double)currentTrackLoading / totalTracks * 0.7);
} else { } else {
if (Audio::instance().isLoadingComplete()) { if (auto sys = AudioManager::get()) {
musicProgress = 0.7; if (sys->isLoadingComplete()) {
} else if (Audio::instance().getLoadedTrackCount() > 0) { musicProgress = 0.7;
musicProgress = 0.35; } else if (sys->getLoadedTrackCount() > 0) {
musicProgress = 0.35;
} else {
Uint32 elapsedMs = SDL_GetTicks() - static_cast<Uint32>(loadStart);
if (elapsedMs > 1500) {
musicProgress = 0.7;
musicLoaded = true;
} else {
musicProgress = 0.0;
}
}
} else { } else {
Uint32 elapsedMs = SDL_GetTicks() - static_cast<Uint32>(loadStart); Uint32 elapsedMs = SDL_GetTicks() - static_cast<Uint32>(loadStart);
if (elapsedMs > 1500) { if (elapsedMs > 1500) {
@ -1916,7 +1947,7 @@ void TetrisApp::Impl::runLoop()
menuTrackLoader = std::jthread([]() { menuTrackLoader = std::jthread([]() {
std::string menuTrack = AssetPath::resolveWithExtensions("assets/music/Every Block You Take", { ".mp3" }); std::string menuTrack = AssetPath::resolveWithExtensions("assets/music/Every Block You Take", { ".mp3" });
if (!menuTrack.empty()) { if (!menuTrack.empty()) {
Audio::instance().setMenuTrack(menuTrack); if (auto sys = AudioManager::get()) sys->setMenuTrack(menuTrack);
} else { } else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Menu track not found (Every Block You Take)"); SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Menu track not found (Every Block You Take)");
} }
@ -1925,9 +1956,9 @@ void TetrisApp::Impl::runLoop()
} }
if (state == AppState::Menu) { if (state == AppState::Menu) {
Audio::instance().playMenuMusic(); if (auto sys = AudioManager::get()) sys->playMenuMusic();
} else { } else {
Audio::instance().playGameMusic(); if (auto sys = AudioManager::get()) sys->playGameMusic();
} }
musicStarted = true; musicStarted = true;
} }
@ -1936,9 +1967,9 @@ void TetrisApp::Impl::runLoop()
static AppState previousState = AppState::Loading; static AppState previousState = AppState::Loading;
if (state != previousState && musicStarted) { if (state != previousState && musicStarted) {
if (state == AppState::Menu && previousState == AppState::Playing) { if (state == AppState::Menu && previousState == AppState::Playing) {
Audio::instance().playMenuMusic(); if (auto sys = AudioManager::get()) sys->playMenuMusic();
} else if (state == AppState::Playing && previousState == AppState::Menu) { } else if (state == AppState::Playing && previousState == AppState::Menu) {
Audio::instance().playGameMusic(); if (auto sys = AudioManager::get()) sys->playGameMusic();
} }
} }
previousState = state; previousState = state;
@ -2712,7 +2743,7 @@ void TetrisApp::Impl::shutdown()
} }
lineEffect.shutdown(); lineEffect.shutdown();
Audio::instance().shutdown(); if (auto sys = AudioManager::get()) sys->shutdown();
SoundEffectManager::instance().shutdown(); SoundEffectManager::instance().shutdown();
// Destroy textures before tearing down the renderer/window. // Destroy textures before tearing down the renderer/window.

View File

@ -6,6 +6,8 @@
#include <mutex> #include <mutex>
#include <sstream> #include <sstream>
#include <filesystem>
#include "utils/ImagePathResolver.h" #include "utils/ImagePathResolver.h"
TextureLoader::TextureLoader( TextureLoader::TextureLoader(
@ -45,6 +47,18 @@ SDL_Texture* TextureLoader::loadFromImage(SDL_Renderer* renderer, const std::str
const std::string resolvedPath = AssetPath::resolveImagePath(path); const std::string resolvedPath = AssetPath::resolveImagePath(path);
setCurrentLoadingFile(resolvedPath.empty() ? path : resolvedPath); 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<SDL_Texture>(path)) {
clearCurrentLoadingFile();
loadedTasks_.fetch_add(1);
return sp.get();
}
}
SDL_Surface* surface = IMG_Load(resolvedPath.c_str()); SDL_Surface* surface = IMG_Load(resolvedPath.c_str());
if (!surface) { if (!surface) {
{ {
@ -54,7 +68,7 @@ SDL_Texture* TextureLoader::loadFromImage(SDL_Renderer* renderer, const std::str
} }
loadedTasks_.fetch_add(1); loadedTasks_.fetch_add(1);
clearCurrentLoadingFile(); 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; return nullptr;
} }
@ -66,6 +80,7 @@ SDL_Texture* TextureLoader::loadFromImage(SDL_Renderer* renderer, const std::str
} }
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
// surface size preserved in outW/outH; no console log
SDL_DestroySurface(surface); SDL_DestroySurface(surface);
if (!texture) { if (!texture) {
@ -80,6 +95,15 @@ SDL_Texture* TextureLoader::loadFromImage(SDL_Renderer* renderer, const std::str
return nullptr; return nullptr;
} }
// No texture-size console diagnostics here
// cache in resource manager if present
if (resourceManager_) {
std::shared_ptr<void> sp(texture, [](void* t){ SDL_DestroyTexture(static_cast<SDL_Texture*>(t)); });
// store under original asset key (path) so callers using logical asset names find them
resourceManager_->put(path, sp);
}
loadedTasks_.fetch_add(1); loadedTasks_.fetch_add(1);
clearCurrentLoadingFile(); clearCurrentLoadingFile();

View File

@ -6,6 +6,7 @@
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <vector> #include <vector>
#include "../resources/ResourceManager.h"
class TextureLoader { class TextureLoader {
public: public:
@ -16,6 +17,8 @@ public:
std::vector<std::string>& assetLoadErrors, std::vector<std::string>& assetLoadErrors,
std::mutex& assetLoadErrorsMutex); 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); SDL_Texture* loadFromImage(SDL_Renderer* renderer, const std::string& path, int* outW = nullptr, int* outH = nullptr);
private: private:
@ -28,4 +31,6 @@ private:
void setCurrentLoadingFile(const std::string& filename); void setCurrentLoadingFile(const std::string& filename);
void clearCurrentLoadingFile(); void clearCurrentLoadingFile();
void recordAssetLoadError(const std::string& message); void recordAssetLoadError(const std::string& message);
resources::ResourceManager* resourceManager_ = nullptr;
}; };

View File

@ -118,6 +118,7 @@ static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int
outCh = static_cast<int>(clientFormat.mChannelsPerFrame); outCh = static_cast<int>(clientFormat.mChannelsPerFrame);
return !outPCM.empty(); return !outPCM.empty();
} }
#else #else
static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int& outRate, int& outCh){ static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int& outRate, int& outCh){
(void)outPCM; (void)outRate; (void)outCh; (void)path; (void)outPCM; (void)outRate; (void)outCh; (void)path;
@ -184,6 +185,8 @@ void Audio::skipToNextTrack(){
void Audio::toggleMute(){ muted=!muted; } void Audio::toggleMute(){ muted=!muted; }
void Audio::setMuted(bool m){ muted=m; } void Audio::setMuted(bool m){ muted=m; }
bool Audio::isMuted() const { return muted; }
void Audio::nextTrack(){ void Audio::nextTrack(){
if(tracks.empty()) { current = -1; return; } if(tracks.empty()) { current = -1; return; }
// Try every track once to find a decodable one // Try every track once to find a decodable one

View File

@ -32,29 +32,27 @@ public:
void setSoundVolume(float volume) override; void setSoundVolume(float volume) override;
bool isMusicPlaying() const override; bool isMusicPlaying() const override;
// Existing Audio class methods // Additional IAudioSystem methods (forwarded to concrete implementation)
bool init(); // initialize backend (MF on Windows) bool init() override;
void addTrack(const std::string& path); // decode MP3 -> PCM16 stereo 44100 void shutdown() override;
void addTrackAsync(const std::string& path); // add track for background loading void addTrack(const std::string& path) override;
void startBackgroundLoading(); // start background thread for loading void addTrackAsync(const std::string& path) override;
void waitForLoadingComplete(); // wait for all tracks to finish loading void startBackgroundLoading() override;
bool isLoadingComplete() const; // check if background loading is done bool isLoadingComplete() const override;
int getLoadedTrackCount() const; // get number of tracks loaded so far int getLoadedTrackCount() const override;
void shuffle(); // randomize order void start() override;
void start(); // begin playback void skipToNextTrack() override;
void skipToNextTrack(); // advance to the next music track void shuffle() override;
void toggleMute(); void toggleMute() override;
bool isMuted() const override;
void setMuted(bool m); void setMuted(bool m);
bool isMuted() const { return muted; } void setMenuTrack(const std::string& path) override;
void playMenuMusic() override;
void playGameMusic() override;
void playSfx(const std::vector<int16_t>& pcm, int channels, int rate, float volume) override;
// Menu music support // Existing Audio class helper methods
void setMenuTrack(const std::string& path); void waitForLoadingComplete(); // wait for all tracks to finish loading
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<int16_t>& pcm, int channels, int rate, float volume);
void shutdown();
private: private:
Audio()=default; ~Audio()=default; Audio(const Audio&)=delete; Audio& operator=(const Audio&)=delete; 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); static void SDLCALL streamCallback(void* userdata, SDL_AudioStream* stream, int additional, int total);

View File

@ -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;
}

11
src/audio/AudioManager.h Normal file
View File

@ -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);
};

View File

@ -2,6 +2,7 @@
#include "SoundEffect.h" #include "SoundEffect.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include "audio/Audio.h" #include "audio/Audio.h"
#include "audio/AudioManager.h"
#include <cstdio> #include <cstdio>
#include <algorithm> #include <algorithm>
#include <random> #include <random>
@ -93,7 +94,9 @@ void SimpleAudioPlayer::playSound(const std::vector<int16_t>& pcmData, int chann
return; return;
} }
// Route through shared Audio mixer so SFX always play over music // 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) { bool SoundEffect::loadWAV(const std::string& filePath) {

View File

@ -7,6 +7,7 @@
#include "../interfaces/IInputHandler.h" #include "../interfaces/IInputHandler.h"
#include <filesystem> #include <filesystem>
#include "../../audio/Audio.h" #include "../../audio/Audio.h"
#include "../../audio/AudioManager.h"
#include "../../audio/SoundEffect.h" #include "../../audio/SoundEffect.h"
#include "../../persistence/Scores.h" #include "../../persistence/Scores.h"
#include "../../states/State.h" #include "../../states/State.h"
@ -267,7 +268,7 @@ void ApplicationManager::shutdown() {
m_running = false; m_running = false;
// Stop audio systems before tearing down SDL to avoid aborts/asserts // Stop audio systems before tearing down SDL to avoid aborts/asserts
Audio::instance().shutdown(); if (auto sys = AudioManager::get()) sys->shutdown();
SoundEffectManager::instance().shutdown(); SoundEffectManager::instance().shutdown();
// Cleanup in reverse order of initialization // 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 // M: Toggle/mute music; start playback if unmuting and not started yet
if (!consume && sc == SDL_SCANCODE_M) { if (!consume && sc == SDL_SCANCODE_M) {
Audio::instance().toggleMute(); if (auto sys = AudioManager::get()) sys->toggleMute();
m_musicEnabled = !m_musicEnabled; m_musicEnabled = !m_musicEnabled;
if (m_musicEnabled && !m_musicStarted && Audio::instance().getLoadedTrackCount() > 0) { if (m_musicEnabled && !m_musicStarted && AudioManager::get() && AudioManager::get()->getLoadedTrackCount() > 0) {
Audio::instance().shuffle(); AudioManager::get()->shuffle();
Audio::instance().start(); AudioManager::get()->start();
m_musicStarted = true; m_musicStarted = true;
} }
consume = true; consume = true;
@ -393,11 +394,7 @@ bool ApplicationManager::initializeManagers() {
// N: Skip to next song in the playlist (or restart menu track) // N: Skip to next song in the playlist (or restart menu track)
if (!consume && sc == SDL_SCANCODE_N) { if (!consume && sc == SDL_SCANCODE_N) {
Audio::instance().skipToNextTrack(); if (auto sys = AudioManager::get()) { sys->skipToNextTrack(); if (!m_musicStarted && sys->getLoadedTrackCount() > 0) { m_musicStarted = true; m_musicEnabled = true; } }
if (!m_musicStarted && Audio::instance().getLoadedTrackCount() > 0) {
m_musicStarted = true;
m_musicEnabled = true;
}
consume = true; consume = true;
} }
@ -515,13 +512,13 @@ void ApplicationManager::registerServices() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IInputHandler service"); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IInputHandler service");
} }
// Register Audio system singleton // Register Audio system singleton (via AudioManager)
auto& audioInstance = Audio::instance(); IAudioSystem* audioInstance = AudioManager::get();
auto audioPtr = std::shared_ptr<Audio>(&audioInstance, [](Audio*) { if (audioInstance) {
// Custom deleter that does nothing since Audio is a singleton std::shared_ptr<IAudioSystem> audioPtr(audioInstance, [](IAudioSystem*){});
}); m_serviceContainer.registerSingleton<IAudioSystem>(audioPtr);
m_serviceContainer.registerSingleton<IAudioSystem>(audioPtr); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IAudioSystem service");
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IAudioSystem service"); }
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Service registration completed successfully"); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Service registration completed successfully");
} }
@ -618,7 +615,7 @@ bool ApplicationManager::initializeGame() {
// as lambdas that reference members here. // as lambdas that reference members here.
// Start background music loading similar to main.cpp: Audio init + file discovery // Start background music loading similar to main.cpp: Audio init + file discovery
Audio::instance().init(); if (auto sys = AudioManager::get()) sys->init();
// Discover available tracks (up to 100) and queue for background loading // Discover available tracks (up to 100) and queue for background loading
m_totalTracks = 0; m_totalTracks = 0;
std::vector<std::string> trackPaths; std::vector<std::string> trackPaths;
@ -634,15 +631,15 @@ bool ApplicationManager::initializeGame() {
} }
m_totalTracks = static_cast<int>(trackPaths.size()); m_totalTracks = static_cast<int>(trackPaths.size());
for (const auto& path : trackPaths) { for (const auto& path : trackPaths) {
Audio::instance().addTrackAsync(path); if (auto sys = AudioManager::get()) sys->addTrackAsync(path);
} }
if (m_totalTracks > 0) { if (m_totalTracks > 0) {
Audio::instance().startBackgroundLoading(); if (auto sys = AudioManager::get()) sys->startBackgroundLoading();
// Kick off playback now; Audio will pick a track once decoded. // Kick off playback now; Audio will pick a track once decoded.
// Do not mark as started yet; we'll flip the flag once a track is actually loaded. // Do not mark as started yet; we'll flip the flag once a track is actually loaded.
if (m_musicEnabled) { if (m_musicEnabled) {
Audio::instance().shuffle(); if (auto sys = AudioManager::get()) { sys->shuffle(); sys->start(); }
Audio::instance().start(); m_musicStarted = true;
} }
m_currentTrackLoading = 1; // mark started m_currentTrackLoading = 1; // mark started
} }
@ -941,15 +938,15 @@ void ApplicationManager::setupStateHandlers() {
// Start music as soon as at least one track has decoded (dont wait for all) // Start music as soon as at least one track has decoded (dont wait for all)
// Start music as soon as at least one track has decoded (don't wait for all) // Start music as soon as at least one track has decoded (don't wait for all)
if (m_musicEnabled && !m_musicStarted) { if (m_musicEnabled && !m_musicStarted) {
if (Audio::instance().getLoadedTrackCount() > 0) { if (auto sys = AudioManager::get()) {
Audio::instance().shuffle(); if (sys->getLoadedTrackCount() > 0) { sys->shuffle(); sys->start(); m_musicStarted = true; }
Audio::instance().start();
m_musicStarted = true;
} }
} }
// Track completion status for UI // Track completion status for UI
if (!m_musicLoaded && Audio::instance().isLoadingComplete()) { if (!m_musicLoaded) {
m_musicLoaded = true; if (auto sys = AudioManager::get()) {
if (sys->isLoadingComplete()) m_musicLoaded = true;
}
} }
}); });

View File

@ -1,12 +1,15 @@
#include "AssetManager.h" #include "AssetManager.h"
#include "../../graphics/ui/Font.h" #include "../../graphics/ui/Font.h"
#include "../../audio/Audio.h" #include "../../audio/Audio.h"
#include "../../audio/AudioManager.h"
#include "../../audio/SoundEffect.h" #include "../../audio/SoundEffect.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <SDL3_ttf/SDL_ttf.h> #include <SDL3_ttf/SDL_ttf.h>
#include <filesystem> #include <filesystem>
#include "../../utils/ImagePathResolver.h" #include "../../utils/ImagePathResolver.h"
#include "../../core/Config.h"
#include "../../resources/AssetPaths.h"
AssetManager::AssetManager() AssetManager::AssetManager()
: m_renderer(nullptr) : m_renderer(nullptr)
@ -38,7 +41,7 @@ bool AssetManager::initialize(SDL_Renderer* renderer) {
m_renderer = renderer; m_renderer = renderer;
// Get references to singleton systems // Get references to singleton systems
m_audioSystem = &Audio::instance(); m_audioSystem = AudioManager::get();
m_soundSystem = &SoundEffectManager::instance(); m_soundSystem = &SoundEffectManager::instance();
m_initialized = true; m_initialized = true;
@ -103,7 +106,34 @@ SDL_Texture* AssetManager::loadTexture(const std::string& id, const std::string&
SDL_Texture* AssetManager::getTexture(const std::string& id) const { SDL_Texture* AssetManager::getTexture(const std::string& id) const {
auto it = m_textures.find(id); 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<std::string> 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<AssetManager*>(this);
SDL_Texture* tex = self->loadTexture(id, candidatePath);
if (tex) return tex;
}
return nullptr;
} }
bool AssetManager::unloadTexture(const std::string& id) { bool AssetManager::unloadTexture(const std::string& id) {

View File

@ -7,12 +7,12 @@
#include <memory> #include <memory>
#include <functional> #include <functional>
#include "../interfaces/IAssetLoader.h" #include "../interfaces/IAssetLoader.h"
#include "../interfaces/IAssetLoader.h"
// Forward declarations // Forward declarations
class FontAtlas; class FontAtlas;
class Audio; class Audio;
class SoundEffectManager; class SoundEffectManager;
class IAudioSystem;
/** /**
* AssetManager - Centralized resource management following SOLID principles * AssetManager - Centralized resource management following SOLID principles
@ -121,7 +121,7 @@ private:
// System references // System references
SDL_Renderer* m_renderer; SDL_Renderer* m_renderer;
Audio* m_audioSystem; // Pointer to singleton IAudioSystem* m_audioSystem; // Pointer to audio system (IAudioSystem)
SoundEffectManager* m_soundSystem; // Pointer to singleton SoundEffectManager* m_soundSystem; // Pointer to singleton
// Configuration // Configuration

View File

@ -1,6 +1,8 @@
#pragma once #pragma once
#include <string> #include <string>
#include <vector>
#include <cstdint>
/** /**
* @brief Abstract interface for audio system operations * @brief Abstract interface for audio system operations
@ -52,4 +54,28 @@ public:
* @return true if music is playing, false otherwise * @return true if music is playing, false otherwise
*/ */
virtual bool isMusicPlaying() const = 0; virtual bool isMusicPlaying() const = 0;
// Extended control methods used by the application
virtual bool init() = 0;
virtual void shutdown() = 0;
virtual void addTrack(const std::string& path) = 0;
virtual void addTrackAsync(const std::string& path) = 0;
virtual void startBackgroundLoading() = 0;
virtual bool isLoadingComplete() const = 0;
virtual int getLoadedTrackCount() const = 0;
virtual void start() = 0;
virtual void skipToNextTrack() = 0;
virtual void shuffle() = 0;
virtual void toggleMute() = 0;
virtual bool isMuted() const = 0;
virtual void setMenuTrack(const std::string& path) = 0;
virtual void playMenuMusic() = 0;
virtual void playGameMusic() = 0;
// Low-level SFX path (raw PCM) used by internal SFX mixer
virtual void playSfx(const std::vector<int16_t>& pcm, int channels, int rate, float volume) = 0;
}; };

View File

@ -3,6 +3,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include "audio/Audio.h" #include "audio/Audio.h"
#include "audio/AudioManager.h"
#ifndef M_PI #ifndef M_PI
#define M_PI 3.14159265358979323846 #define M_PI 3.14159265358979323846
@ -266,6 +267,6 @@ void LineEffect::playLineClearSound(int lineCount) {
const std::vector<int16_t>* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample; const std::vector<int16_t>* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample;
if (sample && !sample->empty()) { if (sample && !sample->empty()) {
// Mix via shared Audio device so it layers with music // Mix via shared Audio device so it layers with music
Audio::instance().playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f); if (auto sys = AudioManager::get()) sys->playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f);
} }
} }

View File

@ -3,6 +3,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include "audio/Audio.h" #include "audio/Audio.h"
#include "audio/AudioManager.h"
#include "gameplay/core/Game.h" #include "gameplay/core/Game.h"
#ifndef M_PI #ifndef M_PI
@ -461,7 +462,7 @@ void LineEffect::playLineClearSound(int lineCount) {
const std::vector<int16_t>* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample; const std::vector<int16_t>* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample;
if (sample && !sample->empty()) { if (sample && !sample->empty()) {
// Mix via shared Audio device so it layers with music // Mix via shared Audio device so it layers with music
Audio::instance().playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f); if (auto sys = AudioManager::get()) sys->playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f);
} }
} }

View File

@ -1,4 +1,5 @@
#include "GameRenderer.h" #include "GameRenderer.h"
#include "../../renderer/Renderer_iface.h"
#include "SyncLineRenderer.h" #include "SyncLineRenderer.h"
#include "../../gameplay/core/Game.h" #include "../../gameplay/core/Game.h"
@ -248,23 +249,25 @@ static void updateAndDrawTransport(SDL_Renderer* renderer, SDL_Texture* blocksTe
Uint8 gridAlpha = static_cast<Uint8>(std::lround(255.0f * t)); Uint8 gridAlpha = static_cast<Uint8>(std::lround(255.0f * t));
Uint8 nextAlpha = gridAlpha; // fade new NEXT preview in at same rate as grid Uint8 nextAlpha = gridAlpha; // fade new NEXT preview in at same rate as grid
// Draw preview fade-out // Create renderer wrapper
if (previewAlpha > 0) { auto rwrap = renderer::MakeSDLRenderer(renderer);
if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, previewAlpha); // Draw preview fade-out
for (int cy = 0; cy < 4; ++cy) { if (previewAlpha > 0) {
for (int cx = 0; cx < 4; ++cx) { if (blocksTex) rwrap->setTextureAlphaMod(blocksTex, previewAlpha);
if (!Game::cellFilled(s_transport.piece, cx, cy)) continue; for (int cy = 0; cy < 4; ++cy) {
float px = s_transport.startX + static_cast<float>(cx) * s_transport.tileSize; for (int cx = 0; cx < 4; ++cx) {
float py = s_transport.startY + static_cast<float>(cy) * s_transport.tileSize; if (!Game::cellFilled(s_transport.piece, cx, cy)) continue;
GameRenderer::drawBlockTexturePublic(renderer, blocksTex, px, py, s_transport.tileSize, s_transport.piece.type); float px = s_transport.startX + static_cast<float>(cx) * s_transport.tileSize;
float py = s_transport.startY + static_cast<float>(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) // Draw grid fade-in (same intensity as next preview fade-in)
if (gridAlpha > 0) { if (gridAlpha > 0) {
if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, gridAlpha); if (blocksTex) rwrap->setTextureAlphaMod(blocksTex, gridAlpha);
for (int cy = 0; cy < 4; ++cy) { for (int cy = 0; cy < 4; ++cy) {
for (int cx = 0; cx < 4; ++cx) { for (int cx = 0; cx < 4; ++cx) {
if (!Game::cellFilled(s_transport.piece, cx, cy)) continue; 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); 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) // Draw new NEXT preview fade-in (simultaneous)
if (nextAlpha > 0) { if (nextAlpha > 0) {
if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, nextAlpha); if (blocksTex) rwrap->setTextureAlphaMod(blocksTex, nextAlpha);
for (int cy = 0; cy < 4; ++cy) { for (int cy = 0; cy < 4; ++cy) {
for (int cx = 0; cx < 4; ++cx) { for (int cx = 0; cx < 4; ++cx) {
if (!Game::cellFilled(s_transport.nextPiece, cx, cy)) continue; 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); 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) { 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) { 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_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) { 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) { 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_FRect glow{ x + inset, y + inset, size - inset * 2.0f, size - inset * 2.0f };
SDL_RenderRect(renderer, &glow); rwrap->drawRectF(&glow);
}; };
if (asteroidTex) { if (asteroidTex) {
@ -330,9 +335,10 @@ static void drawAsteroid(SDL_Renderer* renderer, SDL_Texture* asteroidTex, float
case AsteroidType::Core: col = 3; break; case AsteroidType::Core: col = 3; break;
} }
int row = std::clamp<int>(cell.visualState, 0, 2); int row = std::clamp<int>(cell.visualState, 0, 2);
auto rwrap = renderer::MakeSDLRenderer(renderer);
SDL_FRect src{ col * SPRITE_SIZE, row * SPRITE_SIZE, SPRITE_SIZE, SPRITE_SIZE }; SDL_FRect src{ col * SPRITE_SIZE, row * SPRITE_SIZE, SPRITE_SIZE, SPRITE_SIZE };
SDL_FRect dst{ x, y, size, size }; SDL_FRect dst{ x, y, size, size };
SDL_RenderTexture(renderer, asteroidTex, &src, &dst); rwrap->renderTexture(asteroidTex, &src, &dst);
if (cell.gravityEnabled) { if (cell.gravityEnabled) {
outlineGravity(2.0f, SDL_Color{255, 230, 120, 180}); 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<Uint8>(base.b * hpScale + 40 * (1.0f - hpScale)), static_cast<Uint8>(base.b * hpScale + 40 * (1.0f - hpScale)),
255 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_FRect body{x, y, size - 1.0f, size - 1.0f};
SDL_RenderFillRect(renderer, &body); rwrap->fillRectF(&body);
SDL_Color outline = base; SDL_Color outline = base;
outline.a = 220; outline.a = 220;
SDL_FRect border{x + 1.0f, y + 1.0f, size - 2.0f, size - 2.0f}; 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); rwrap->setDrawColor(outline);
SDL_RenderRect(renderer, &border); rwrap->drawRectF(&border);
if (cell.gravityEnabled) { if (cell.gravityEnabled) {
outlineGravity(2.0f, SDL_Color{255, 230, 120, 180}); 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 srcRect = {srcX, srcY, srcW, srcH};
SDL_FRect dstRect = {x, y, size, size}; 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) { 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); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
// Draw ghost piece as barely visible gray outline // 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_FRect rect = {px + 2, py + 2, tileSize - 4, tileSize - 4};
SDL_RenderFillRect(renderer, &rect); rwrap->fillRectF(&rect);
// Draw thin gray border // 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_FRect border = {px + 1, py + 1, tileSize - 2, tileSize - 2};
SDL_RenderRect(renderer, &border); rwrap->drawRectF(&border);
} else { } else {
drawBlockTexture(renderer, blocksTex, px, py, tileSize, piece.type); 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) { void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize) {
if (pieceType >= PIECE_COUNT) return; if (pieceType >= PIECE_COUNT) return;
auto rwrap = renderer::MakeSDLRenderer(renderer);
// Use the first rotation (index 0) for preview // Use the first rotation (index 0) for preview
Game::Piece previewPiece; Game::Piece previewPiece;
@ -461,7 +473,7 @@ void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex
// Use semi-transparent alpha for preview blocks // Use semi-transparent alpha for preview blocks
Uint8 previewAlpha = 180; Uint8 previewAlpha = 180;
if (blocksTex) { if (blocksTex) {
SDL_SetTextureAlphaMod(blocksTex, previewAlpha); rwrap->setTextureAlphaMod(blocksTex, previewAlpha);
} }
for (int cy = 0; cy < 4; ++cy) { for (int cy = 0; cy < 4; ++cy) {
@ -476,7 +488,7 @@ void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex
// Reset alpha // Reset alpha
if (blocksTex) { if (blocksTex) {
SDL_SetTextureAlphaMod(blocksTex, 255); rwrap->setTextureAlphaMod(blocksTex, 255);
} }
} }
@ -496,6 +508,8 @@ void GameRenderer::renderNextPanel(
return; return;
} }
auto rwrap = renderer::MakeSDLRenderer(renderer);
const SDL_Color gridBorderColor{60, 80, 160, 255}; // matches main grid outline const SDL_Color gridBorderColor{60, 80, 160, 255}; // matches main grid outline
const SDL_Color bayColor{8, 12, 24, 235}; const SDL_Color bayColor{8, 12, 24, 235};
const SDL_Color bayOutline{25, 62, 86, 220}; 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. // the panel rectangle and skip the custom background/frame drawing.
if (nextPanelTex) { if (nextPanelTex) {
SDL_FRect dst{panelX, panelY, panelW, panelH}; SDL_FRect dst{panelX, panelY, panelW, panelH};
SDL_RenderTexture(renderer, nextPanelTex, nullptr, &dst); rwrap->renderTexture(nextPanelTex, nullptr, &dst);
// Draw the panel label over the texture — user requested visible label
const float labelPad = tileSize * 0.25f; const float labelPad = tileSize * 0.25f;
pixelFont->draw(renderer, panelX + labelPad, panelY + labelPad * 0.5f, "NEXT", 0.9f, labelColor); pixelFont->draw(renderer, panelX + labelPad, panelY + labelPad * 0.5f, "NEXT", 0.9f, labelColor);
} else { } else {
SDL_FRect bayRect{panelX, panelY, panelW, panelH}; SDL_FRect bayRect{panelX, panelY, panelW, panelH};
SDL_SetRenderDrawColor(renderer, bayColor.r, bayColor.g, bayColor.b, bayColor.a); rwrap->setDrawColor(bayColor);
SDL_RenderFillRect(renderer, &bayRect); rwrap->fillRectF(&bayRect);
SDL_FRect thinOutline{panelX - 1.0f, panelY - 1.0f, panelW + 2.0f, panelH + 2.0f}; SDL_FRect thinOutline{panelX - 1.0f, panelY - 1.0f, panelW + 2.0f, panelH + 2.0f};
auto drawOutlineNoBottom = [&](const SDL_FRect& rect, SDL_Color color) { 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 left = rect.x;
const float top = rect.y; const float top = rect.y;
const float right = rect.x + rect.w; const float right = rect.x + rect.w;
const float bottom = rect.y + rect.h; const float bottom = rect.y + rect.h;
SDL_RenderLine(renderer, left, top, right, top); // top edge rwrap->renderLine(left, top, right, top); // top edge
SDL_RenderLine(renderer, left, top, left, bottom); // left edge rwrap->renderLine(left, top, left, bottom); // left edge
SDL_RenderLine(renderer, right, top, right, bottom); // right edge rwrap->renderLine(right, top, right, bottom); // right edge
}; };
drawOutlineNoBottom(thinOutline, gridBorderColor); drawOutlineNoBottom(thinOutline, gridBorderColor);
@ -641,11 +654,12 @@ void GameRenderer::renderPlayingState(
float contentOffsetX = (winW - contentW) * 0.5f / contentScale; float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
float contentOffsetY = (winH - contentH) * 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) { 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_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 // Responsive layout that scales with window size while maintaining margins
@ -747,28 +761,28 @@ void GameRenderer::renderPlayingState(
scaledW, scaledW,
scaledH scaledH
}; };
SDL_RenderTexture(renderer, statisticsPanelTex, nullptr, &dstF); rwrap->renderTexture(statisticsPanelTex, nullptr, &dstF);
} }
} else { } else {
// Fallback: render entire texture stretched to panel // Fallback: render entire texture stretched to panel
SDL_RenderTexture(renderer, statisticsPanelTex, nullptr, &blocksPanelBg); rwrap->renderTexture(statisticsPanelTex, nullptr, &blocksPanelBg);
} }
} else if (scorePanelTex) { } else if (scorePanelTex) {
SDL_RenderTexture(renderer, scorePanelTex, nullptr, &blocksPanelBg); rwrap->renderTexture(scorePanelTex, nullptr, &blocksPanelBg);
} else { } else {
SDL_SetRenderDrawColor(renderer, 12, 18, 32, 205); rwrap->setDrawColor(SDL_Color{12, 18, 32, 205});
SDL_RenderFillRect(renderer, &blocksPanelBg); rwrap->fillRectF(&blocksPanelBg);
} }
// Draw grid lines // 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) { for (int x = 1; x < Game::COLS; ++x) {
float lineX = gridX + x * finalBlockSize; 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) { for (int y = 1; y < Game::ROWS; ++y) {
float lineY = gridY + y * finalBlockSize; 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) { if (!s_starfieldInitialized) {
@ -817,6 +831,7 @@ void GameRenderer::renderPlayingState(
SDL_BlendMode oldBlend = SDL_BLENDMODE_NONE; SDL_BlendMode oldBlend = SDL_BLENDMODE_NONE;
SDL_GetRenderDrawBlendMode(renderer, &oldBlend); SDL_GetRenderDrawBlendMode(renderer, &oldBlend);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); 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 // Add a small, smooth sub-pixel jitter to the starfield origin so the
// brightest star doesn't permanently sit exactly at the visual center. // brightest star doesn't permanently sit exactly at the visual center.
{ {
@ -933,10 +948,10 @@ void GameRenderer::renderPlayingState(
float pulse = 0.5f + 0.5f * std::sin(sp.pulse); float pulse = 0.5f + 0.5f * std::sin(sp.pulse);
Uint8 alpha = static_cast<Uint8>(std::clamp(lifeRatio * pulse, 0.0f, 1.0f) * 255.0f); Uint8 alpha = static_cast<Uint8>(std::clamp(lifeRatio * pulse, 0.0f, 1.0f) * 255.0f);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); 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; float half = sp.size * 0.5f;
SDL_FRect fr{gridX + sp.x - half, gridY + sp.y - half, sp.size, sp.size}; SDL_FRect fr{gridX + sp.x - half, gridY + sp.y - half, sp.size, sp.size};
SDL_RenderFillRect(renderer, &fr); rwrap->fillRectF(&fr);
++it; ++it;
} }
@ -949,11 +964,11 @@ void GameRenderer::renderPlayingState(
// Draw a small filled connector to visually merge NEXT panel and grid border // 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 // If an external NEXT panel texture is used, skip the connector to avoid
// drawing a visible seam under the image/artwork. // drawing a visible seam under the image/artwork.
if (!nextPanelTex) { if (!nextPanelTex) {
SDL_SetRenderDrawColor(renderer, 60, 80, 160, 255); // same as grid border 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) 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_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) // Draw transport effect if active (renders the moving piece and trail)
@ -1164,27 +1179,27 @@ void GameRenderer::renderPlayingState(
} }
if (asteroidsTex && spawnAlpha < 1.0f) { if (asteroidsTex && spawnAlpha < 1.0f) {
SDL_SetTextureAlphaMod(asteroidsTex, static_cast<Uint8>(std::clamp(spawnAlpha, 0.0f, 1.0f) * 255.0f)); rwrap->setTextureAlphaMod(asteroidsTex, static_cast<Uint8>(std::clamp(spawnAlpha, 0.0f, 1.0f) * 255.0f));
} }
float size = finalBlockSize * spawnScale * clearScale; float size = finalBlockSize * spawnScale * clearScale;
float offset = (finalBlockSize - size) * 0.5f; float offset = (finalBlockSize - size) * 0.5f;
if (asteroidsTex && clearAlpha < 1.0f) { if (asteroidsTex && clearAlpha < 1.0f) {
Uint8 alpha = static_cast<Uint8>(std::clamp(spawnAlpha * clearAlpha, 0.0f, 1.0f) * 255.0f); Uint8 alpha = static_cast<Uint8>(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); drawAsteroid(renderer, asteroidsTex, bx + offset, by + offset, size, cell);
if (asteroidsTex && (spawnAlpha < 1.0f || clearAlpha < 1.0f)) { if (asteroidsTex && (spawnAlpha < 1.0f || clearAlpha < 1.0f)) {
SDL_SetTextureAlphaMod(asteroidsTex, 255); rwrap->setTextureAlphaMod(asteroidsTex, 255);
} }
} else { } else {
if (blocksTex && clearAlpha < 1.0f) { if (blocksTex && clearAlpha < 1.0f) {
SDL_SetTextureAlphaMod(blocksTex, static_cast<Uint8>(std::clamp(clearAlpha, 0.0f, 1.0f) * 255.0f)); rwrap->setTextureAlphaMod(blocksTex, static_cast<Uint8>(std::clamp(clearAlpha, 0.0f, 1.0f) * 255.0f));
} }
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize * clearScale, v - 1); drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize * clearScale, v - 1);
if (blocksTex && clearAlpha < 1.0f) { if (blocksTex && clearAlpha < 1.0f) {
SDL_SetTextureAlphaMod(blocksTex, 255); rwrap->setTextureAlphaMod(blocksTex, 255);
} }
} }
} }
@ -1209,7 +1224,7 @@ void GameRenderer::renderPlayingState(
s.y += s.vy * sparkDeltaMs; s.y += s.vy * sparkDeltaMs;
float lifeRatio = std::clamp(static_cast<float>(s.lifeMs / s.maxLifeMs), 0.0f, 1.0f); float lifeRatio = std::clamp(static_cast<float>(s.lifeMs / s.maxLifeMs), 0.0f, 1.0f);
Uint8 alpha = static_cast<Uint8>(lifeRatio * 200.0f); Uint8 alpha = static_cast<Uint8>(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); float size = s.size * (0.7f + (1.0f - lifeRatio) * 0.8f);
SDL_FRect shardRect{ SDL_FRect shardRect{
s.x - size * 0.5f, s.x - size * 0.5f,
@ -1217,7 +1232,7 @@ void GameRenderer::renderPlayingState(
size, size,
size * 1.4f size * 1.4f
}; };
SDL_RenderFillRect(renderer, &shardRect); rwrap->fillRectF(&shardRect);
++shardIt; ++shardIt;
} }
@ -1238,14 +1253,14 @@ void GameRenderer::renderPlayingState(
SDL_Color c = b.color; SDL_Color c = b.color;
Uint8 a = static_cast<Uint8>(alpha * 220.0f); Uint8 a = static_cast<Uint8>(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{ SDL_FRect outer{
b.x - radius + jitter, b.x - radius + jitter,
b.y - radius + jitter, b.y - radius + jitter,
radius * 2.0f, radius * 2.0f,
radius * 2.0f radius * 2.0f
}; };
SDL_RenderRect(renderer, &outer); rwrap->drawRectF(&outer);
SDL_FRect inner{ SDL_FRect inner{
b.x - (radius - thickness), b.x - (radius - thickness),
@ -1253,8 +1268,8 @@ void GameRenderer::renderPlayingState(
(radius - thickness) * 2.0f, (radius - thickness) * 2.0f,
(radius - thickness) * 2.0f (radius - thickness) * 2.0f
}; };
SDL_SetRenderDrawColor(renderer, 255, 255, 255, static_cast<Uint8>(a * 0.9f)); rwrap->setDrawColor(SDL_Color{255, 255, 255, static_cast<Uint8>(a * 0.9f)});
SDL_RenderRect(renderer, &inner); rwrap->drawRectF(&inner);
++it; ++it;
} }
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
@ -1275,14 +1290,14 @@ void GameRenderer::renderPlayingState(
} }
float lifeRatio = spark.lifeMs / spark.maxLifeMs; float lifeRatio = spark.lifeMs / spark.maxLifeMs;
Uint8 alpha = static_cast<Uint8>(std::clamp(lifeRatio, 0.0f, 1.0f) * 160.0f); Uint8 alpha = static_cast<Uint8>(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{ SDL_FRect sparkRect{
spark.x - spark.size * 0.5f, spark.x - spark.size * 0.5f,
spark.y - spark.size * 0.5f, spark.y - spark.size * 0.5f,
spark.size, spark.size,
spark.size * 1.4f spark.size * 1.4f
}; };
SDL_RenderFillRect(renderer, &sparkRect); rwrap->fillRectF(&sparkRect);
++it; ++it;
} }
} }
@ -1526,9 +1541,9 @@ void GameRenderer::renderPlayingState(
float barW = numbersW; float barW = numbersW;
float barY = numbersY + numbersH + 8.0f; 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_FRect track{barX, barY, barW, barHeight};
SDL_RenderFillRect(renderer, &track); rwrap->fillRectF(&track);
// Fill color brightness based on usage and highlight for top piece // Fill color brightness based on usage and highlight for top piece
float strength = (totalBlocks > 0) ? (float(blockCounts[i]) / float(totalBlocks)) : 0.0f; float strength = (totalBlocks > 0) ? (float(blockCounts[i]) / float(totalBlocks)) : 0.0f;
@ -1542,9 +1557,9 @@ void GameRenderer::renderPlayingState(
}; };
float fillW = barW * std::clamp(strength, 0.0f, 1.0f); 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_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) // Advance cursor to next row: after bar + gap (leave more space between blocks)
yCursor = barY + barHeight + rowGap + 6.0f; yCursor = barY + barHeight + rowGap + 6.0f;
@ -1719,10 +1734,10 @@ void GameRenderer::renderPlayingState(
SDL_FRect statsBg{statsPanelLeft, statsPanelTop, statsPanelWidth, statsPanelHeight}; SDL_FRect statsBg{statsPanelLeft, statsPanelTop, statsPanelWidth, statsPanelHeight};
if (scorePanelTex) { if (scorePanelTex) {
SDL_RenderTexture(renderer, scorePanelTex, nullptr, &statsBg); rwrap->renderTexture(scorePanelTex, nullptr, &statsBg);
} else { } else {
SDL_SetRenderDrawColor(renderer, 12, 18, 32, 205); rwrap->setDrawColor(SDL_Color{12, 18, 32, 205});
SDL_RenderFillRect(renderer, &statsBg); rwrap->fillRectF(&statsBg);
} }
scorePanelMetricsValid = true; scorePanelMetricsValid = true;
@ -1810,12 +1825,12 @@ void GameRenderer::renderPlayingState(
SDL_FRect panelDst{panelX, panelY, panelW, panelH}; SDL_FRect panelDst{panelX, panelY, panelW, panelH};
SDL_SetTextureBlendMode(holdPanelTex, SDL_BLENDMODE_BLEND); SDL_SetTextureBlendMode(holdPanelTex, SDL_BLENDMODE_BLEND);
SDL_SetTextureScaleMode(holdPanelTex, SDL_SCALEMODE_LINEAR); SDL_SetTextureScaleMode(holdPanelTex, SDL_SCALEMODE_LINEAR);
SDL_RenderTexture(renderer, holdPanelTex, nullptr, &panelDst); rwrap->renderTexture(holdPanelTex, nullptr, &panelDst);
} else { } else {
// fallback: draw a dark panel rect so UI is visible even without texture // 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_FRect panelDst{panelX, panelY, panelW, panelH};
SDL_RenderFillRect(renderer, &panelDst); rwrap->fillRectF(&panelDst);
} }
// Display "HOLD" label on right side // Display "HOLD" label on right side
@ -1854,6 +1869,8 @@ void GameRenderer::renderCoopPlayingState(
) { ) {
if (!renderer || !game || !pixelFont) return; if (!renderer || !game || !pixelFont) return;
auto rwrap = renderer::MakeSDLRenderer(renderer);
static SyncLineRenderer s_syncLine; static SyncLineRenderer s_syncLine;
static bool s_lastHadCompletedLines = false; static bool s_lastHadCompletedLines = false;
@ -1892,9 +1909,9 @@ void GameRenderer::renderCoopPlayingState(
float contentOffsetY = (winH - contentH) * 0.5f / contentScale; float contentOffsetY = (winH - contentH) * 0.5f / contentScale;
auto drawRectWithOffset = [&](float x, float y, float w, float h, SDL_Color c) { 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_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h};
SDL_RenderFillRect(renderer, &fr); rwrap->fillRectF(&fr);
}; };
static constexpr float COOP_GAP_PX = 20.0f; static constexpr float COOP_GAP_PX = 20.0f;
@ -1967,19 +1984,19 @@ void GameRenderer::renderCoopPlayingState(
}; };
// Grid lines (draw per-half so the gap is clean) // 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) { for (int x = 1; x < 10; ++x) {
float lineX = gridX + x * finalBlockSize; 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) { for (int x = 1; x < 10; ++x) {
float lineX = gridX + HALF_W + COOP_GAP_PX + x * finalBlockSize; 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) { for (int y = 1; y < CoopGame::ROWS; ++y) {
float lineY = gridY + y * finalBlockSize; float lineY = gridY + y * finalBlockSize;
SDL_RenderLine(renderer, gridX, lineY, gridX + HALF_W, lineY); rwrap->renderLine(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 + 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) // In-grid 3D starfield + ambient sparkles (match classic feel, per-half)
@ -2164,10 +2181,10 @@ void GameRenderer::renderCoopPlayingState(
float pulse = 0.5f + 0.5f * std::sin(sp.pulse); float pulse = 0.5f + 0.5f * std::sin(sp.pulse);
Uint8 alpha = static_cast<Uint8>(std::clamp(lifeRatio * pulse, 0.0f, 1.0f) * 255.0f); Uint8 alpha = static_cast<Uint8>(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; float half = sp.size * 0.5f;
SDL_FRect fr{ originX + sp.x - half, gridY + sp.y - half, sp.size, sp.size }; SDL_FRect fr{ originX + sp.x - half, gridY + sp.y - half, sp.size, sp.size };
SDL_RenderFillRect(renderer, &fr); rwrap->fillRectF(&fr);
++it; ++it;
} }
} }
@ -2186,14 +2203,14 @@ void GameRenderer::renderCoopPlayingState(
} }
float lifeRatio = spark.lifeMs / spark.maxLifeMs; float lifeRatio = spark.lifeMs / spark.maxLifeMs;
Uint8 alpha = static_cast<Uint8>(std::clamp(lifeRatio, 0.0f, 1.0f) * 160.0f); Uint8 alpha = static_cast<Uint8>(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{ SDL_FRect sparkRect{
spark.x - spark.size * 0.5f, spark.x - spark.size * 0.5f,
spark.y - spark.size * 0.5f, spark.y - spark.size * 0.5f,
spark.size, spark.size,
spark.size * 1.4f spark.size * 1.4f
}; };
SDL_RenderFillRect(renderer, &sparkRect); rwrap->fillRectF(&sparkRect);
++it; ++it;
} }
} }
@ -2225,17 +2242,17 @@ void GameRenderer::renderCoopPlayingState(
} }
if (rs.leftFull && rs.rightFull) { 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_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_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) { } 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 w = HALF_W;
float x = rs.leftFull ? gridX : (gridX + HALF_W + COOP_GAP_PX); float x = rs.leftFull ? gridX : (gridX + HALF_W + COOP_GAP_PX);
SDL_FRect fr{x, rowY, w, finalBlockSize}; SDL_FRect fr{x, rowY, w, finalBlockSize};
SDL_RenderFillRect(renderer, &fr); rwrap->fillRectF(&fr);
} }
} }
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
@ -2432,7 +2449,7 @@ void GameRenderer::renderCoopPlayingState(
float elapsed = static_cast<float>(nowTicks - sf.startTick); float elapsed = static_cast<float>(nowTicks - sf.startTick);
float t = sf.durationMs <= 0.0f ? 1.0f : std::clamp(elapsed / sf.durationMs, 0.0f, 1.0f); float t = sf.durationMs <= 0.0f ? 1.0f : std::clamp(elapsed / sf.durationMs, 0.0f, 1.0f);
Uint8 alpha = static_cast<Uint8>(std::lround(255.0f * t)); Uint8 alpha = static_cast<Uint8>(std::lround(255.0f * t));
if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, alpha); if (blocksTex) rwrap->setTextureAlphaMod(blocksTex, alpha);
int minCy = 4; int minCy = 4;
int maxCy = -1; int maxCy = -1;
@ -2479,7 +2496,7 @@ void GameRenderer::renderCoopPlayingState(
drawBlockTexturePublic(renderer, blocksTex, px, py, sf.tileSize, livePiece.type); 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 // End fade after duration, but never stop while we are pinning (otherwise
// I can briefly disappear until it becomes visible in the real grid). // I can briefly disappear until it becomes visible in the real grid).
@ -2499,12 +2516,12 @@ void GameRenderer::renderCoopPlayingState(
float py = gridY + (float)pyIdx * finalBlockSize + offsets.second; float py = gridY + (float)pyIdx * finalBlockSize + offsets.second;
if (isGhost) { if (isGhost) {
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); 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_FRect rect = {px + 2.0f, py + 2.0f, finalBlockSize - 4.0f, finalBlockSize - 4.0f};
SDL_RenderFillRect(renderer, &rect); rwrap->fillRectF(&rect);
SDL_SetRenderDrawColor(renderer, 180, 180, 180, 30); rwrap->setDrawColor(SDL_Color{180, 180, 180, 30});
SDL_FRect border = {px + 1.0f, py + 1.0f, finalBlockSize - 2.0f, finalBlockSize - 2.0f}; SDL_FRect border = {px + 1.0f, py + 1.0f, finalBlockSize - 2.0f, finalBlockSize - 2.0f};
SDL_RenderRect(renderer, &border); rwrap->drawRectF(&border);
} else { } else {
drawBlockTexturePublic(renderer, blocksTex, px, py, finalBlockSize, p.type); drawBlockTexturePublic(renderer, blocksTex, px, py, finalBlockSize, p.type);
} }
@ -2579,7 +2596,7 @@ void GameRenderer::renderCoopPlayingState(
auto drawNextPanel = [&](float panelX, float panelY, const CoopGame::Piece& piece) { auto drawNextPanel = [&](float panelX, float panelY, const CoopGame::Piece& piece) {
SDL_FRect panel{ panelX, panelY, nextPanelW, nextPanelH }; SDL_FRect panel{ panelX, panelY, nextPanelW, nextPanelH };
if (nextPanelTex) { if (nextPanelTex) {
SDL_RenderTexture(renderer, nextPanelTex, nullptr, &panel); rwrap->renderTexture(nextPanelTex, nullptr, &panel);
} else { } else {
drawRectWithOffset(panel.x - contentOffsetX, panel.y - contentOffsetY, panel.w, panel.h, SDL_Color{18,22,30,200}); drawRectWithOffset(panel.x - contentOffsetX, panel.y - contentOffsetY, panel.w, panel.h, SDL_Color{18,22,30,200});
} }
@ -2707,10 +2724,10 @@ void GameRenderer::renderCoopPlayingState(
float panelX = (side == CoopGame::PlayerSide::Right) ? (columnRightX - panelW) : columnLeftX; float panelX = (side == CoopGame::PlayerSide::Right) ? (columnRightX - panelW) : columnLeftX;
SDL_FRect panelBg{ panelX, panelY, panelW, panelH }; SDL_FRect panelBg{ panelX, panelY, panelW, panelH };
if (scorePanelTex) { if (scorePanelTex) {
SDL_RenderTexture(renderer, scorePanelTex, nullptr, &panelBg); rwrap->renderTexture(scorePanelTex, nullptr, &panelBg);
} else { } else {
SDL_SetRenderDrawColor(renderer, 12, 18, 32, 205); rwrap->setDrawColor(SDL_Color{12, 18, 32, 205});
SDL_RenderFillRect(renderer, &panelBg); rwrap->fillRectF(&panelBg);
} }
float textDrawX = panelX + statsPanelPadLeft; float textDrawX = panelX + statsPanelPadLeft;
@ -2777,9 +2794,10 @@ void GameRenderer::renderExitPopup(
SDL_SetRenderViewport(renderer, nullptr); SDL_SetRenderViewport(renderer, nullptr);
SDL_SetRenderScale(renderer, 1.0f, 1.0f); 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_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 scale = std::max(0.8f, logicalScale);
const float panelW = 740.0f * scale; const float panelW = 740.0f * scale;
@ -2797,8 +2815,8 @@ void GameRenderer::renderExitPopup(
panel.w + 4.0f * scale, panel.w + 4.0f * scale,
panel.h + 4.0f * scale panel.h + 4.0f * scale
}; };
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140); rwrap->setDrawColor(SDL_Color{0, 0, 0, 140});
SDL_RenderFillRect(renderer, &shadow); rwrap->fillRectF(&shadow);
const std::array<SDL_Color, 3> panelLayers{ const std::array<SDL_Color, 3> panelLayers{
SDL_Color{7, 10, 22, 255}, SDL_Color{7, 10, 22, 255},
@ -2814,12 +2832,12 @@ void GameRenderer::renderExitPopup(
panel.h - inset * 2.0f panel.h - inset * 2.0f
}; };
SDL_Color c = panelLayers[i]; SDL_Color c = panelLayers[i];
SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); rwrap->setDrawColor(c);
SDL_RenderFillRect(renderer, &layer); rwrap->fillRectF(&layer);
} }
SDL_SetRenderDrawColor(renderer, 60, 90, 150, 255); rwrap->setDrawColor(SDL_Color{60, 90, 150, 255});
SDL_RenderRect(renderer, &panel); rwrap->drawRectF(&panel);
SDL_FRect insetFrame{ SDL_FRect insetFrame{
panel.x + 10.0f * scale, panel.x + 10.0f * scale,
@ -2827,8 +2845,8 @@ void GameRenderer::renderExitPopup(
panel.w - 20.0f * scale, panel.w - 20.0f * scale,
panel.h - 20.0f * scale panel.h - 20.0f * scale
}; };
SDL_SetRenderDrawColor(renderer, 24, 45, 84, 255); rwrap->setDrawColor(SDL_Color{24, 45, 84, 255});
SDL_RenderRect(renderer, &insetFrame); rwrap->drawRectF(&insetFrame);
const float contentPad = 44.0f * scale; const float contentPad = 44.0f * scale;
float textX = panel.x + contentPad; float textX = panel.x + contentPad;
@ -2842,9 +2860,9 @@ void GameRenderer::renderExitPopup(
pixelFont->draw(renderer, textX, cursorY, title, titleScale, SDL_Color{255, 224, 130, 255}); pixelFont->draw(renderer, textX, cursorY, title, titleScale, SDL_Color{255, 224, 130, 255});
cursorY += titleH + 18.0f * scale; 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_FRect divider{textX, cursorY, contentWidth, 2.0f * scale};
SDL_RenderFillRect(renderer, &divider); rwrap->fillRectF(&divider);
cursorY += 26.0f * scale; cursorY += 26.0f * scale;
const std::array<const char*, 2> lines{ const std::array<const char*, 2> lines{
@ -2885,29 +2903,29 @@ void GameRenderer::renderExitPopup(
SDL_Color border = selected ? SDL_Color{255, 225, 150, 255} : SDL_Color{90, 120, 170, 255}; 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_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_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); rwrap->setDrawColor(body);
SDL_RenderFillRect(renderer, &btn); rwrap->fillRectF(&btn);
SDL_FRect topStrip{btn.x, btn.y, btn.w, 6.0f * scale}; SDL_FRect topStrip{btn.x, btn.y, btn.w, 6.0f * scale};
SDL_SetRenderDrawColor(renderer, topEdge.r, topEdge.g, topEdge.b, topEdge.a); rwrap->setDrawColor(topEdge);
SDL_RenderFillRect(renderer, &topStrip); rwrap->fillRectF(&topStrip);
SDL_SetRenderDrawColor(renderer, border.r, border.g, border.b, border.a); rwrap->setDrawColor(border);
SDL_RenderRect(renderer, &btn); rwrap->drawRectF(&btn);
if (selected) { if (selected) {
SDL_SetRenderDrawColor(renderer, 255, 230, 160, 90); rwrap->setDrawColor(SDL_Color{255, 230, 160, 90});
SDL_FRect glow{ SDL_FRect glow{
btn.x - 6.0f * scale, btn.x - 6.0f * scale,
btn.y - 6.0f * scale, btn.y - 6.0f * scale,
btn.w + 12.0f * scale, btn.w + 12.0f * scale,
btn.h + 12.0f * scale btn.h + 12.0f * scale
}; };
SDL_RenderRect(renderer, &glow); rwrap->drawRectF(&glow);
} }
const float labelScale = 1.35f * scale; const float labelScale = 1.35f * scale;
@ -2948,9 +2966,10 @@ void GameRenderer::renderPauseOverlay(
SDL_SetRenderScale(renderer, 1.0f, 1.0f); SDL_SetRenderScale(renderer, 1.0f, 1.0f);
// Draw full screen overlay (darken) // 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_FRect pauseOverlay{0, 0, winW, winH};
SDL_RenderFillRect(renderer, &pauseOverlay); rwrap->fillRectF(&pauseOverlay);
// Draw centered text // Draw centered text
const char* pausedText = "PAUSED"; const char* pausedText = "PAUSED";

59
src/logic/Board.cpp Normal file
View File

@ -0,0 +1,59 @@
#include "Board.h"
#include <algorithm>
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

32
src/logic/Board.h Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include <vector>
#include <cstdint>
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<Cell>& data() const { return grid_; }
private:
std::vector<Cell> grid_; // row-major: y*Width + x
};
} // namespace logic

38
src/renderer/Renderer.h Normal file
View File

@ -0,0 +1,38 @@
// Renderer abstraction (minimal scaffold)
#pragma once
#include <memory>
#include <SDL3/SDL.h>
namespace renderer {
class Renderer {
public:
virtual ~Renderer() = default;
// Create/destroy textures
virtual SDL_Texture* createTextureFromSurface(SDL_Surface* surf) = 0;
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;
};
// Factory helper implemented by SDL-specific backend
std::unique_ptr<Renderer> MakeSDLRenderer(SDL_Renderer* rdr);
} // namespace renderer

View File

@ -0,0 +1,27 @@
// Clean renderer interface for local use
#pragma once
#include <memory>
#include <SDL3/SDL.h>
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<Renderer> MakeSDLRenderer(SDL_Renderer* rdr);
} // namespace renderer

View File

@ -0,0 +1,80 @@
#include "Renderer_iface.h"
#include <SDL3/SDL.h>
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;
// 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<float>(src->x); fs.y = static_cast<float>(src->y); fs.w = static_cast<float>(src->w); fs.h = static_cast<float>(src->h); ps = &fs; }
if (dst) { fd.x = static_cast<float>(dst->x); fd.y = static_cast<float>(dst->y); fd.w = static_cast<float>(dst->w); fd.h = static_cast<float>(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 {
if (!rdr_) return;
SDL_SetRenderDrawColor(rdr_, color.r, color.g, color.b, color.a);
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_);
}
private:
SDL_Renderer* rdr_ = nullptr;
};
// Factory helper
std::unique_ptr<Renderer> MakeSDLRenderer(SDL_Renderer* rdr) {
return std::make_unique<SDLRendererImpl>(rdr);
}
} // namespace renderer

View File

@ -0,0 +1,41 @@
#include "ResourceManager.h"
#include <future>
namespace resources {
ResourceManager::ResourceManager() = default;
ResourceManager::~ResourceManager() = default;
std::future<std::shared_ptr<void>> ResourceManager::loadAsync(const std::string& key, std::function<std::shared_ptr<void>(const std::string&)> loader)
{
// Quick check for existing cached resource
{
std::lock_guard<std::mutex> 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<std::mutex> lk(mutex_);
cache_[key] = res; // store strong reference
}
return res;
});
}
void ResourceManager::put(const std::string& key, std::shared_ptr<void> resource)
{
std::lock_guard<std::mutex> lk(mutex_);
cache_[key] = resource; // store strong reference so callers using raw pointers stay valid
}
} // namespace resources

View File

@ -0,0 +1,43 @@
#pragma once
#include <string>
#include <memory>
#include <unordered_map>
#include <mutex>
#include <future>
#include <functional>
namespace resources {
class ResourceManager {
public:
ResourceManager();
~ResourceManager();
// Return cached resource if available and of the right type
template<typename T>
std::shared_ptr<T> get(const std::string& key)
{
std::lock_guard<std::mutex> 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<T>(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<std::shared_ptr<void>> loadAsync(const std::string& key, std::function<std::shared_ptr<void>(const std::string&)> loader);
// Insert a resource into the cache (thread-safe)
void put(const std::string& key, std::shared_ptr<void> resource);
private:
// Keep strong ownership of cached resources so they remain valid
// while present in the cache.
std::unordered_map<std::string, std::shared_ptr<void>> cache_;
std::mutex mutex_;
};
} // namespace resources

View File

@ -8,6 +8,7 @@
#include "../core/Settings.h" #include "../core/Settings.h"
#include "../core/state/StateManager.h" #include "../core/state/StateManager.h"
#include "../audio/Audio.h" #include "../audio/Audio.h"
#include "../audio/AudioManager.h"
#include "../audio/SoundEffect.h" #include "../audio/SoundEffect.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3/SDL_render.h> #include <SDL3/SDL_render.h>
@ -180,7 +181,7 @@ void MenuState::showCoopSetupPanel(bool show, bool resumeMusic) {
coopSetupStep = CoopSetupStep::ChoosePartner; coopSetupStep = CoopSetupStep::ChoosePartner;
// Resume menu music only when requested (ESC should pass resumeMusic=false) // Resume menu music only when requested (ESC should pass resumeMusic=false)
if (resumeMusic && ctx.musicEnabled && *ctx.musicEnabled) { if (resumeMusic && ctx.musicEnabled && *ctx.musicEnabled) {
Audio::instance().playMenuMusic(); if (auto sys = AudioManager::get()) sys->playMenuMusic();
} }
} }
} }

View File

@ -2,6 +2,7 @@
#include "../core/state/StateManager.h" #include "../core/state/StateManager.h"
#include "../graphics/ui/Font.h" #include "../graphics/ui/Font.h"
#include "../audio/Audio.h" #include "../audio/Audio.h"
#include "../audio/AudioManager.h"
#include "../audio/SoundEffect.h" #include "../audio/SoundEffect.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
@ -220,7 +221,7 @@ void OptionsState::toggleFullscreen() {
} }
void OptionsState::toggleMusic() { void OptionsState::toggleMusic() {
Audio::instance().toggleMute(); if (auto sys = AudioManager::get()) sys->toggleMute();
// If muted, music is disabled. If not muted, music is enabled. // If muted, music is disabled. If not muted, music is enabled.
// Note: Audio::instance().isMuted() returns true if muted. // Note: Audio::instance().isMuted() returns true if muted.
// But Audio class doesn't expose isMuted directly in header usually? // But Audio class doesn't expose isMuted directly in header usually?

View File

@ -3,6 +3,7 @@
#include "../video/VideoPlayer.h" #include "../video/VideoPlayer.h"
#include "../audio/Audio.h" #include "../audio/Audio.h"
#include "../audio/AudioManager.h"
#include "../core/state/StateManager.h" #include "../core/state/StateManager.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
@ -104,7 +105,7 @@ void VideoState::startAudioIfReady() {
if (m_audioPcm.empty()) return; if (m_audioPcm.empty()) return;
// Use the existing audio output path (same device as music/SFX). // Use the existing audio output path (same device as music/SFX).
Audio::instance().playSfx(m_audioPcm, m_audioChannels, m_audioRate, 1.0f); if (auto sys = AudioManager::get()) sys->playSfx(m_audioPcm, m_audioChannels, m_audioRate, 1.0f);
m_audioStarted = true; m_audioStarted = true;
} }

38
tests/test_board.cpp Normal file
View File

@ -0,0 +1,38 @@
#include "../src/logic/Board.h"
#include <gtest/gtest.h>
using logic::Board;
TEST(BoardTests, InitiallyEmpty)
{
Board b;
for (int y = 0; y < Board::Height; ++y)
for (int x = 0; x < Board::Width; ++x)
EXPECT_EQ(b.at(x, y), Board::Cell::Empty);
}
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();
EXPECT_EQ(cleared, 1);
for (int x = 0; x < Board::Width; ++x) EXPECT_EQ(b.at(x, Board::Height - 1), Board::Cell::Empty);
}
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); }
int cleared = b.clearFullLines();
EXPECT_EQ(cleared, 2);
}
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@ -8,6 +8,7 @@
}, },
"enet", "enet",
"catch2", "catch2",
"gtest",
"cpr", "cpr",
"nlohmann-json", "nlohmann-json",
"ffmpeg" "ffmpeg"