diff --git a/examples/actions/main.cpp b/examples/actions/main.cpp index e1134cd..693d83f 100644 --- a/examples/actions/main.cpp +++ b/examples/actions/main.cpp @@ -150,7 +150,7 @@ int main() asw::display::present(); } - asw::core::cleanup(); + asw::core::shutdown(); return 0; } diff --git a/examples/controller/main.cpp b/examples/controller/main.cpp index e5ec765..e0f5533 100644 --- a/examples/controller/main.cpp +++ b/examples/controller/main.cpp @@ -159,7 +159,7 @@ int main() asw::display::present(); } - asw::core::cleanup(); + asw::core::shutdown(); return 0; } diff --git a/examples/keyboard/main.cpp b/examples/keyboard/main.cpp index 9132e33..0330a56 100644 --- a/examples/keyboard/main.cpp +++ b/examples/keyboard/main.cpp @@ -26,6 +26,8 @@ int main() while (!asw::core::is_exiting()) { asw::core::update(); + const auto& keyboard = asw::input::get_keyboard(); + // --- Movement (held) --- if (asw::input::get_key(asw::input::Key::W) || asw::input::get_key(asw::input::Key::Up)) { pos.y -= speed; @@ -68,15 +70,15 @@ int main() } // --- Text input --- - if (!asw::input::text_input.empty()) { - asw::log::info("Text input: {}", asw::input::text_input); + if (!asw::input::get_text_input().empty()) { + asw::log::info("Text input: {}", asw::input::get_text_input()); } // --- Draw --- asw::display::clear(asw::color::darkslategray); // Box - turns cyan when any key is held - const bool any_held = asw::input::keyboard.any_pressed; + const bool any_held = keyboard.any_pressed; asw::draw::rect_fill( { pos, { box_size, box_size } }, any_held ? asw::color::cyan : asw::color::white); asw::draw::rect({ pos, { box_size, box_size } }, asw::color::gray); @@ -99,7 +101,7 @@ int main() asw::display::present(); } - asw::core::cleanup(); + asw::core::shutdown(); return 0; } diff --git a/examples/mouse/main.cpp b/examples/mouse/main.cpp index 888eab0..98e461b 100644 --- a/examples/mouse/main.cpp +++ b/examples/mouse/main.cpp @@ -2,7 +2,7 @@ /// @brief Mouse input example /// /// Demonstrates: -/// - Reading mouse position and delta via asw::input::mouse +/// - Reading mouse position and delta via mouse /// - Polling buttons with get_mouse_button_down() / get_mouse_button_up() / get_mouse_button() /// - Scroll wheel via mouse.z /// - Changing cursor style with set_cursor() @@ -28,17 +28,18 @@ int main() while (!asw::core::is_exiting()) { asw::core::update(); + const auto& mouse = asw::input::get_mouse(); // --- Button events --- if (asw::input::get_mouse_button_down(asw::input::MouseButton::Left)) { circle_color = asw::color::red; - asw::log::info("Left button pressed at ({:.0f}, {:.0f})", asw::input::mouse.position.x, - asw::input::mouse.position.y); + asw::log::info( + "Left button pressed at ({:.0f}, {:.0f})", mouse.position.x, mouse.position.y); } if (asw::input::get_mouse_button_down(asw::input::MouseButton::Right)) { circle_color = asw::color::blue; - asw::log::info("Right button pressed at ({:.0f}, {:.0f})", asw::input::mouse.position.x, - asw::input::mouse.position.y); + asw::log::info( + "Right button pressed at ({:.0f}, {:.0f})", mouse.position.x, mouse.position.y); } if (asw::input::get_mouse_button_up(asw::input::MouseButton::Left) || asw::input::get_mouse_button_up(asw::input::MouseButton::Right)) { @@ -46,9 +47,9 @@ int main() } // --- Scroll wheel --- - if (asw::input::mouse.z != 0.0F) { - radius = std::clamp(radius + asw::input::mouse.z * 5.0F, 5.0F, 150.0F); - asw::log::info("Scroll: {:.1f} radius: {:.0f}", asw::input::mouse.z, radius); + if (mouse.z != 0.0F) { + radius = std::clamp(radius + mouse.z * 5.0F, 5.0F, 150.0F); + asw::log::info("Scroll: {:.1f} radius: {:.0f}", mouse.z, radius); } // --- Cursor style based on button held --- @@ -67,7 +68,7 @@ int main() // --- Draw --- asw::display::clear(asw::color::darkslategray); - const auto& mp = asw::input::mouse.position; + const auto& mp = mouse.position; // Crosshair lines const auto win = asw::display::get_logical_size(); @@ -79,7 +80,7 @@ int main() asw::draw::circle(mp, radius, asw::color::white); // Delta indicator (shows mouse movement direction) - const auto& delta = asw::input::mouse.change; + const auto& delta = mouse.change; if (delta.x != 0.0F || delta.y != 0.0F) { asw::draw::line( mp, { mp.x + delta.x * 10.0F, mp.y + delta.y * 10.0F }, asw::color::yellow); @@ -103,7 +104,7 @@ int main() asw::display::present(); } - asw::core::cleanup(); + asw::core::shutdown(); return 0; } diff --git a/examples/primitives/main.cpp b/examples/primitives/main.cpp index 84dffe1..78cebe1 100644 --- a/examples/primitives/main.cpp +++ b/examples/primitives/main.cpp @@ -107,7 +107,7 @@ int main() asw::display::present(); } - asw::core::cleanup(); + asw::core::shutdown(); return 0; } diff --git a/include/asw/modules/core.h b/include/asw/modules/core.h index 70c9dab..941e1c6 100644 --- a/include/asw/modules/core.h +++ b/include/asw/modules/core.h @@ -38,7 +38,7 @@ bool is_exiting(); /// @brief Cleanup resources used by the core module. Should be called on application exit. /// -void cleanup(); +void shutdown(); } // namespace asw::core diff --git a/include/asw/modules/display.h b/include/asw/modules/display.h index 79ebe18..0655696 100644 --- a/include/asw/modules/display.h +++ b/include/asw/modules/display.h @@ -17,11 +17,31 @@ namespace asw::display { -/// @brief The renderer for the display module. -extern asw::Renderer* renderer; +/// @brief Initialize the display module. Called by asw::core::init(). +/// +/// @param width The logical width of the display. +/// @param height The logical height of the display. +/// @param scale The initial window scale factor. +/// +void _init(int width, int height, int scale); -/// @brief The window for the display module. -extern asw::Window* window; +/// @brief Shut down the display module. Called by asw::core::shutdown(). +/// Nulls the renderer and window pointers before destroying them, so any +/// outstanding shared_ptr asset deleters see a null renderer and become no-ops. +/// +void _shutdown(); + +/// @brief Get the SDL renderer. +/// +/// @return Pointer to the SDL_Renderer, or nullptr if not initialized. +/// +asw::Renderer* get_renderer(); + +/// @brief Get the SDL window. +/// +/// @return Pointer to the SDL_Window, or nullptr if not initialized. +/// +asw::Window* get_window(); /// @brief Set the title of the window. /// diff --git a/include/asw/modules/input.h b/include/asw/modules/input.h index 49dd862..197cac8 100644 --- a/include/asw/modules/input.h +++ b/include/asw/modules/input.h @@ -315,8 +315,11 @@ using MouseState = struct MouseState { std::array down { false }; }; -/// @brief Global mouse state. -extern MouseState mouse; +/// @brief Get the current mouse state. +/// +/// @return Const reference to the current MouseState. +/// +const MouseState& get_mouse(); /// @brief Check if a mouse button is down. /// @@ -354,9 +357,11 @@ using KeyState = struct KeyState { int last_pressed { -1 }; }; -/// @brief Global keyboard state. +/// @brief Get the current keyboard state. +/// +/// @return Const reference to the current KeyState. /// -extern KeyState keyboard; +const KeyState& get_keyboard(); /// @brief Check if a key is down. /// @@ -474,14 +479,20 @@ int get_controller_count(); /// @brief Get the name of a controller. std::string get_controller_name(uint32_t index); -/// @brief Text input received this frame. -extern std::string text_input; +/// @brief Get the text input received this frame. +/// +/// @return Const reference to the text input string. +/// +const std::string& get_text_input(); /// @brief Reset all input states. Called by the core. void reset(); /// Event Hooks +/// @brief Append text to this frame's text input. Called by the core. +void _append_text(const char* text); + /// @brief Key down hook void _key_down(SDL_Scancode scancode); diff --git a/include/asw/modules/scene.h b/include/asw/modules/scene.h index 9806c15..160cb7f 100644 --- a/include/asw/modules/scene.h +++ b/include/asw/modules/scene.h @@ -69,22 +69,22 @@ template class Scene { virtual void update(float dt) { // Erase inactive objects - std::erase_if(objects_, [](const auto& obj) { return !obj->alive; }); + std::erase_if(_objects, [](const auto& obj) { return !obj->alive; }); // Update all objects in the scene - for (auto const& obj : objects_) { + for (auto const& obj : _objects) { if (obj->active && obj->alive) { obj->update(dt); } } // Create new objects - for (auto const& obj : obj_to_create_) { - objects_.push_back(obj); + for (auto const& obj : _obj_to_create) { + _objects.push_back(obj); } // Clear the objects to create - obj_to_create_.clear(); + _obj_to_create.clear(); }; /// @brief Draw the game scene. @@ -94,9 +94,9 @@ template class Scene { virtual void draw() { // Sort objects by z-index - std::ranges::sort(objects_, std::less {}, &game::GameObject::z_index); + std::ranges::sort(_objects, std::less {}, &game::GameObject::z_index); - for (auto const& obj : objects_) { + for (auto const& obj : _objects) { if (obj->active) { obj->draw(); } @@ -110,7 +110,7 @@ template class Scene { /// virtual void cleanup() { - objects_.clear(); + _objects.clear(); }; /// @brief Add a game object to the scene. @@ -119,7 +119,7 @@ template class Scene { /// void register_object(const std::shared_ptr& obj) { - objects_.push_back(obj); + _objects.push_back(obj); } /// @brief Create a new game object in the scene. @@ -135,7 +135,7 @@ template class Scene { "ObjectType must be constructible with the given arguments"); auto obj = std::make_shared(std::forward(args)...); - obj_to_create_.emplace_back(obj); + _obj_to_create.emplace_back(obj); return obj; } @@ -145,7 +145,7 @@ template class Scene { /// const std::vector>& get_objects() const { - return objects_; + return _objects; } /// @brief Get game objects of a specific type in the scene. @@ -160,7 +160,7 @@ template class Scene { "ObjectType must be derived from Scene"); std::vector> result; - for (const auto& obj : objects_) { + for (const auto& obj : _objects) { if (auto casted_obj = std::dynamic_pointer_cast(obj)) { result.push_back(casted_obj); } @@ -174,10 +174,10 @@ template class Scene { private: /// @brief Collection of game objects in the scene. - std::vector> objects_; + std::vector> _objects; /// @brief Objects to be created in the next frame. - std::vector> obj_to_create_; + std::vector> _obj_to_create; }; /// @brief SceneManager class for managing game scenes. @@ -212,7 +212,7 @@ template class SceneManager { "SceneType must be constructible with the given arguments"); auto scene = std::make_shared(std::forward(args)...); - scenes_[scene_id] = scene; + _scenes[scene_id] = scene; } /// @brief Set the next scene @@ -221,8 +221,8 @@ template class SceneManager { /// void set_next_scene(const T scene_id) { - next_scene_ = scene_id; - has_next_scene_ = true; + _next_scene = scene_id; + _has_next_scene = true; } /// @brief Main loop for the scene engine. If this is not enough, or you @@ -248,9 +248,9 @@ template class SceneManager { time_start = std::chrono::high_resolution_clock::now(); lag += std::chrono::duration_cast(delta_time); - while (lag >= this->timestep_) { - update(std::chrono::duration(this->timestep_).count()); - lag -= this->timestep_; + while (lag >= this->_timestep) { + update(std::chrono::duration(this->_timestep).count()); + lag -= this->_timestep; } // Draw @@ -259,14 +259,30 @@ template class SceneManager { frames++; if (std::chrono::high_resolution_clock::now() - last_second >= 1s) { - fps_ = frames; + _fps = frames; frames = 0; last_second = last_second + 1s; } } + + // Cleanup + cleanup(); #endif } + /// @brief Destroy the scene manager and clean up resources. + /// This function is called when the scene manager is destroyed to clean up any resources used + /// by the scene engine. + /// + void cleanup() + { + if (_active_scene != nullptr) { + _active_scene->cleanup(); + } + + _scenes.clear(); + } + /// @brief Update the current scene. /// /// @param dt The time in seconds since the last update. @@ -280,8 +296,8 @@ template class SceneManager { asw::core::update(); change_scene(); - if (active_scene_ != nullptr) { - active_scene_->update(dt); + if (_active_scene != nullptr) { + _active_scene->update(dt); } } @@ -293,9 +309,9 @@ template class SceneManager { return; } - if (active_scene_ != nullptr) { + if (_active_scene != nullptr) { asw::display::clear(); - active_scene_->draw(); + _active_scene->draw(); asw::display::present(); } } @@ -306,7 +322,7 @@ template class SceneManager { /// void set_timestep(std::chrono::nanoseconds ts) { - timestep_ = ts; + _timestep = ts; } /// @brief Get the current timestep. @@ -315,7 +331,7 @@ template class SceneManager { /// std::chrono::nanoseconds get_timestep() const { - return timestep_; + return _timestep; } /// @brief Get the current FPS. Only applies to the managed loop. @@ -324,7 +340,7 @@ template class SceneManager { /// int get_fps() const { - return fps_; + return _fps; } private: @@ -332,39 +348,39 @@ template class SceneManager { /// void change_scene() { - if (!has_next_scene_) { + if (!_has_next_scene) { return; } - if (active_scene_ != nullptr) { - active_scene_->cleanup(); + if (_active_scene != nullptr) { + _active_scene->cleanup(); } - if (auto it = scenes_.find(next_scene_); it != scenes_.end()) { - active_scene_ = it->second; - active_scene_->init(); + if (auto it = _scenes.find(_next_scene); it != _scenes.end()) { + _active_scene = it->second; + _active_scene->init(); } - has_next_scene_ = false; + _has_next_scene = false; } /// @brief The current scene of the scene engine. - std::shared_ptr> active_scene_ { nullptr }; + std::shared_ptr> _active_scene { nullptr }; /// @brief The next scene of the scene engine. - T next_scene_; + T _next_scene; /// @brief Flag to indicate if there is a next scene to change to. - bool has_next_scene_ { false }; + bool _has_next_scene { false }; /// @brief Collection of all scenes registered in the scene engine. - std::unordered_map>> scenes_; + std::unordered_map>> _scenes; /// @brief Fixed timestep for the game loop. - std::chrono::nanoseconds timestep_ { DEFAULT_TIMESTEP }; + std::chrono::nanoseconds _timestep { DEFAULT_TIMESTEP }; /// @brief FPS Counter for managed loop. - int fps_ { 0 }; + int _fps { 0 }; #ifdef __EMSCRIPTEN__ /// @brief Pointer to the current instance of the scene manager. diff --git a/include/asw/modules/sound.h b/include/asw/modules/sound.h index 64c2c27..4b37599 100644 --- a/include/asw/modules/sound.h +++ b/include/asw/modules/sound.h @@ -13,17 +13,22 @@ namespace asw::sound { -/// @brief Global mixer device -extern MIX_Mixer* mixer; - -/// @brief Initialize the sound module. Must be called before using any other -/// sound functions. This is called automatically by asw::core::init(), -/// so you don't need to call it +/// @brief Initialize the sound module. Called automatically by asw::core::init(). /// /// @return True if initialization was successful, false otherwise. /// bool _init(); +/// @brief Shut down the sound module. Called automatically by asw::core::shutdown(). +/// +void _shutdown(); + +/// @brief Get the SDL mixer device. +/// +/// @return Pointer to the MIX_Mixer, or nullptr if not initialized. +/// +MIX_Mixer* get_mixer(); + /// @brief Play a sample. /// /// @param sample Sample to play diff --git a/src/modules/action.cpp b/src/modules/action.cpp index b20917e..a76d286 100644 --- a/src/modules/action.cpp +++ b/src/modules/action.cpp @@ -77,10 +77,10 @@ bool binding_is_pressed(const asw::input::ActionBinding& binding) using T = std::decay_t; if constexpr (std::is_same_v) { - return asw::input::keyboard.pressed[static_cast(b.key)]; + return asw::input::get_key_down(b.key); } else if constexpr (std::is_same_v) { - return asw::input::mouse.pressed[static_cast(b.button)]; + return asw::input::get_mouse_button_down(b.button); } else if constexpr (std::is_same_v) { return asw::input::get_controller_button_down(b.controller_index, b.button); @@ -101,10 +101,10 @@ bool binding_is_released(const asw::input::ActionBinding& binding) using T = std::decay_t; if constexpr (std::is_same_v) { - return asw::input::keyboard.released[static_cast(b.key)]; + return asw::input::get_key_up(b.key); } else if constexpr (std::is_same_v) { - return asw::input::mouse.released[static_cast(b.button)]; + return asw::input::get_mouse_button_up(b.button); } else if constexpr (std::is_same_v) { return asw::input::get_controller_button_up(b.controller_index, b.button); diff --git a/src/modules/assets.cpp b/src/modules/assets.cpp index 71996f4..eaec0a4 100644 --- a/src/modules/assets.cpp +++ b/src/modules/assets.cpp @@ -37,7 +37,7 @@ std::string asw::assets::get_path(const std::string& filename) asw::Texture asw::assets::load_texture(const std::string& filename) { const auto full_path = get_path(filename); - SDL_Texture* temp = IMG_LoadTexture(asw::display::renderer, full_path.c_str()); + SDL_Texture* temp = IMG_LoadTexture(asw::display::get_renderer(), full_path.c_str()); if (temp == nullptr) { asw::util::abort_on_error("Failed to load texture: " + full_path); @@ -46,7 +46,14 @@ asw::Texture asw::assets::load_texture(const std::string& filename) SDL_SetTextureScaleMode(temp, SDL_SCALEMODE_NEAREST); SDL_SetTextureBlendMode(temp, SDL_BLENDMODE_BLEND); - return { temp, SDL_DestroyTexture }; + // Guard: if the renderer is already gone when the deleter fires (e.g. a + // shared_ptr outliving core::shutdown()), skip the SDL call - SDL has + // already freed the texture via SDL_DestroyRenderer. + return { temp, [](SDL_Texture* t) { + if (asw::display::get_renderer() != nullptr) { + SDL_DestroyTexture(t); + } + } }; } asw::Texture asw::assets::load_texture(const std::string& filename, const std::string& key) @@ -76,14 +83,19 @@ void asw::assets::unload_texture(const std::string& key) asw::Texture asw::assets::create_texture(int w, int h) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { asw::util::abort_on_error("Renderer not initialized"); } - SDL_Texture* text = SDL_CreateTexture( - asw::display::renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h); + SDL_Texture* text + = SDL_CreateTexture(r, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h); - return { text, SDL_DestroyTexture }; + return { text, [](SDL_Texture* t) { + if (asw::display::get_renderer() != nullptr) { + SDL_DestroyTexture(t); + } + } }; } // --- Font --- @@ -97,7 +109,13 @@ asw::Font asw::assets::load_font(const std::string& filename, float size) asw::util::abort_on_error("Failed to load font: " + full_path); } - return { temp, TTF_CloseFont }; + // Use renderer as proxy for "SDL still alive" - renderer is nulled in + // display::_shutdown() before TTF_Quit() is called. + return { temp, [](TTF_Font* f) { + if (asw::display::get_renderer() != nullptr) { + TTF_CloseFont(f); + } + } }; } asw::Font asw::assets::load_font(const std::string& filename, float size, const std::string& key) @@ -130,13 +148,17 @@ void asw::assets::unload_font(const std::string& key) asw::Sample asw::assets::load_sample(const std::string& filename) { const auto full_path = get_path(filename); - MIX_Audio* temp = MIX_LoadAudio(asw::sound::mixer, full_path.c_str(), true); + MIX_Audio* temp = MIX_LoadAudio(asw::sound::get_mixer(), full_path.c_str(), true); if (temp == nullptr) { asw::util::abort_on_error("Failed to load sample: " + full_path); } - return { temp, MIX_DestroyAudio }; + return { temp, [](MIX_Audio* a) { + if (asw::sound::get_mixer() != nullptr) { + MIX_DestroyAudio(a); + } + } }; } asw::Sample asw::assets::load_sample(const std::string& filename, const std::string& key) @@ -169,13 +191,17 @@ void asw::assets::unload_sample(const std::string& key) asw::Music asw::assets::load_music(const std::string& filename) { const auto full_path = get_path(filename); - MIX_Audio* temp = MIX_LoadAudio(asw::sound::mixer, full_path.c_str(), false); + MIX_Audio* temp = MIX_LoadAudio(asw::sound::get_mixer(), full_path.c_str(), false); if (temp == nullptr) { asw::util::abort_on_error("Failed to load music: " + full_path); } - return { temp, MIX_DestroyAudio }; + return { temp, [](MIX_Audio* a) { + if (asw::sound::get_mixer() != nullptr) { + MIX_DestroyAudio(a); + } + } }; } asw::Music asw::assets::load_music(const std::string& filename, const std::string& key) diff --git a/src/modules/core.cpp b/src/modules/core.cpp index b0bbdc1..d47f8eb 100644 --- a/src/modules/core.cpp +++ b/src/modules/core.cpp @@ -7,6 +7,7 @@ #include #include "./asw/modules/action.h" +#include "./asw/modules/assets.h" #include "./asw/modules/display.h" #include "./asw/modules/input.h" #include "./asw/modules/log.h" @@ -26,17 +27,17 @@ void asw::core::update() while (SDL_PollEvent(&e)) { switch (e.type) { case SDL_EVENT_WINDOW_RESIZED: { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { break; } // Maintain aspect ratio SDL_Point window_size; - SDL_GetRenderOutputSize(asw::display::renderer, &window_size.x, &window_size.y); + SDL_GetRenderOutputSize(r, &window_size.x, &window_size.y); SDL_Point render_size; - SDL_GetRenderLogicalPresentation( - asw::display::renderer, &render_size.x, &render_size.y, nullptr); + SDL_GetRenderLogicalPresentation(r, &render_size.x, &render_size.y, nullptr); const auto x_scale = static_cast(window_size.x) / static_cast(render_size.x); @@ -46,7 +47,7 @@ void asw::core::update() const auto scale = std::min(x_scale, y_scale); - SDL_SetWindowSize(asw::display::window, + SDL_SetWindowSize(asw::display::get_window(), static_cast(static_cast(render_size.x) * scale), static_cast(static_cast(render_size.y) * scale)); break; @@ -78,8 +79,9 @@ void asw::core::update() case SDL_EVENT_MOUSE_MOTION: { // Ensure scale is applied to mouse coordinates - if (asw::display::renderer != nullptr) { - SDL_ConvertEventToRenderCoordinates(asw::display::renderer, &e); + auto* r = asw::display::get_renderer(); + if (r != nullptr) { + SDL_ConvertEventToRenderCoordinates(r, &e); } asw::input::_mouse_motion(e.motion.x, e.motion.y, e.motion.xrel, e.motion.yrel); @@ -117,7 +119,7 @@ void asw::core::update() } case SDL_EVENT_TEXT_INPUT: { - asw::input::text_input += e.text.text; + asw::input::_append_text(e.text.text); break; } @@ -145,20 +147,7 @@ void asw::core::init(int width, int height, int scale) asw::util::abort_on_error("Sound initialization failed"); } - asw::display::window - = SDL_CreateWindow("", width * scale, height * scale, SDL_WINDOW_RESIZABLE); - if (asw::display::window == nullptr) { - asw::util::abort_on_error("WINDOW"); - } - - // Hints - SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1"); - - // Get window surface - asw::display::renderer = SDL_CreateRenderer(asw::display::window, nullptr); - - SDL_SetRenderLogicalPresentation( - asw::display::renderer, width, height, SDL_LOGICAL_PRESENTATION_LETTERBOX); + asw::display::_init(width, height, scale); } void asw::core::print_info() @@ -167,8 +156,8 @@ void asw::core::print_info() asw::log::info("========"); const char* renderer_name = "none"; - if (asw::display::renderer != nullptr) { - renderer_name = SDL_GetRendererName(asw::display::renderer); + if (auto* r = asw::display::get_renderer(); r != nullptr) { + renderer_name = SDL_GetRendererName(r); } asw::log::info( @@ -181,20 +170,24 @@ void asw::core::exit() exiting = true; } -void asw::core::cleanup() +void asw::core::shutdown() { asw::input::clear_actions(); - if (asw::display::renderer != nullptr) { - SDL_DestroyRenderer(asw::display::renderer); - asw::display::renderer = nullptr; - } - if (asw::display::window != nullptr) { - SDL_DestroyWindow(asw::display::window); - asw::display::window = nullptr; - } + // Clear asset caches while SDL resources are still valid — SDL_Destroy* + // calls in the shared_ptr deleters are safe at this point. + asw::assets::clear_all(); + + // _shutdown() nulls the renderer/window pointers BEFORE calling + // SDL_DestroyRenderer/SDL_DestroyWindow. Any outstanding shared_ptr + // held by user code will see a null renderer in their deleter and skip the + // SDL_DestroyTexture call, preventing use-after-free. + asw::display::_shutdown(); + + // Same pattern for sound: null mixer pointer before teardown so any + // surviving Sample/Music shared_ptrs become no-ops. + asw::sound::_shutdown(); - MIX_Quit(); TTF_Quit(); SDL_Quit(); } @@ -202,4 +195,4 @@ void asw::core::cleanup() bool asw::core::is_exiting() { return exiting; -} \ No newline at end of file +} diff --git a/src/modules/display.cpp b/src/modules/display.cpp index 041e988..863b8fd 100644 --- a/src/modules/display.cpp +++ b/src/modules/display.cpp @@ -5,13 +5,56 @@ #include #include "./asw/modules/types.h" +#include "./asw/modules/util.h" -asw::Renderer* asw::display::renderer = nullptr; -asw::Window* asw::display::window = nullptr; +namespace { +asw::Renderer* renderer = nullptr; +asw::Window* window = nullptr; +} // namespace + +void asw::display::_init(int width, int height, int scale) +{ + window = SDL_CreateWindow("", width * scale, height * scale, SDL_WINDOW_RESIZABLE); + if (window == nullptr) { + asw::util::abort_on_error("WINDOW"); + } + + SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1"); + + renderer = SDL_CreateRenderer(window, nullptr); + + SDL_SetRenderLogicalPresentation( + renderer, width, height, SDL_LOGICAL_PRESENTATION_LETTERBOX); +} + +void asw::display::_shutdown() +{ + auto* r = renderer; + renderer = nullptr; + if (r != nullptr) { + SDL_DestroyRenderer(r); + } + + auto* w = window; + window = nullptr; + if (w != nullptr) { + SDL_DestroyWindow(w); + } +} + +asw::Renderer* asw::display::get_renderer() +{ + return renderer; +} + +asw::Window* asw::display::get_window() +{ + return window; +} void asw::display::set_title(const std::string& title) { - SDL_SetWindowTitle(asw::display::window, title.c_str()); + SDL_SetWindowTitle(window, title.c_str()); } void asw::display::set_icon(const std::string& path) @@ -22,28 +65,29 @@ void asw::display::set_icon(const std::string& path) return; } - SDL_SetWindowIcon(asw::display::window, icon); + SDL_SetWindowIcon(window, icon); + SDL_DestroySurface(icon); } void asw::display::set_fullscreen(bool fullscreen) { - SDL_SetWindowFullscreen(asw::display::window, fullscreen); + SDL_SetWindowFullscreen(window, fullscreen); } void asw::display::set_resolution(int w, int h) { - SDL_SetWindowSize(asw::display::window, w, h); + SDL_SetWindowSize(window, w, h); } void asw::display::set_resizable(bool resizable) { - SDL_SetWindowResizable(asw::display::window, resizable); + SDL_SetWindowResizable(window, resizable); } asw::Vec2 asw::display::get_size() { asw::Vec2 size; - SDL_GetWindowSize(asw::display::window, &size.x, &size.y); + SDL_GetWindowSize(window, &size.x, &size.y); return size; } @@ -51,11 +95,11 @@ asw::Vec2 asw::display::get_logical_size() { asw::Vec2 size; - if (asw::display::renderer == nullptr) { + if (renderer == nullptr) { return size; } - SDL_GetRenderLogicalPresentation(asw::display::renderer, &size.x, &size.y, nullptr); + SDL_GetRenderLogicalPresentation(renderer, &size.x, &size.y, nullptr); return size; } @@ -63,57 +107,57 @@ asw::Vec2 asw::display::get_scale() { asw::Vec2 scale; - if (asw::display::renderer == nullptr) { + if (renderer == nullptr) { return scale; } - SDL_GetRenderScale(asw::display::renderer, &scale.x, &scale.y); + SDL_GetRenderScale(renderer, &scale.x, &scale.y); return scale; } void asw::display::set_render_target(const asw::Texture& texture) { - if (asw::display::renderer == nullptr) { + if (renderer == nullptr) { return; } - SDL_SetRenderTarget(asw::display::renderer, texture.get()); + SDL_SetRenderTarget(renderer, texture.get()); } void asw::display::reset_render_target() { - if (asw::display::renderer == nullptr) { + if (renderer == nullptr) { return; } - SDL_SetRenderTarget(asw::display::renderer, nullptr); + SDL_SetRenderTarget(renderer, nullptr); } void asw::display::clear() { - if (asw::display::renderer == nullptr) { + if (renderer == nullptr) { return; } - SDL_RenderClear(asw::display::renderer); + SDL_RenderClear(renderer); } void asw::display::clear(const asw::Color& color) { - SDL_SetRenderDrawColor(asw::display::renderer, color.r, color.g, color.b, color.a); - SDL_RenderClear(asw::display::renderer); + SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); + SDL_RenderClear(renderer); } void asw::display::present() { - if (asw::display::renderer == nullptr) { + if (renderer == nullptr) { return; } - SDL_RenderPresent(asw::display::renderer); + SDL_RenderPresent(renderer); } void asw::display::set_blend_mode(asw::BlendMode mode) { - SDL_SetRenderDrawBlendMode(asw::display::renderer, static_cast(mode)); + SDL_SetRenderDrawBlendMode(renderer, static_cast(mode)); } diff --git a/src/modules/draw.cpp b/src/modules/draw.cpp index 6b20fc6..dd68354 100644 --- a/src/modules/draw.cpp +++ b/src/modules/draw.cpp @@ -11,17 +11,19 @@ void asw::draw::clear_color(asw::Color color) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } - SDL_SetRenderDrawColor(asw::display::renderer, color.r, color.g, color.b, color.a); - SDL_RenderClear(asw::display::renderer); + SDL_SetRenderDrawColor(r, color.r, color.g, color.b, color.a); + SDL_RenderClear(r); } void asw::draw::sprite(const asw::Texture& tex, const asw::Vec2& position) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } @@ -33,13 +35,14 @@ void asw::draw::sprite(const asw::Texture& tex, const asw::Vec2& position dest.w = size.x; dest.h = size.y; - SDL_RenderTexture(asw::display::renderer, tex.get(), nullptr, &dest); + SDL_RenderTexture(r, tex.get(), nullptr, &dest); } void asw::draw::sprite_flip( const asw::Texture& tex, const asw::Vec2& position, bool flip_x, bool flip_y) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } @@ -61,12 +64,13 @@ void asw::draw::sprite_flip( flip = static_cast(flip | SDL_FLIP_VERTICAL); } - SDL_RenderTextureRotated(asw::display::renderer, tex.get(), nullptr, &dest, 0, nullptr, flip); + SDL_RenderTextureRotated(r, tex.get(), nullptr, &dest, 0, nullptr, flip); } void asw::draw::stretch_sprite(const asw::Texture& tex, const asw::Quad& position) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } @@ -76,13 +80,14 @@ void asw::draw::stretch_sprite(const asw::Texture& tex, const asw::Quad& dest.w = position.size.x; dest.h = position.size.y; - SDL_RenderTexture(asw::display::renderer, tex.get(), nullptr, &dest); + SDL_RenderTexture(r, tex.get(), nullptr, &dest); } void asw::draw::rotate_sprite( const asw::Texture& tex, const asw::Vec2& position, float angle) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } @@ -97,14 +102,14 @@ void asw::draw::rotate_sprite( // Rad to deg const double angleDeg = angle * (180.0 / std::numbers::pi); - SDL_RenderTextureRotated( - asw::display::renderer, tex.get(), nullptr, &dest, angleDeg, nullptr, SDL_FLIP_NONE); + SDL_RenderTextureRotated(r, tex.get(), nullptr, &dest, angleDeg, nullptr, SDL_FLIP_NONE); } void asw::draw::stretch_sprite_blit( const asw::Texture& tex, const asw::Quad& source, const asw::Quad& dest) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } @@ -120,13 +125,14 @@ void asw::draw::stretch_sprite_blit( r_dest.w = dest.size.x; r_dest.h = dest.size.y; - SDL_RenderTexture(asw::display::renderer, tex.get(), &r_src, &r_dest); + SDL_RenderTexture(r, tex.get(), &r_src, &r_dest); } void asw::draw::stretch_sprite_rotate_blit(const asw::Texture& tex, const asw::Quad& source, const asw::Quad& dest, float angle) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } @@ -144,20 +150,20 @@ void asw::draw::stretch_sprite_rotate_blit(const asw::Texture& tex, const asw::Q const double angleDeg = angle * (180.0 / std::numbers::pi); - SDL_RenderTextureRotated( - asw::display::renderer, tex.get(), &r_src, &r_dest, angleDeg, nullptr, SDL_FLIP_NONE); + SDL_RenderTextureRotated(r, tex.get(), &r_src, &r_dest, angleDeg, nullptr, SDL_FLIP_NONE); } void asw::draw::text(const asw::Font& font, const std::string& text, const asw::Vec2& position, asw::Color color, asw::TextJustify justify) { - if (text.empty() || asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (text.empty() || r == nullptr) { return; } const auto sdlColor = SDL_Color { color.r, color.g, color.b, color.a }; SDL_Surface* textSurface = TTF_RenderText_Blended(font.get(), text.c_str(), 0, sdlColor); - SDL_Texture* textTexture = SDL_CreateTextureFromSurface(asw::display::renderer, textSurface); + SDL_Texture* textTexture = SDL_CreateTextureFromSurface(r, textSurface); SDL_SetTextureBlendMode(textTexture, SDL_BLENDMODE_BLEND); SDL_SetTextureScaleMode(textTexture, SDL_SCALEMODE_LINEAR); @@ -175,71 +181,76 @@ void asw::draw::text(const asw::Font& font, const std::string& text, dest.x -= dest.w; } - SDL_RenderTexture(asw::display::renderer, textTexture, nullptr, &dest); + SDL_RenderTexture(r, textTexture, nullptr, &dest); SDL_DestroySurface(textSurface); SDL_DestroyTexture(textTexture); } void asw::draw::point(const asw::Vec2& position, asw::Color color) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } - SDL_SetRenderDrawColor(asw::display::renderer, color.r, color.g, color.b, color.a); - SDL_RenderPoint(asw::display::renderer, position.x, position.y); + SDL_SetRenderDrawColor(r, color.r, color.g, color.b, color.a); + SDL_RenderPoint(r, position.x, position.y); } void asw::draw::line( const asw::Vec2& position1, const asw::Vec2& position2, asw::Color color) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } - SDL_SetRenderDrawColor(asw::display::renderer, color.r, color.g, color.b, color.a); - SDL_RenderLine(asw::display::renderer, position1.x, position1.y, position2.x, position2.y); + SDL_SetRenderDrawColor(r, color.r, color.g, color.b, color.a); + SDL_RenderLine(r, position1.x, position1.y, position2.x, position2.y); } void asw::draw::rect(const asw::Quad& position, asw::Color color) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } - SDL_SetRenderDrawColor(asw::display::renderer, color.r, color.g, color.b, color.a); + SDL_SetRenderDrawColor(r, color.r, color.g, color.b, color.a); SDL_FRect rect; rect.x = position.position.x; rect.y = position.position.y; rect.w = position.size.x; rect.h = position.size.y; - SDL_RenderRect(asw::display::renderer, &rect); + SDL_RenderRect(r, &rect); } void asw::draw::rect_fill(const asw::Quad& position, asw::Color color) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } - SDL_SetRenderDrawColor(asw::display::renderer, color.r, color.g, color.b, color.a); + SDL_SetRenderDrawColor(r, color.r, color.g, color.b, color.a); SDL_FRect rect; rect.x = position.position.x; rect.y = position.position.y; rect.w = position.size.x; rect.h = position.size.y; - SDL_RenderFillRect(asw::display::renderer, &rect); + SDL_RenderFillRect(r, &rect); } void asw::draw::circle(const asw::Vec2& position, float radius, asw::Color color) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } - SDL_SetRenderDrawColor(asw::display::renderer, color.r, color.g, color.b, color.a); + SDL_SetRenderDrawColor(r, color.r, color.g, color.b, color.a); // Midpoint circle algorithm — no trig, integer arithmetic only auto x = radius; @@ -249,14 +260,14 @@ void asw::draw::circle(const asw::Vec2& position, float radius, asw::Colo const float cy = position.y; while (x >= y) { - SDL_RenderPoint(asw::display::renderer, cx + x, cy + y); - SDL_RenderPoint(asw::display::renderer, cx - x, cy + y); - SDL_RenderPoint(asw::display::renderer, cx + x, cy - y); - SDL_RenderPoint(asw::display::renderer, cx - x, cy - y); - SDL_RenderPoint(asw::display::renderer, cx + y, cy + x); - SDL_RenderPoint(asw::display::renderer, cx - y, cy + x); - SDL_RenderPoint(asw::display::renderer, cx + y, cy - x); - SDL_RenderPoint(asw::display::renderer, cx - y, cy - x); + SDL_RenderPoint(r, cx + x, cy + y); + SDL_RenderPoint(r, cx - x, cy + y); + SDL_RenderPoint(r, cx + x, cy - y); + SDL_RenderPoint(r, cx - x, cy - y); + SDL_RenderPoint(r, cx + y, cy + x); + SDL_RenderPoint(r, cx - y, cy + x); + SDL_RenderPoint(r, cx + y, cy - x); + SDL_RenderPoint(r, cx - y, cy - x); y++; if (err < 0) { err += (2.0F * y) + 1.0F; @@ -269,11 +280,12 @@ void asw::draw::circle(const asw::Vec2& position, float radius, asw::Colo void asw::draw::circle_fill(const asw::Vec2& position, float radius, asw::Color color) { - if (asw::display::renderer == nullptr) { + auto* r = asw::display::get_renderer(); + if (r == nullptr) { return; } - SDL_SetRenderDrawColor(asw::display::renderer, color.r, color.g, color.b, color.a); + SDL_SetRenderDrawColor(r, color.r, color.g, color.b, color.a); // Midpoint circle with horizontal scanlines — no gaps, no trig float x = radius; @@ -283,10 +295,10 @@ void asw::draw::circle_fill(const asw::Vec2& position, float radius, asw: const float cy = position.y; while (x >= y) { - SDL_RenderLine(asw::display::renderer, cx - x, cy + y, cx + x, cy + y); - SDL_RenderLine(asw::display::renderer, cx - x, cy - y, cx + x, cy - y); - SDL_RenderLine(asw::display::renderer, cx - y, cy + x, cx + y, cy + x); - SDL_RenderLine(asw::display::renderer, cx - y, cy - x, cx + y, cy - x); + SDL_RenderLine(r, cx - x, cy + y, cx + x, cy + y); + SDL_RenderLine(r, cx - x, cy - y, cx + x, cy - y); + SDL_RenderLine(r, cx - y, cy + x, cx + y, cy + x); + SDL_RenderLine(r, cx - y, cy - x, cx + y, cy - x); y++; if (err < 0) { err += (2.0F * y) + 1.0F; diff --git a/src/modules/input.cpp b/src/modules/input.cpp index 1c3b865..618998e 100644 --- a/src/modules/input.cpp +++ b/src/modules/input.cpp @@ -32,21 +32,39 @@ std::vector controller {}; /// @brief Map of SDL_JoystickID to controller index in the controller vector. std::unordered_map controller_id_map {}; + +asw::input::KeyState keyboard {}; +asw::input::MouseState mouse {}; +std::string text_input; } // namespace -asw::input::KeyState asw::input::keyboard {}; +const asw::input::KeyState& asw::input::get_keyboard() +{ + return keyboard; +} + +const asw::input::MouseState& asw::input::get_mouse() +{ + return mouse; +} -asw::input::MouseState asw::input::mouse {}; +const std::string& asw::input::get_text_input() +{ + return text_input; +} -std::string asw::input::text_input; +void asw::input::_append_text(const char* text) +{ + text_input += text; +} void asw::input::reset() { // Snapshot action states before raw input arrays are cleared. asw::input::update_actions(); - auto& k_state = asw::input::keyboard; - auto& m_state = asw::input::mouse; + auto& k_state = keyboard; + auto& m_state = mouse; // Clear key state k_state.any_pressed = false; @@ -74,7 +92,7 @@ void asw::input::reset() } // Clear text input - asw::input::text_input.clear(); + text_input.clear(); // Clear controller state for (auto& cont : controller) { diff --git a/src/modules/sound.cpp b/src/modules/sound.cpp index 3e4e628..3e88f7e 100644 --- a/src/modules/sound.cpp +++ b/src/modules/sound.cpp @@ -14,6 +14,7 @@ float music_volume = 1.0F; std::array tracks; MIX_Track* music_track = nullptr; +MIX_Mixer* mixer = nullptr; float compute_sfx_volume(float vol) { @@ -39,7 +40,20 @@ int find_free_track() } // namespace -MIX_Mixer* asw::sound::mixer = nullptr; +MIX_Mixer* asw::sound::get_mixer() +{ + return mixer; +} + +void asw::sound::_shutdown() +{ + auto* m = mixer; + mixer = nullptr; + if (m != nullptr) { + MIX_DestroyMixer(m); + } + MIX_Quit(); +} bool asw::sound::_init() { diff --git a/src/modules/ui/input_box.cpp b/src/modules/ui/input_box.cpp index 057dd29..5d5d296 100644 --- a/src/modules/ui/input_box.cpp +++ b/src/modules/ui/input_box.cpp @@ -13,10 +13,10 @@ void asw::ui::InputBox::on_focus_changed(Context& ctx, bool focused) (void)ctx; if (focused) { - SDL_StartTextInput(asw::display::window); + SDL_StartTextInput(asw::display::get_window()); _cursor_pos = value.size(); } else { - SDL_StopTextInput(asw::display::window); + SDL_StopTextInput(asw::display::get_window()); } } @@ -135,7 +135,7 @@ void asw::ui::InputBox::draw(Context& ctx) static_cast(transform.size.x - (text_padding * 2)), static_cast(transform.size.y), }; - SDL_SetRenderClipRect(asw::display::renderer, &clip); + SDL_SetRenderClipRect(asw::display::get_renderer(), &clip); // Text position (vertically centered) const auto display_text = value.empty() ? placeholder : value; @@ -168,7 +168,7 @@ void asw::ui::InputBox::draw(Context& ctx) } // Reset clip - SDL_SetRenderClipRect(asw::display::renderer, nullptr); + SDL_SetRenderClipRect(asw::display::get_renderer(), nullptr); // Focus ring if (_focused && ctx.theme.show_focus) { diff --git a/src/modules/ui/root.cpp b/src/modules/ui/root.cpp index 6cae966..d6e872e 100644 --- a/src/modules/ui/root.cpp +++ b/src/modules/ui/root.cpp @@ -95,6 +95,7 @@ void asw::ui::Root::update() root.layout(ctx); // --- Mouse --- + const auto& mouse = get_mouse(); // Hover and Unhover events if (mouse.change.x != 0.0F || mouse.change.y != 0.0F) { @@ -142,8 +143,8 @@ void asw::ui::Root::update() } // --- Text Input --- - if (!input::text_input.empty()) { - const UIEvent ti { .type = UIEvent::Type::TextInput, .text = input::text_input }; + if (!input::get_text_input().empty()) { + const UIEvent ti { .type = UIEvent::Type::TextInput, .text = input::get_text_input() }; dispatch_to_focused(ti); }