From 7113584632878a358d0bfae308776bf17f67a1b3 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 10 Jul 2026 17:42:24 -0600 Subject: [PATCH] SlotMap abstraction & log fix --- src/dusk/mods/svc/config.cpp | 158 ++++++++++++--------------- src/dusk/mods/svc/gfx.cpp | 188 ++++++++++++--------------------- src/dusk/mods/svc/host.cpp | 49 +++++---- src/dusk/mods/svc/overlay.cpp | 79 +++++++------- src/dusk/mods/svc/slot_map.hpp | 188 +++++++++++++++++++++++++++++++++ src/dusk/mods/svc/ui.cpp | 144 +++++++------------------ src/dusk/ui/logs_window.cpp | 20 ++-- 7 files changed, 445 insertions(+), 381 deletions(-) create mode 100644 src/dusk/mods/svc/slot_map.hpp diff --git a/src/dusk/mods/svc/config.cpp b/src/dusk/mods/svc/config.cpp index d919c0ab1f..bf943d61b8 100644 --- a/src/dusk/mods/svc/config.cpp +++ b/src/dusk/mods/svc/config.cpp @@ -1,6 +1,7 @@ #include "config.hpp" #include "registry.hpp" +#include "slot_map.hpp" #include "aurora/lib/logging.hpp" #include "dusk/config.hpp" @@ -15,7 +16,6 @@ #include #include #include -#include #include namespace dusk::mods::svc { @@ -23,25 +23,22 @@ namespace { aurora::Module Log("dusk::mods::config"); -struct ModConfigVarEntry { - uint64_t handle = 0; - ConfigVarType type = CONFIG_VAR_BOOL; - std::unique_ptr var; +enum class ConfigSlotKind : uint8_t { + Var, + Subscription, }; -struct ModConfigSubscription { - uint64_t handle = 0; +struct ConfigSlot { + ConfigSlotKind kind = ConfigSlotKind::Var; + // Var payload + ConfigVarType type = CONFIG_VAR_BOOL; + std::unique_ptr var; + // Subscription payload uint64_t varHandle = 0; config::Subscription coreSubscription = 0; }; -struct ModConfigRecord { - std::vector vars; - std::vector subscriptions; -}; - -std::unordered_map s_modConfig; -uint64_t s_nextHandle = 1; +SlotMap s_slots; bool s_dirty = false; std::chrono::steady_clock::time_point s_lastSave{}; constexpr std::chrono::seconds kSaveDebounce{2}; @@ -130,17 +127,13 @@ bool valid_var_fragment(const char* name) { } config::ConfigVarBase* find_var(LoadedMod& mod, const uint64_t handle, uint32_t expectedType) { - const auto recordIt = s_modConfig.find(&mod); - if (recordIt == s_modConfig.end()) { + const auto* entry = s_slots.find_owned(handle, mod); + if (entry == nullptr || entry->value.kind != ConfigSlotKind::Var || + entry->value.type != expectedType) + { return nullptr; } - const auto& vars = recordIt->second.vars; - const auto entry = - std::ranges::find_if(vars, [&](const auto& e) { return e.handle == handle; }); - if (entry == vars.end() || entry->type != expectedType) { - return nullptr; - } - return entry->var.get(); + return entry->value.var.get(); } template @@ -154,17 +147,17 @@ ConfigVar* find_typed_var(ModContext* context, ConfigVarHandle handle, uint32 } void config_remove_mod(LoadedMod& mod) { - const auto it = s_modConfig.find(&mod); - if (it == s_modConfig.end()) { - return; + const auto entries = s_slots.take_all(mod); + for (const auto& entry : entries) { + if (entry.value.kind == ConfigSlotKind::Subscription) { + config::unsubscribe(entry.value.coreSubscription); + } } - for (const auto& sub : it->second.subscriptions) { - config::unsubscribe(sub.coreSubscription); + for (const auto& entry : entries) { + if (entry.value.kind == ConfigSlotKind::Var) { + config::unregister(*entry.value.var); + } } - for (const auto& entry : it->second.vars) { - config::unregister(*entry.var); - } - s_modConfig.erase(it); } ModResult config_register_var( @@ -190,16 +183,16 @@ ModResult config_register_var( std::unique_ptr var; switch (desc->type) { case CONFIG_VAR_BOOL: - var = std::make_unique >(fullName, desc->default_bool); + var = std::make_unique>(fullName, desc->default_bool); break; case CONFIG_VAR_INT: - var = std::make_unique >(fullName, desc->default_int); + var = std::make_unique>(fullName, desc->default_int); break; case CONFIG_VAR_FLOAT: - var = std::make_unique >(fullName, desc->default_float); + var = std::make_unique>(fullName, desc->default_float); break; case CONFIG_VAR_STRING: - var = std::make_unique >( + var = std::make_unique>( fullName, desc->default_string != nullptr ? desc->default_string : ""); break; default: @@ -211,13 +204,10 @@ ModResult config_register_var( // reads the value right after registration anyway. config::Register(*var); - auto& record = s_modConfig[mod]; - auto& entry = record.vars.emplace_back(); - entry.handle = s_nextHandle++; - entry.type = desc->type; - entry.var = std::move(var); + const auto handle = s_slots.emplace( + *mod, ConfigSlot{.kind = ConfigSlotKind::Var, .type = desc->type, .var = std::move(var)}); if (outHandle != nullptr) { - *outHandle = entry.handle; + *outHandle = handle; } return MOD_OK; } @@ -227,29 +217,26 @@ ModResult config_unregister_var(ModContext* context, ConfigVarHandle var) { if (mod == nullptr || var == 0) { return MOD_INVALID_ARGUMENT; } - const auto recordIt = s_modConfig.find(mod); - if (recordIt != s_modConfig.end()) { - auto& record = recordIt->second; - const auto entry = - std::ranges::find_if(record.vars, [&](const auto& e) { return e.handle == var; }); - if (entry != record.vars.end()) { - std::erase_if(record.subscriptions, [&](const ModConfigSubscription& sub) { - if (sub.varHandle != var) { - return false; - } - config::unsubscribe(sub.coreSubscription); - return true; - }); - - // The persisted value is stashed and restored by a future registration of the - // same name. - config::unregister(*entry->var); - record.vars.erase(entry); - return MOD_OK; - } + const auto* entry = s_slots.find_owned(var, *mod); + if (entry == nullptr || entry->value.kind != ConfigSlotKind::Var) { + Log.error("[{}] config unregister failed: unknown handle {}", mod->metadata.id, var); + return MOD_INVALID_ARGUMENT; } - Log.error("[{}] config unregister failed: unknown handle {}", mod->metadata.id, var); - return MOD_INVALID_ARGUMENT; + + // Only the owning mod can (currently) subscribe to a var + std::vector bound; + s_slots.for_each([&](const uint64_t handle, const auto& e) { + if (e.value.kind == ConfigSlotKind::Subscription && e.value.varHandle == var) { + bound.push_back(handle); + } + }); + for (const auto handle : bound) { + config::unsubscribe(s_slots.take(handle)->value.coreSubscription); + } + + // The persisted value is stashed and restored by a future registration of the same name. + config::unregister(*s_slots.take(var)->value.var); + return MOD_OK; } ModResult config_get_bool(ModContext* context, ConfigVarHandle var, bool* outValue) { @@ -361,22 +348,13 @@ ModResult config_subscribe(ModContext* context, ConfigVarHandle var, ConfigChang if (mod == nullptr || var == 0 || callback == nullptr) { return MOD_INVALID_ARGUMENT; } - const auto recordIt = s_modConfig.find(mod); - if (recordIt == s_modConfig.end()) { - return MOD_INVALID_ARGUMENT; - } - auto& record = recordIt->second; - const auto entry = - std::ranges::find_if(record.vars, [&](const auto& e) { return e.handle == var; }); - if (entry == record.vars.end()) { + const auto* entry = s_slots.find_owned(var, *mod); + if (entry == nullptr || entry->value.kind != ConfigSlotKind::Var) { return MOD_INVALID_ARGUMENT; } - auto& sub = record.subscriptions.emplace_back(); - sub.handle = s_nextHandle++; - sub.varHandle = var; - sub.coreSubscription = config::subscribe(entry->var->getName(), - [modPtr = mod, callback, userData, varHandle = var, type = entry->type]( + const auto coreSubscription = config::subscribe(entry->value.var->getName(), + [modPtr = mod, callback, userData, varHandle = var, type = entry->value.type]( config::ConfigVarBase& varBase, const void* previous) { const ConfigVarValue previousValue = translate_previous(type, previous); std::string stringStorage; @@ -390,8 +368,13 @@ ModResult config_subscribe(ModContext* context, ConfigVarHandle var, ConfigChang fail_mod(*modPtr, MOD_ERROR, "Unknown exception in config change callback"); } }); + const auto handle = s_slots.emplace(*mod, ConfigSlot{ + .kind = ConfigSlotKind::Subscription, + .varHandle = var, + .coreSubscription = coreSubscription, + }); if (outHandle != nullptr) { - *outHandle = sub.handle; + *outHandle = handle; } return MOD_OK; } @@ -401,19 +384,14 @@ ModResult config_unsubscribe(ModContext* context, ConfigSubscriptionHandle handl if (mod == nullptr || handle == 0) { return MOD_INVALID_ARGUMENT; } - const auto recordIt = s_modConfig.find(mod); - if (recordIt != s_modConfig.end()) { - auto& subscriptions = recordIt->second.subscriptions; - const auto sub = - std::ranges::find_if(subscriptions, [&](const auto& s) { return s.handle == handle; }); - if (sub != subscriptions.end()) { - config::unsubscribe(sub->coreSubscription); - subscriptions.erase(sub); - return MOD_OK; - } + const auto* entry = s_slots.find_owned(handle, *mod); + if (entry == nullptr || entry->value.kind != ConfigSlotKind::Subscription) { + Log.error("[{}] config unsubscribe failed: unknown handle {}", mod->metadata.id, handle); + return MOD_INVALID_ARGUMENT; } - Log.error("[{}] config unsubscribe failed: unknown handle {}", mod->metadata.id, handle); - return MOD_INVALID_ARGUMENT; + config::unsubscribe(entry->value.coreSubscription); + s_slots.erase(handle); + return MOD_OK; } constexpr ConfigService s_configService{ diff --git a/src/dusk/mods/svc/gfx.cpp b/src/dusk/mods/svc/gfx.cpp index 7fa472454d..e14b587f5e 100644 --- a/src/dusk/mods/svc/gfx.cpp +++ b/src/dusk/mods/svc/gfx.cpp @@ -1,4 +1,5 @@ #include "registry.hpp" +#include "slot_map.hpp" #include "aurora/lib/logging.hpp" #include "dusk/gfx.hpp" @@ -35,9 +36,6 @@ enum class GfxStreamBuffer : uint8_t { struct GfxSlot { GfxSlotKind kind = GfxSlotKind::DrawType; - uint32_t generation = 1; - bool alive = false; - LoadedMod* owner = nullptr; ModContext* ownerContext = nullptr; std::string ownerId; void* userData = nullptr; @@ -60,71 +58,37 @@ struct WorkerFailure { }; std::mutex s_mutex; -std::vector s_slots; -std::vector s_freeSlots; +using GfxSlotMap = svc::SlotMap; +GfxSlotMap s_slots; std::vector s_workerFailures; bool s_modOffscreenOpen = false; -constexpr uint64_t make_handle(uint32_t index, uint32_t generation) { - return static_cast(generation) << 32 | index; +GfxSlotMap::Entry* resolve_entry_locked(uint64_t handle, GfxSlotKind kind) { + auto* entry = s_slots.find(handle); + if (entry == nullptr || entry->value.kind != kind) { + return nullptr; + } + return entry; } GfxSlot* resolve_slot_locked(uint64_t handle, GfxSlotKind kind) { - const auto index = static_cast(handle & 0xFFFFFFFFu); - const auto generation = static_cast(handle >> 32); - if (handle == 0 || index >= s_slots.size()) { - return nullptr; - } - auto& slot = s_slots[index]; - if (!slot.alive || slot.generation != generation || slot.kind != kind) { - return nullptr; - } - return &slot; + auto* entry = resolve_entry_locked(handle, kind); + return entry != nullptr ? &entry->value : nullptr; } GfxSlot* resolve_owned_slot_locked(LoadedMod& mod, uint64_t handle, GfxSlotKind kind) { - auto* slot = resolve_slot_locked(handle, kind); - if (slot == nullptr || slot->owner != &mod) { + auto* entry = s_slots.find_owned(handle, mod); + if (entry == nullptr || entry->value.kind != kind) { return nullptr; } - return slot; + return &entry->value; } -uint32_t alloc_slot_locked() { - if (!s_freeSlots.empty()) { - const auto index = s_freeSlots.back(); - s_freeSlots.pop_back(); - return index; - } - const auto index = static_cast(s_slots.size()); - s_slots.emplace_back(); - return index; -} - -void free_slot_locked(uint32_t index) { - auto& slot = s_slots[index]; - slot.alive = false; - ++slot.generation; - slot.owner = nullptr; - slot.ownerContext = nullptr; - slot.ownerId.clear(); - slot.userData = nullptr; - slot.drawFn = nullptr; - slot.auroraDrawId = aurora::gfx::InvalidDrawType; - slot.stageFn = nullptr; - slot.stage = GFX_STAGE_SCENE_AFTER_TERRAIN; - slot.computeFn = nullptr; - slot.auroraTaskId = aurora::gfx::InvalidEncoderTask; - s_freeSlots.push_back(index); -} - -void collect_mod_slots_locked(LoadedMod* owner, std::vector& drawIds, +void collect_mod_slots_locked(LoadedMod& owner, std::vector& drawIds, std::vector& taskIds) { - for (uint32_t i = 0; i < s_slots.size(); ++i) { - auto& slot = s_slots[i]; - if (!slot.alive || slot.owner != owner) { - continue; - } + auto entries = s_slots.take_all(owner); + for (auto& entry : entries) { + const auto& slot = entry.value; if (slot.kind == GfxSlotKind::DrawType && slot.auroraDrawId != aurora::gfx::InvalidDrawType) { drawIds.push_back(slot.auroraDrawId); @@ -133,7 +97,6 @@ void collect_mod_slots_locked(LoadedMod* owner, std::vectordrawFn; - userData = slot->userData; - modContext = slot->ownerContext; - owner = slot->owner; - ownerId = slot->ownerId; + const auto& slot = entry->value; + fn = slot.drawFn; + userData = slot.userData; + modContext = slot.ownerContext; + owner = entry->owner; + ownerId = slot.ownerId; } GfxDrawContext drawContext{ @@ -200,7 +164,7 @@ void draw_trampoline(const aurora::gfx::DrawContext& ctx, const wgpu::RenderPass .modId = std::move(ownerId), .message = std::move(failure), }; - collect_mod_slots_locked(owner, record.drawIds, record.taskIds); + collect_mod_slots_locked(*owner, record.drawIds, record.taskIds); s_workerFailures.push_back(std::move(record)); } @@ -214,15 +178,16 @@ void compute_trampoline(const aurora::gfx::EncoderTaskContext& ctx, const wgpu:: std::string ownerId; { std::lock_guard lock{s_mutex}; - auto* slot = resolve_slot_locked(handle, GfxSlotKind::ComputeType); - if (slot == nullptr) { + auto* entry = resolve_entry_locked(handle, GfxSlotKind::ComputeType); + if (entry == nullptr) { return; } - fn = slot->computeFn; - userData = slot->userData; - modContext = slot->ownerContext; - owner = slot->owner; - ownerId = slot->ownerId; + const auto& slot = entry->value; + fn = slot.computeFn; + userData = slot.userData; + modContext = slot.ownerContext; + owner = entry->owner; + ownerId = slot.ownerId; } GfxComputeContext computeContext{ @@ -251,7 +216,7 @@ void compute_trampoline(const aurora::gfx::EncoderTaskContext& ctx, const wgpu:: .modId = std::move(ownerId), .message = std::move(failure), }; - collect_mod_slots_locked(owner, record.drawIds, record.taskIds); + collect_mod_slots_locked(*owner, record.drawIds, record.taskIds); s_workerFailures.push_back(std::move(record)); } @@ -264,16 +229,13 @@ ModResult gfx_register_draw_type( uint64_t handle = 0; { std::lock_guard lock{s_mutex}; - const auto index = alloc_slot_locked(); - auto& slot = s_slots[index]; - slot.kind = GfxSlotKind::DrawType; - slot.alive = true; - slot.owner = &mod; - slot.ownerContext = mod.context.get(); - slot.ownerId = mod.metadata.id; - slot.userData = userData; - slot.drawFn = draw; - handle = make_handle(index, slot.generation); + handle = s_slots.emplace(mod, GfxSlot{ + .kind = GfxSlotKind::DrawType, + .ownerContext = mod.context.get(), + .ownerId = mod.metadata.id, + .userData = userData, + .drawFn = draw, + }); } const auto auroraId = aurora::gfx::register_draw_type(aurora::gfx::DrawTypeDescriptor{ @@ -283,9 +245,7 @@ ModResult gfx_register_draw_type( }); if (auroraId == aurora::gfx::InvalidDrawType) { std::lock_guard lock{s_mutex}; - if (resolve_owned_slot_locked(mod, handle, GfxSlotKind::DrawType) != nullptr) { - free_slot_locked(static_cast(handle & 0xFFFFFFFFu)); - } + s_slots.erase_owned(handle, mod); return MOD_ERROR; } @@ -308,7 +268,7 @@ ModResult gfx_unregister_draw_type(LoadedMod& mod, uint64_t handle) { return MOD_INVALID_ARGUMENT; } auroraId = slot->auroraDrawId; - free_slot_locked(static_cast(handle & 0xFFFFFFFFu)); + s_slots.erase_owned(handle, mod); } aurora::gfx::unregister_draw_type(auroraId); return MOD_OK; @@ -359,17 +319,14 @@ ModResult gfx_register_stage_hook( LoadedMod& mod, GfxStage stage, GfxStageFn callback, void* userData, uint64_t& outHandle) { outHandle = 0; std::lock_guard lock{s_mutex}; - const auto index = alloc_slot_locked(); - auto& slot = s_slots[index]; - slot.kind = GfxSlotKind::StageHook; - slot.alive = true; - slot.owner = &mod; - slot.ownerContext = mod.context.get(); - slot.ownerId = mod.metadata.id; - slot.userData = userData; - slot.stageFn = callback; - slot.stage = stage; - outHandle = make_handle(index, slot.generation); + outHandle = s_slots.emplace(mod, GfxSlot{ + .kind = GfxSlotKind::StageHook, + .ownerContext = mod.context.get(), + .ownerId = mod.metadata.id, + .userData = userData, + .stageFn = callback, + .stage = stage, + }); return MOD_OK; } @@ -379,7 +336,7 @@ ModResult gfx_unregister_stage_hook(LoadedMod& mod, uint64_t handle) { if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } - free_slot_locked(static_cast(handle & 0xFFFFFFFFu)); + s_slots.erase_owned(handle, mod); return MOD_OK; } @@ -429,16 +386,13 @@ ModResult gfx_register_compute_type( uint64_t handle = 0; { std::lock_guard lock{s_mutex}; - const auto index = alloc_slot_locked(); - auto& slot = s_slots[index]; - slot.kind = GfxSlotKind::ComputeType; - slot.alive = true; - slot.owner = &mod; - slot.ownerContext = mod.context.get(); - slot.ownerId = mod.metadata.id; - slot.userData = userData; - slot.computeFn = callback; - handle = make_handle(index, slot.generation); + handle = s_slots.emplace(mod, GfxSlot{ + .kind = GfxSlotKind::ComputeType, + .ownerContext = mod.context.get(), + .ownerId = mod.metadata.id, + .userData = userData, + .computeFn = callback, + }); } const auto auroraId = @@ -449,9 +403,7 @@ ModResult gfx_register_compute_type( }); if (auroraId == aurora::gfx::InvalidEncoderTask) { std::lock_guard lock{s_mutex}; - if (resolve_owned_slot_locked(mod, handle, GfxSlotKind::ComputeType) != nullptr) { - free_slot_locked(static_cast(handle & 0xFFFFFFFFu)); - } + s_slots.erase_owned(handle, mod); return MOD_ERROR; } @@ -474,7 +426,7 @@ ModResult gfx_unregister_compute_type(LoadedMod& mod, uint64_t handle) { return MOD_INVALID_ARGUMENT; } auroraId = slot->auroraTaskId; - free_slot_locked(static_cast(handle & 0xFFFFFFFFu)); + s_slots.erase_owned(handle, mod); } aurora::gfx::unregister_encoder_task_type(auroraId); return MOD_OK; @@ -510,18 +462,18 @@ void gfx_run_stage( std::vector entries; { std::lock_guard lock{s_mutex}; - for (uint32_t i = 0; i < s_slots.size(); ++i) { - const auto& slot = s_slots[i]; - if (slot.alive && slot.kind == GfxSlotKind::StageHook && slot.stage == stage) { + s_slots.for_each([&](uint64_t handle, const auto& slotEntry) { + const auto& slot = slotEntry.value; + if (slot.kind == GfxSlotKind::StageHook && slot.stage == stage) { entries.push_back(StageEntry{ - .handle = make_handle(i, slot.generation), + .handle = handle, .fn = slot.stageFn, .userData = slot.userData, .context = slot.ownerContext, - .owner = slot.owner, + .owner = slotEntry.owner, }); } - } + }); } if (entries.empty()) { return; @@ -600,7 +552,7 @@ void gfx_remove_mod(LoadedMod& mod) { std::vector taskIds; { std::lock_guard lock{s_mutex}; - collect_mod_slots_locked(&mod, drawIds, taskIds); + collect_mod_slots_locked(mod, drawIds, taskIds); } if (drawIds.empty() && taskIds.empty()) { return; diff --git a/src/dusk/mods/svc/host.cpp b/src/dusk/mods/svc/host.cpp index 5904903ee7..0199a7865a 100644 --- a/src/dusk/mods/svc/host.cpp +++ b/src/dusk/mods/svc/host.cpp @@ -1,4 +1,5 @@ #include "registry.hpp" +#include "slot_map.hpp" #include "dusk/mods/loader/loader.hpp" #include "dusk/mods/manifest.hpp" @@ -63,14 +64,13 @@ const char* host_mod_dir(ModContext* context) { } struct LifecycleWatcher { - uint64_t handle = 0; - LoadedMod* owner = nullptr; ModLifecycleFn fn = nullptr; void* userData = nullptr; + uint64_t order = 0; }; -std::vector s_watchers; -uint64_t s_nextWatchHandle = 1; +SlotMap s_watchers; +uint64_t s_nextWatchOrder = 0; ModResult host_watch_mod_lifecycle( ModContext* context, ModLifecycleFn fn, void* userData, uint64_t* outHandle) { @@ -78,8 +78,8 @@ ModResult host_watch_mod_lifecycle( if (mod == nullptr || fn == nullptr || outHandle == nullptr) { return MOD_INVALID_ARGUMENT; } - const auto handle = s_nextWatchHandle++; - s_watchers.push_back({handle, mod, fn, userData}); + const auto handle = s_watchers.emplace( + *mod, LifecycleWatcher{.fn = fn, .userData = userData, .order = s_nextWatchOrder++}); *outHandle = handle; return MOD_OK; } @@ -89,32 +89,41 @@ ModResult host_unwatch_mod_lifecycle(ModContext* context, const uint64_t handle) if (mod == nullptr) { return MOD_INVALID_ARGUMENT; } - const auto erased = std::erase_if(s_watchers, - [&](const LifecycleWatcher& w) { return w.handle == handle && w.owner == mod; }); - return erased != 0 ? MOD_OK : MOD_INVALID_ARGUMENT; + return s_watchers.erase_owned(handle, *mod) ? MOD_OK : MOD_INVALID_ARGUMENT; } void host_mod_detached(LoadedMod& mod) { // The subject's own watches go first: a mod is never notified about its own teardown. - std::erase_if(s_watchers, [&](const LifecycleWatcher& w) { return w.owner == &mod; }); + s_watchers.erase_all(mod); - // Iterate a snapshot: callbacks may watch/unwatch, and a failing callback erases the - // failing mod's services. - const auto snapshot = s_watchers; - for (const auto& watcher : snapshot) { - const bool alive = std::ranges::any_of( - s_watchers, [&](const LifecycleWatcher& w) { return w.handle == watcher.handle; }); - if (!alive) { + // Iterate a snapshot in registration order: callbacks may watch/unwatch, and a failing + // callback erases the failing mod's services. + struct PendingNotify { + uint64_t order; + uint64_t handle; + }; + std::vector snapshot; + s_watchers.for_each([&](const uint64_t handle, const auto& entry) { + snapshot.push_back({.order = entry.value.order, .handle = handle}); + }); + std::ranges::sort(snapshot, {}, &PendingNotify::order); + + for (const auto& pending : snapshot) { + const auto* entry = s_watchers.find(pending.handle); + if (entry == nullptr) { continue; } + // Do not retain pointers into SlotMap across a callback that may mutate it. + auto* owner = entry->owner; + const auto watcher = entry->value; try { - watcher.fn(watcher.owner->context.get(), mod.context.get(), mod.metadata.id.c_str(), + watcher.fn(owner->context.get(), mod.context.get(), mod.metadata.id.c_str(), MOD_LIFECYCLE_DETACHED, watcher.userData); } catch (const std::exception& e) { - fail_mod(*watcher.owner, MOD_ERROR, + fail_mod(*owner, MOD_ERROR, fmt::format("Exception in mod lifecycle callback: {}", e.what())); } catch (...) { - fail_mod(*watcher.owner, MOD_ERROR, "Unknown exception in mod lifecycle callback"); + fail_mod(*owner, MOD_ERROR, "Unknown exception in mod lifecycle callback"); } } } diff --git a/src/dusk/mods/svc/overlay.cpp b/src/dusk/mods/svc/overlay.cpp index 13cab1ac0f..44eef6ab17 100644 --- a/src/dusk/mods/svc/overlay.cpp +++ b/src/dusk/mods/svc/overlay.cpp @@ -1,10 +1,12 @@ #include "registry.hpp" +#include "slot_map.hpp" #include "aurora/dvd.h" #include "aurora/lib/logging.hpp" #include "dusk/mods/loader/loader.hpp" #include "mods/svc/overlay.h" +#include #include #include #include @@ -33,15 +35,15 @@ std::unordered_map s_overlayFiles; uintptr_t s_nextOverlayId = 1; std::mutex s_overlayMutex; -struct RuntimeOverlayEntry { - uint64_t handle = 0; +struct RuntimeOverlaySlot { std::string discPath; std::string bundlePath; // bundle-backed if non-empty std::shared_ptr> buffer; // buffer-backed otherwise size_t size = 0; + uint64_t order = 0; }; -std::unordered_map> s_runtimeOverlays; -uint64_t s_nextRuntimeHandle = 1; +SlotMap s_runtimeOverlays; +uint64_t s_nextRuntimeOrder = 0; bool s_overlaysDirty = false; // Aurora matches overlay paths against the disc case-insensitively and later entries win, so @@ -84,20 +86,25 @@ void find_overlay_files(std::vector& files, LoadedMod& mod, void append_runtime_overlays(std::vector& files, LoadedMod& mod, std::unordered_map& claims) { - const auto it = s_runtimeOverlays.find(&mod); - if (it == s_runtimeOverlays.end()) { - return; - } - - for (const auto& entry : it->second) { - const auto id = s_nextOverlayId++; - if (entry.buffer != nullptr) { - s_overlayFiles.emplace(id, OverlayFileData{{}, nullptr, entry.buffer}); - } else { - s_overlayFiles.emplace(id, OverlayFileData{entry.bundlePath, mod.bundle, nullptr}); + // Aurora resolves duplicate paths later-entry-wins, so emit in registration order (SlotMap + // iteration is index order, and freed indices are reused). + std::vector slots; + s_runtimeOverlays.for_each([&](uint64_t, const auto& entry) { + if (entry.owner == &mod) { + slots.push_back(&entry.value); } - claim_overlay_path(claims, entry.discPath, mod); - files.emplace_back(strdup(entry.discPath.c_str()), reinterpret_cast(id), entry.size); + }); + std::ranges::sort(slots, {}, &RuntimeOverlaySlot::order); + + for (const auto* slot : slots) { + const auto id = s_nextOverlayId++; + if (slot->buffer != nullptr) { + s_overlayFiles.emplace(id, OverlayFileData{{}, nullptr, slot->buffer}); + } else { + s_overlayFiles.emplace(id, OverlayFileData{slot->bundlePath, mod.bundle, nullptr}); + } + claim_overlay_path(claims, slot->discPath, mod); + files.emplace_back(strdup(slot->discPath.c_str()), reinterpret_cast(id), slot->size); } } @@ -201,47 +208,39 @@ void overlay_sync_files() { uint64_t overlay_add_file( LoadedMod& mod, std::string discPath, std::string bundlePath, size_t size) { - const auto handle = s_nextRuntimeHandle++; - s_runtimeOverlays[&mod].push_back({ - .handle = handle, - .discPath = std::move(discPath), - .bundlePath = std::move(bundlePath), - .size = size, - }); + const auto handle = s_runtimeOverlays.emplace(mod, RuntimeOverlaySlot{ + .discPath = std::move(discPath), + .bundlePath = std::move(bundlePath), + .size = size, + .order = s_nextRuntimeOrder++, + }); s_overlaysDirty = true; return handle; } uint64_t overlay_add_buffer(LoadedMod& mod, std::string discPath, std::vector data) { - const auto handle = s_nextRuntimeHandle++; const auto size = data.size(); - s_runtimeOverlays[&mod].push_back({ - .handle = handle, - .discPath = std::move(discPath), - .buffer = std::make_shared>(std::move(data)), - .size = size, - }); + const auto handle = s_runtimeOverlays.emplace(mod, + RuntimeOverlaySlot{ + .discPath = std::move(discPath), + .buffer = std::make_shared>(std::move(data)), + .size = size, + .order = s_nextRuntimeOrder++, + }); s_overlaysDirty = true; return handle; } bool overlay_remove(LoadedMod& mod, uint64_t handle) { - const auto it = s_runtimeOverlays.find(&mod); - if (it == s_runtimeOverlays.end()) { + if (!s_runtimeOverlays.erase_owned(handle, mod)) { return false; } - if (std::erase_if(it->second, [&](const auto& entry) { return entry.handle == handle; }) == 0) { - return false; - } - if (it->second.empty()) { - s_runtimeOverlays.erase(it); - } s_overlaysDirty = true; return true; } void overlay_remove_mod(LoadedMod& mod) { - if (s_runtimeOverlays.erase(&mod) != 0) { + if (s_runtimeOverlays.erase_all(mod) != 0) { s_overlaysDirty = true; } } diff --git a/src/dusk/mods/svc/slot_map.hpp b/src/dusk/mods/svc/slot_map.hpp new file mode 100644 index 0000000000..3e45635d79 --- /dev/null +++ b/src/dusk/mods/svc/slot_map.hpp @@ -0,0 +1,188 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dusk::mods { + +struct LoadedMod; + +namespace svc { + +template +class SlotMap { +public: + static_assert(std::is_nothrow_move_constructible_v); + + using Handle = uint64_t; + static constexpr Handle InvalidHandle = 0; + + struct Entry { + LoadedMod* owner = nullptr; + T value; + }; + + template + Handle emplace(LoadedMod& owner, Args&&... args) { + T value{std::forward(args)...}; + const auto index = allocate_index(); + auto& slot = m_slots[index]; + slot.entry.emplace(Entry{.owner = &owner, .value = std::move(value)}); + return make_handle(index, slot.generation); + } + + // Returned pointers remain valid only until the next mutating operation. + Entry* find(Handle handle) { + auto* slot = find_slot(handle); + return slot != nullptr ? &*slot->entry : nullptr; + } + + const Entry* find(Handle handle) const { + const auto* slot = find_slot(handle); + return slot != nullptr ? &*slot->entry : nullptr; + } + + Entry* find_owned(Handle handle, const LoadedMod& owner) { + auto* entry = find(handle); + return entry != nullptr && entry->owner == &owner ? entry : nullptr; + } + + const Entry* find_owned(Handle handle, const LoadedMod& owner) const { + const auto* entry = find(handle); + return entry != nullptr && entry->owner == &owner ? entry : nullptr; + } + + std::optional take(Handle handle) { + const auto index = handle_index(handle); + auto* slot = find_slot(handle); + if (slot == nullptr) { + return std::nullopt; + } + std::optional entry{std::move(slot->entry)}; + release_slot(index); + return entry; + } + + std::optional take_owned(Handle handle, const LoadedMod& owner) { + if (find_owned(handle, owner) == nullptr) { + return std::nullopt; + } + return take(handle); + } + + std::vector take_all(const LoadedMod& owner) { + std::vector entries; + for (size_t slotIndex = 0; slotIndex < m_slots.size(); ++slotIndex) { + const auto index = static_cast(slotIndex); + auto& slot = m_slots[index]; + if (!slot.entry.has_value() || slot.entry->owner != &owner) { + continue; + } + entries.push_back(std::move(*slot.entry)); + release_slot(index); + } + return entries; + } + + bool erase(Handle handle) { + const auto index = handle_index(handle); + if (find_slot(handle) == nullptr) { + return false; + } + release_slot(index); + return true; + } + + bool erase_owned(Handle handle, const LoadedMod& owner) { + if (find_owned(handle, owner) == nullptr) { + return false; + } + return erase(handle); + } + + size_t erase_all(const LoadedMod& owner) { + return take_all(owner).size(); + } + + template + void for_each(Fn&& fn) const { + // The visitor may inspect entries but must not mutate this SlotMap. + for (size_t slotIndex = 0; slotIndex < m_slots.size(); ++slotIndex) { + const auto index = static_cast(slotIndex); + const auto& slot = m_slots[index]; + if (slot.entry.has_value()) { + fn(make_handle(index, slot.generation), *slot.entry); + } + } + } + +private: + struct Slot { + uint32_t generation = 1; + std::optional entry; + }; + + static constexpr Handle make_handle(uint32_t index, uint32_t generation) { + return static_cast(generation) << 32 | index; + } + + static constexpr uint32_t handle_index(Handle handle) { + return static_cast(handle & std::numeric_limits::max()); + } + + static constexpr uint32_t handle_generation(Handle handle) { + return static_cast(handle >> 32); + } + + Slot* find_slot(Handle handle) { + return const_cast(std::as_const(*this).find_slot(handle)); + } + + const Slot* find_slot(Handle handle) const { + const auto index = handle_index(handle); + if (handle == InvalidHandle || index >= m_slots.size()) { + return nullptr; + } + const auto& slot = m_slots[index]; + if (!slot.entry.has_value() || slot.generation != handle_generation(handle)) { + return nullptr; + } + return &slot; + } + + uint32_t allocate_index() { + if (!m_freeSlots.empty()) { + const auto index = m_freeSlots.back(); + m_freeSlots.pop_back(); + return index; + } + if (m_slots.size() > std::numeric_limits::max()) { + throw std::length_error{"SlotMap handle space exhausted"}; + } + const auto index = static_cast(m_slots.size()); + m_slots.emplace_back(); + return index; + } + + void release_slot(uint32_t index) { + auto& slot = m_slots[index]; + slot.entry.reset(); + if (slot.generation == std::numeric_limits::max()) { + return; + } + ++slot.generation; + m_freeSlots.push_back(index); + } + + std::vector m_slots; + std::vector m_freeSlots; +}; + +} // namespace svc +} // namespace dusk::mods diff --git a/src/dusk/mods/svc/ui.cpp b/src/dusk/mods/svc/ui.cpp index 72366aaab7..cf4ea86808 100644 --- a/src/dusk/mods/svc/ui.cpp +++ b/src/dusk/mods/svc/ui.cpp @@ -2,6 +2,7 @@ #include "config.hpp" #include "registry.hpp" +#include "slot_map.hpp" #include "aurora/lib/logging.hpp" #include "dusk/mod_loader.hpp" @@ -33,7 +34,6 @@ namespace { aurora::Module Log("dusk::mods::ui"); enum class UiSlotKind : u8 { - Free, Window, Dialog, Pane, @@ -63,17 +63,14 @@ const char* slot_kind_name(UiSlotKind kind) { case UiSlotKind::MenuTab: return "menu tab"; default: - return "free"; + return "unknown"; } } -// Generation-checked handle slots. Game thread only: all mutations happen in service calls made -// from mod code, in UI callbacks (ui::update), or in the loader's deactivate paths. +// Game thread only: all mutations happen in service calls made from mod code, in UI callbacks +// (ui::update), or in the loader's deactivate paths. struct UiSlot { - // Bumped on free, which invalidates every outstanding handle to this slot. - uint32_t generation = 1; - UiSlotKind kind = UiSlotKind::Free; - LoadedMod* owner = nullptr; + UiSlotKind kind = UiSlotKind::Window; // Pane/Text/Progress/Control: freed automatically when the element is destroyed Rml::Element* element = nullptr; // Pane payload @@ -93,8 +90,7 @@ struct UiSlot { bool hasElementValue = false; }; -std::vector s_slots; -std::vector s_freeSlots; +SlotMap s_slots; struct ModUiPanel { UiPanelBuildFn build = nullptr; @@ -112,71 +108,26 @@ struct ModMenuTab { std::unordered_map> s_modMenuTabs; bool s_menuTabsDirty = false; -uint64_t handle_for(uint32_t index) { - return (uint64_t{s_slots[index].generation} << 32) | index; -} - -uint32_t slot_index(const UiSlot& slot) { - return static_cast(&slot - s_slots.data()); -} - UiSlot* slot_from_handle(uint64_t handle) { - const auto index = static_cast(handle & 0xFFFFFFFFu); - const auto generation = static_cast(handle >> 32); - if (index >= s_slots.size()) { - return nullptr; - } - auto& slot = s_slots[index]; - if (slot.kind == UiSlotKind::Free || slot.generation != generation) { - return nullptr; - } - return &slot; + auto* entry = s_slots.find(handle); + return entry != nullptr ? &entry->value : nullptr; } // Note: s_slots may reallocate on any later allocation, so callers must not hold the returned // slot reference across calls that can allocate (e.g. mod build callbacks); re-resolve instead. UiSlot& alloc_slot(LoadedMod& mod, UiSlotKind kind, uint64_t& outHandle) { - uint32_t index; - if (!s_freeSlots.empty()) { - index = s_freeSlots.back(); - s_freeSlots.pop_back(); - } else { - index = static_cast(s_slots.size()); - s_slots.emplace_back(); - } - auto& slot = s_slots[index]; - slot.kind = kind; - slot.owner = &mod; - outHandle = handle_for(index); - return slot; -} - -void free_slot(UiSlot& slot) { - slot.generation++; - slot.kind = UiSlotKind::Free; - slot.owner = nullptr; - slot.element = nullptr; - slot.pane = nullptr; - slot.helpPane = nullptr; - slot.document = nullptr; - slot.onClosed = nullptr; - slot.onClosedUserData = nullptr; - slot.styleScope = ui::DocumentScope::None; - slot.styleId.clear(); - slot.elementRml.clear(); - slot.elementFloat = 0.0f; - slot.hasElementValue = false; - s_freeSlots.push_back(slot_index(slot)); + outHandle = s_slots.emplace(mod, UiSlot{.kind = kind}); + return s_slots.find(outHandle)->value; } UiSlot* resolve(LoadedMod& mod, uint64_t handle, UiSlotKind kind, const char* what) { - auto* slot = slot_from_handle(handle); - if (slot == nullptr || slot->owner != &mod || slot->kind != kind) { + auto* entry = s_slots.find_owned(handle, mod); + if (entry == nullptr || entry->value.kind != kind) { Log.error("[{}] {}: stale or invalid {} handle {:#x}", mod.metadata.id, what, slot_kind_name(kind), handle); return nullptr; } - return slot; + return &entry->value; } // Whether the registration a callback was created under is still live. Callbacks captured by @@ -184,7 +135,7 @@ UiSlot* resolve(LoadedMod& mod, uint64_t handle, UiSlotKind kind, const char* wh // once a reload completes, but captured fn pointers still target the unloaded image. Teardown // frees the slots (ui_remove_mod), which invalidates every callback built under them. bool slot_live(uint64_t handle) { - return slot_from_handle(handle) != nullptr; + return s_slots.find(handle) != nullptr; } bool dialog_open(uint64_t handle) { @@ -197,30 +148,22 @@ bool dialog_open(uint64_t handle) { // The generation check makes a late detach of an already-recycled slot a no-op. class SlotDetachListener final : public Rml::EventListener { public: - SlotDetachListener(uint32_t index, uint32_t generation) - : m_index{index}, m_generation{generation} {} + explicit SlotDetachListener(uint64_t handle) : m_handle{handle} {} void ProcessEvent(Rml::Event&) override {} void OnDetach(Rml::Element*) override { - if (m_index < s_slots.size()) { - auto& slot = s_slots[m_index]; - if (slot.kind != UiSlotKind::Free && slot.generation == m_generation) { - free_slot(slot); - } - } + s_slots.erase(m_handle); delete this; } private: - uint32_t m_index; - uint32_t m_generation; + uint64_t m_handle; }; -void track_element(UiSlot& slot, Rml::Element& element) { +void track_element(uint64_t handle, UiSlot& slot, Rml::Element& element) { slot.element = &element; - element.AddEventListener( - Rml::EventId::Click, new SlotDetachListener(slot_index(slot), slot.generation)); + element.AddEventListener(Rml::EventId::Click, new SlotDetachListener{handle}); } template @@ -269,7 +212,7 @@ uint64_t wrap_pane(LoadedMod& mod, ui::Pane& pane, ui::Pane* helpPane) { auto& slot = alloc_slot(mod, UiSlotKind::Pane, handle); slot.pane = &pane; slot.helpPane = helpPane; - track_element(slot, *pane.root()); + track_element(handle, slot, *pane.root()); return handle; } @@ -446,14 +389,14 @@ bool wire_config_var_binding(LoadedMod& mod, const UiControlDesc& desc, ui::ModC } void on_mod_window_destroyed(uint64_t handle) { - auto* slot = slot_from_handle(handle); - if (slot == nullptr || slot->kind != UiSlotKind::Window) { + const auto* entry = s_slots.find(handle); + if (entry == nullptr || entry->value.kind != UiSlotKind::Window) { return; } - LoadedMod* mod = slot->owner; - const UiWindowClosedFn onClosed = slot->onClosed; - void* userData = slot->onClosedUserData; - free_slot(*slot); + auto released = s_slots.take(handle); + auto* mod = released->owner; + const UiWindowClosedFn onClosed = released->value.onClosed; + void* userData = released->value.onClosedUserData; if (mod != nullptr && onClosed != nullptr) { guarded_call(*mod, "window on_closed callback", [&] { onClosed(mod->context.get(), handle, userData); @@ -464,7 +407,7 @@ void on_mod_window_destroyed(uint64_t handle) { void on_mod_dialog_destroyed(uint64_t handle) { auto* slot = slot_from_handle(handle); if (slot != nullptr && slot->kind == UiSlotKind::Dialog) { - free_slot(*slot); + s_slots.erase(handle); } } @@ -573,7 +516,7 @@ ModResult ui_pane_add_text(LoadedMod& mod, uint64_t pane, const char* text, uint auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem); elemSlot.elementRml = ui::escape(text); elemSlot.hasElementValue = true; - track_element(elemSlot, *elem); + track_element(*outElem, elemSlot, *elem); } return MOD_OK; } @@ -588,7 +531,7 @@ ModResult ui_pane_add_rml(LoadedMod& mod, uint64_t pane, const char* rml, uint64 auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem); elemSlot.elementRml = rml; elemSlot.hasElementValue = true; - track_element(elemSlot, *elem); + track_element(*outElem, elemSlot, *elem); } return MOD_OK; } @@ -604,7 +547,7 @@ ModResult ui_pane_add_progress(LoadedMod& mod, uint64_t pane, float value, uint6 auto& elemSlot = alloc_slot(mod, UiSlotKind::Progress, *outElem); elemSlot.elementFloat = value; elemSlot.hasElementValue = true; - track_element(elemSlot, *elem); + track_element(*outElem, elemSlot, *elem); } return MOD_OK; } @@ -691,7 +634,7 @@ ModResult ui_pane_add_control( } if (outElem != nullptr) { auto& elemSlot = alloc_slot(mod, UiSlotKind::Control, *outElem); - track_element(elemSlot, *control->root()); + track_element(*outElem, elemSlot, *control->root()); } return MOD_OK; } @@ -740,13 +683,13 @@ ModResult ui_elem_set_progress(LoadedMod& mod, uint64_t elem, float value) { } ModResult ui_elem_set_class(LoadedMod& mod, uint64_t elem, const char* name, bool active) { - auto* slot = slot_from_handle(elem); - if (slot == nullptr || slot->owner != &mod || slot->element == nullptr) { + auto* entry = s_slots.find_owned(elem, mod); + if (entry == nullptr || entry->value.element == nullptr) { Log.error( "[{}] elem_set_class: stale or invalid element handle {:#x}", mod.metadata.id, elem); return MOD_INVALID_ARGUMENT; } - slot->element->SetClass(name, active); + entry->value.element->SetClass(name, active); return MOD_OK; } @@ -945,7 +888,7 @@ ModResult ui_unregister_menu_tab(LoadedMod& mod, uint64_t handle) { s_modMenuTabs.erase(it); } } - free_slot(*slot); + s_slots.erase_owned(handle, mod); s_menuTabsDirty = true; return MOD_OK; } @@ -1026,7 +969,7 @@ ModResult ui_register_styles( slot.styleId = fmt::format("{}:{:x}", mod.metadata.id, handle); if (!ui::register_scoped_styles(docScope, slot.styleId, rcss)) { Log.error("[{}] register_styles: failed to parse RCSS", mod.metadata.id); - free_slot(slot); + s_slots.erase(handle); return MOD_INVALID_ARGUMENT; } outHandle = handle; @@ -1056,8 +999,8 @@ ModResult ui_unregister_styles(LoadedMod& mod, uint64_t handle) { if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } - ui::unregister_scoped_styles(slot->styleScope, slot->styleId); - free_slot(*slot); + auto released = s_slots.take_owned(handle, mod); + ui::unregister_scoped_styles(released->value.styleScope, released->value.styleId); return MOD_OK; } @@ -1066,14 +1009,12 @@ void ui_remove_mod(LoadedMod& mod) { if (s_modMenuTabs.erase(&mod) != 0) { s_menuTabsDirty = true; } - for (auto& slot : s_slots) { - if (slot.kind == UiSlotKind::Free || slot.owner != &mod) { - continue; - } + auto entries = s_slots.take_all(mod); + for (auto& entry : entries) { + auto& slot = entry.value; switch (slot.kind) { case UiSlotKind::Window: { auto* window = static_cast(slot.document); - free_slot(slot); if (window != nullptr) { window->force_close(); } @@ -1081,7 +1022,6 @@ void ui_remove_mod(LoadedMod& mod) { } case UiSlotKind::Dialog: { auto* dialog = static_cast(slot.document); - free_slot(slot); if (dialog != nullptr) { dialog->force_close(); } @@ -1089,10 +1029,8 @@ void ui_remove_mod(LoadedMod& mod) { } case UiSlotKind::Style: ui::unregister_scoped_styles(slot.styleScope, slot.styleId); - free_slot(slot); break; default: - free_slot(slot); break; } } diff --git a/src/dusk/ui/logs_window.cpp b/src/dusk/ui/logs_window.cpp index fc9dad96ca..0a6fc2402d 100644 --- a/src/dusk/ui/logs_window.cpp +++ b/src/dusk/ui/logs_window.cpp @@ -73,10 +73,10 @@ std::string format_time(int64_t timeMs) { } Rml::Element* append_span(Rml::Element* parent, const char* className, const Rml::String& text) { - auto span = parent->GetOwnerDocument()->CreateElement("span"); + auto* span = append(parent, "span"); span->SetClass(className, true); - append_text(span.get(), text); - return parent->AppendChild(std::move(span)); + append_text(span, text); + return span; } } // namespace @@ -260,18 +260,18 @@ Rml::Element* LogsWindow::append_log_line(const mods::log::Line& line) { modId = "?"; } - auto elem = mDocument->CreateElement("div"); + auto* elem = append(mLinesElem, "div"); elem->SetClass("log-line", true); elem->SetClass(level_class(line.level), true); constexpr const char* kNbsp = "\xc2\xa0"; - append_span(elem.get(), "log-time", format_time(line.timeMs)); - append_text(elem.get(), kNbsp); - append_span(elem.get(), "log-mod", fmt::format("[{}]", modId)); - append_text(elem.get(), kNbsp); - append_span(elem.get(), "log-msg", line.message); + append_span(elem, "log-time", format_time(line.timeMs)); + append_text(elem, kNbsp); + append_span(elem, "log-mod", fmt::format("[{}]", modId)); + append_text(elem, kNbsp); + append_span(elem, "log-msg", line.message); - return mLinesElem->AppendChild(std::move(elem)); + return elem; } void LogsWindow::copy_to_clipboard() {