diff --git a/docs/pages/events_and_input.md b/docs/pages/events_and_input.md index 0050ebbe..90f67160 100644 --- a/docs/pages/events_and_input.md +++ b/docs/pages/events_and_input.md @@ -43,7 +43,7 @@ every handler registered for that event type before returning. The event dispatch hub is `Sleak::EventDispatcher` (`include/public/Events/Event.hpp`), an all-static class backed by an -`unordered_map>>`: +`unordered_map>>`: ```cpp template @@ -52,10 +52,16 @@ static std::string RegisterEventHandler(T* instance, void (T::*memberFunction)(c template static std::string RegisterEventCallback(std::function callback); -static void UnregisterEvent(EventType type, std::string id); +static void UnregisterEvent(EventType type, const std::string& id); static void UnregisterEvents(EventType type); static void UnregisterAllEvents(); +template +static EventSubscription Subscribe(T* instance, void (T::*memberFunction)(const EventT&)); + +template +static EventSubscription Subscribe(std::function callback); + template static void DispatchEvent(const EventT& event); ``` @@ -136,16 +142,23 @@ raised from `Window::Update()` is fully handled before the frame's scene update begins. There is no event queue, no deferred delivery, and no ordering guarantee between handlers beyond registration order. -`DispatchEvent` copies the handler vector before iterating it, so a handler -may register or unregister handlers while it runs without invalidating the -loop. Handlers added during dispatch do not receive the event currently -being delivered. - -`RegisterEventHandler` returns a `std::string` id. Keep it and pass it to -`UnregisterEvent(type, id)` when the registering object dies. A handler -bound to a destroyed `this` is a dangling call, and the dispatcher has no -way to detect it. Unregister in the same place you destroy the object, -typically a scene's destructor or `OnDeactivate`. +A handler may register or unregister handlers while it runs. A handler +unregistered during dispatch is skipped for the rest of that dispatch, and +the list is compacted once the outermost dispatch returns. Handlers added +during dispatch do not receive the event currently being delivered. + +A handler can set `e.Handled = true` (the flag is `mutable`, so this works +through the `const&`) to stop the event from reaching handlers registered +after it. + +`RegisterEventHandler` returns a `std::string` id, the decimal form of a +counter. Keep it and pass it to `UnregisterEvent(type, id)` when the +registering object dies. A handler bound to a destroyed `this` is a +dangling call, and the dispatcher has no way to detect it. Unregister in +the same place you destroy the object, typically a scene's destructor or +`OnDeactivate`. Or use `Subscribe`, which returns a move-only +`EventSubscription` that unregisters itself when it is destroyed or +`Reset()`. --- diff --git a/include/public/Core/Application.hpp b/include/public/Core/Application.hpp index 896cafaa..def68e94 100644 --- a/include/public/Core/Application.hpp +++ b/include/public/Core/Application.hpp @@ -215,6 +215,10 @@ class ENGINE_API Application { Timer FrameTimer; + EventSubscription m_keyPressedSub; + EventSubscription m_windowResizeSub; + EventSubscription m_windowFullScreenSub; + // Deferred resize — set in the event handler, applied at start of next frame bool m_pendingResize = false; uint32_t m_pendingResizeW = 0; diff --git a/include/public/Events/Delegate.hpp b/include/public/Events/Delegate.hpp index 932a27af..4581f4c4 100644 --- a/include/public/Events/Delegate.hpp +++ b/include/public/Events/Delegate.hpp @@ -1,15 +1,16 @@ #ifndef _DELEGATE_H_ #define _DELEGATE_H_ +#include +#include +#include +#include #include -#include #include +#include #include #include - -#include -#include -#include +#include namespace Sleak { /// Type-erased callable interface storable in a homogeneous handler list. @@ -49,13 +50,41 @@ namespace Sleak { std::tuple storedArgs; // Store by reference using forward_as_tuple }; + /// Process-wide source of delegate handles. Exported so the engine + /// and every module built on it draw from one counter. + /// @ingroup events + struct ENGINE_API DelegateHandleCounter { + static inline std::atomic next{0}; + }; + // Specialized delegate for Event types /// Common base for EventDelegate instantiations, so they can share a handler list. /// @ingroup events class EventDelegateBase : public IDelegate { public: - virtual ~EventDelegateBase() = default; - virtual std::string GetID() = 0; + EventDelegateBase() { GenerateID(); } + virtual ~EventDelegateBase() = default; + + /// Assigns a fresh process-unique ID, used to identify this delegate + /// for unregistration. + std::string GenerateID() { + handle = DelegateHandleCounter::next.fetch_add( + 1, std::memory_order_relaxed) + + 1; + id = std::to_string(handle); + return id; + } + + std::string GetID() override { return id; } + /// Numeric form of GetID(). + uint64_t GetHandle() const { return handle; } + + private: + friend class EventDispatcher; + + uint64_t handle = 0; + std::string id; + bool removed = false; }; /// Delegate bound to a single EventT callback; holds a non-owning pointer @@ -66,9 +95,7 @@ namespace Sleak { public: using FunctionType = std::function; - EventDelegate(FunctionType func) : function(func) { - GenerateID(); - } + EventDelegate(FunctionType func) : function(std::move(func)) {} void SetEvent(const EventT& event) { eventPtr = &event; @@ -80,29 +107,15 @@ namespace Sleak { } } - /// Assigns a fresh random 32-character ID, used to identify this delegate for unregistration. - std::string GenerateID() { - uuid = ""; - std::string str = - "0123456789!^#%&=*?+-_/" - "[]{}()" - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - std::random_device dev; - std::mt19937 rand(dev()); - std::uniform_int_distribution<> dist(0,str.size() - 1); - - for(int i = 0; i < 32; i++) - uuid += str[dist(rand)]; - - return uuid; + /// Calls the handler with event directly. + void Invoke(const EventT& event) { + if (function) function(event); } - std::string GetID() override { return uuid; } - private: FunctionType function; - const EventT* eventPtr = nullptr; // Store a pointer to avoid copying - std::string uuid; + const EventT* eventPtr = + nullptr; // Store a pointer to avoid copying }; /// Fan-out list of Delegate instances, all invoked together via Broadcast. diff --git a/include/public/Events/Event.hpp b/include/public/Events/Event.hpp index 68daaf55..c993c524 100644 --- a/include/public/Events/Event.hpp +++ b/include/public/Events/Event.hpp @@ -2,18 +2,23 @@ #define _EVENT_H_ #include -#include +#include +#include +#include #include +#include #include #include -#include - #define BIND_LAMBDA(fn) [this](auto&&... args) -> decltype(auto) { return this->fn(std::forward(args)...); } #define BIND_FUNC_0(Function, class_name) std::bind(&class_name::Function, this) #define BIND_FUNC_1(Function, class_name) std::bind(&class_name::Function, this, std::placeholders::_1) -#define BIND_FUNC_2(Function, class_name) std::bind(&class_name::Function, this, std::placeholders::_2) -#define BIND_FUNC_3(Function, class_name) std::bind(&class_name::Function, this, std::placeholders::_3) +#define BIND_FUNC_2(Function, class_name) \ + std::bind(&class_name::Function, this, std::placeholders::_1, \ + std::placeholders::_2) +#define BIND_FUNC_3(Function, class_name) \ + std::bind(&class_name::Function, this, std::placeholders::_1, \ + std::placeholders::_2, std::placeholders::_3) #define GET_DISPATCHER Sleak::EventDispatcher::GetInstance() @@ -58,7 +63,7 @@ namespace Sleak { { return static_cast(category); } /// Base for all engine events; carries type/category identity and the - /// Handled flag consumers can set to stop further propagation. + /// Handled flag a handler can set to stop further propagation. /// @ingroup events class ENGINE_API Event { public: @@ -66,7 +71,8 @@ namespace Sleak { virtual ~Event() = default; - bool Handled = false; + /// Set from a handler to skip the handlers registered after it. + mutable bool Handled = false; virtual EventType GetEventType() const = 0; virtual const char* GetName() const = 0; @@ -79,6 +85,8 @@ namespace Sleak { } }; + class EventSubscription; + /// Static registry mapping EventType to subscribed handlers; dispatches /// events synchronously to every handler registered for its type. /// @@ -92,11 +100,16 @@ namespace Sleak { /// Both return a string id. Keep it and pass it to UnregisterEvent() /// when the subscriber goes away, typically in a scene's OnDeactivate() /// or destructor. Handlers outlive the objects they were bound to - /// otherwise, and the next dispatch calls into freed memory. + /// otherwise, and the next dispatch calls into freed memory. Subscribe() + /// does the same but returns an EventSubscription that unregisters + /// itself when destroyed. /// /// Dispatch is synchronous and runs in registration order on the - /// calling thread. The handler list is copied before iteration, so a - /// handler may register or unregister during dispatch safely. + /// calling thread, stopping early once a handler sets Event::Handled. + /// A handler may register or unregister during dispatch: a handler + /// removed mid-dispatch is not called again, and one added mid-dispatch + /// first runs on the next event. Not thread-safe; use from the main + /// thread only. /// /// @code{.cpp} /// class WorldScene : public Sleak::Scene { @@ -126,90 +139,202 @@ namespace Sleak { /// }; /// @endcode /// - /// @see Event, EventType, Events::Input::KeyPressedEvent, - /// Events::Input::MouseMovedEvent + /// @see Event, EventType, EventSubscription, + /// Events::Input::KeyPressedEvent, Events::Input::MouseMovedEvent /// @ingroup events class ENGINE_API EventDispatcher { - public: - // Register a handler for any event type - /// Registers a free-function/lambda callback for EventT, returning an ID for later unregistration. - template - static std::string RegisterEventCallback(std::function callback) { - EventType type = EventT::GetStaticType(); - - auto delegate = std::make_shared>(callback); - eventHandlers[type].push_back(delegate); - - return delegate->GetUUID(); - } - - // Register a member function handler + public: + /// Registers a free-function/lambda callback for EventT, returning an + /// ID for later unregistration. + template + static std::string RegisterEventCallback( + std::function callback) { + auto delegate = + std::make_shared>(std::move(callback)); + eventHandlers[EventT::GetStaticType()].push_back(delegate); + return delegate->GetID(); + } + /// Registers a member-function handler bound to instance, returning an ID for later unregistration. - template - static std::string RegisterEventHandler(T* instance, void (T::*memberFunction)(const EventT&)) { - EventType type = EventT::GetStaticType(); - + template + static std::string RegisterEventHandler( + T* instance, void (T::*memberFunction)(const EventT&)) { auto callback = [instance, memberFunction](const EventT& event) { (instance->*memberFunction)(event); }; - - auto delegate = std::make_shared>(callback); - eventHandlers[type].push_back(delegate); - - return delegate->GetID(); + return RegisterEventCallback(std::move(callback)); } + /// Like RegisterEventCallback(), but unregisters when the returned + /// token is destroyed. + template + [[nodiscard]] static EventSubscription Subscribe( + std::function callback); + + /// Like RegisterEventHandler(), but unregisters when the returned + /// token is destroyed. + template + [[nodiscard]] static EventSubscription Subscribe( + T* instance, void (T::*memberFunction)(const EventT&)); + /// Removes the single handler with matching id from type's handler list. - static void UnregisterEvent(EventType type, std::string id) { - for(auto it = eventHandlers[type].begin(); it != eventHandlers[type].end(); ++it) { - if((*it)->GetID() == id) { - eventHandlers[type].erase(it); - break; + static void UnregisterEvent(EventType type, const std::string& id) { + uint64_t handle = 0; + auto [end, ec] = + std::from_chars(id.data(), id.data() + id.size(), handle); + if (ec != std::errc() || end != id.data() + id.size()) return; + + auto found = eventHandlers.find(type); + if (found == eventHandlers.end()) return; + + auto& handlers = found->second; + for (size_t i = 0; i < handlers.size(); ++i) { + if (handlers[i]->GetHandle() != handle || + handlers[i]->removed) + continue; + if (dispatchDepth > 0) { + handlers[i]->removed = true; + needsCompaction = true; + } else { + handlers.erase(handlers.begin() + i); } + return; } } /// Drops every handler registered for type. static void UnregisterEvents(EventType type) { - eventHandlers[type].clear(); + auto found = eventHandlers.find(type); + if (found == eventHandlers.end()) return; + if (dispatchDepth > 0) { + MarkRemoved(found->second); + } else { + found->second.clear(); + } } /// Drops every handler for every event type. static void UnregisterAllEvents() { - eventHandlers.clear(); + if (dispatchDepth > 0) { + for (auto& [type, handlers] : eventHandlers) + MarkRemoved(handlers); + } else { + eventHandlers.clear(); + } } - // Dispatch an event to all registered handlers /// Invokes every handler registered for event's type, in registration order. template static void DispatchEvent(const EventT& event) { - EventType type = event.GetEventType(); + auto found = eventHandlers.find(event.GetEventType()); + if (found == eventHandlers.end()) return; - if (eventHandlers.find(type) == eventHandlers.end()) - return; - - // Copy the handler list so that handlers which add/remove entries - // during dispatch don't invalidate the iteration. - auto handlers = eventHandlers[type]; - for (auto& handler : handlers) { - // Try to cast to the right event delegate type - auto typedDelegate = std::dynamic_pointer_cast>(handler); - if (typedDelegate) { - typedDelegate->SetEvent(event); - typedDelegate->Execute(); - } + auto& handlers = found->second; + const size_t count = handlers.size(); + DispatchScope scope; + for (size_t i = 0; i < count && !event.Handled; ++i) { + EventDelegateBase* handler = handlers[i].get(); + if (handler->removed) continue; + if (auto* typed = + dynamic_cast*>(handler)) + typed->Invoke(event); } } - + /// Drops every handler for every event type; equivalent to UnregisterAllEvents(). - static void ClearEventHandlers() { - eventHandlers.clear(); + static void ClearEventHandlers() { UnregisterAllEvents(); } + + private: + using HandlerList = std::vector>; + + // Removals during dispatch are deferred so indices and delegates + // stay valid. + struct DispatchScope { + DispatchScope() { ++dispatchDepth; } + ~DispatchScope() { + if (--dispatchDepth == 0 && needsCompaction) Compact(); + } + }; + + static void MarkRemoved(HandlerList& handlers) { + for (auto& handler : handlers) handler->removed = true; + needsCompaction = true; + } + + static void Compact() { + needsCompaction = false; + for (auto& [type, handlers] : eventHandlers) { + std::erase_if(handlers, + [](const auto& h) { return h->removed; }); + } } - private: - static inline std::unordered_map>> eventHandlers; - }; + static inline std::unordered_map + eventHandlers; + static inline int dispatchDepth = 0; + static inline bool needsCompaction = false; + }; + + /// Move-only owner of one handler registration; unregisters it on + /// destruction or Reset(). + /// @ingroup events + class EventSubscription { + public: + EventSubscription() = default; + EventSubscription(EventType type, std::string id) + : m_type(type), m_id(std::move(id)) {} + + EventSubscription(EventSubscription&& other) noexcept + : m_type(other.m_type), m_id(std::move(other.m_id)) { + other.m_id.clear(); + } + EventSubscription& operator=(EventSubscription&& other) noexcept { + if (this != &other) { + Reset(); + m_type = other.m_type; + m_id = std::move(other.m_id); + other.m_id.clear(); + } + return *this; + } + + EventSubscription(const EventSubscription&) = delete; + EventSubscription& operator=(const EventSubscription&) = delete; + + ~EventSubscription() { Reset(); } + + /// Unregisters the handler now; safe to call more than once. + void Reset() { + if (m_id.empty()) return; + EventDispatcher::UnregisterEvent(m_type, m_id); + m_id.clear(); + } + + bool IsActive() const { return !m_id.empty(); } + EventType GetType() const { return m_type; } + const std::string& GetID() const { return m_id; } + + private: + EventType m_type = EventType::Unknown; + std::string m_id; + }; + + template + EventSubscription EventDispatcher::Subscribe( + std::function callback) { + return EventSubscription( + EventT::GetStaticType(), + RegisterEventCallback(std::move(callback))); + } + + template + EventSubscription EventDispatcher::Subscribe( + T* instance, void (T::*memberFunction)(const EventT&)) { + return EventSubscription( + EventT::GetStaticType(), + RegisterEventHandler(instance, memberFunction)); + } // Helper function for easier event dispatching /// Constructs a T from args and dispatches it through EventDispatcher. diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index 83c0eae5..8385072b 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -93,13 +93,19 @@ namespace Sleak { RenderEngine::RendererType::Vulkan, CoreWindow); } - EventDispatcher::RegisterEventHandler(this,&Application::OnKeyPressed); - EventDispatcher::RegisterEventHandler(this, &Application::OnWindowResize); - EventDispatcher::RegisterEventHandler(this, &Application::OnWindowFullScreen); - + m_keyPressedSub = + EventDispatcher::Subscribe(this, &Application::OnKeyPressed); + m_windowResizeSub = + EventDispatcher::Subscribe(this, &Application::OnWindowResize); + m_windowFullScreenSub = + EventDispatcher::Subscribe(this, &Application::OnWindowFullScreen); } Application::~Application() { + m_keyPressedSub.Reset(); + m_windowResizeSub.Reset(); + m_windowFullScreenSub.Reset(); + // Flush the GPU before tearing down any scene-owned resources (textures, // meshes, materials) — the renderer's descriptor sets still reference // samplers/image views owned by the game's scene objects. Without this