Przeglądaj źródła

Merge pull request #91 from Polkm105/threading-improvements

Threading improvements
stan 4 dni temu
rodzic
commit
a1a7968fce
37 zmienionych plików z 699 dodań i 625 usunięć
  1. 1 1
      .clang-tidy
  2. 6 5
      Sunrise/src/client/content/investment/investment_refresh.cpp
  3. 50 47
      Sunrise/src/client/content/investment/worker/investment_refresh_worker.cpp
  4. 4 3
      Sunrise/src/client/hooks/assert_handler/assert_handler_observer.cpp
  5. 0 3
      Sunrise/src/client/hooks/assert_handler/assert_handler_observer.h
  6. 0 1
      Sunrise/src/client/hooks/egress/internal.h
  7. 6 12
      Sunrise/src/client/hooks/egress/lifecycle/egress_guard_lifecycle.cpp
  8. 7 18
      Sunrise/src/client/hooks/graphics/input/graphics_window_input.cpp
  9. 9 10
      Sunrise/src/core/logging/log.cpp
  10. 34 34
      Sunrise/src/core/logging/snapshot/log_snapshot_ring.cpp
  11. 5 10
      Sunrise/src/core/runtime/core_runtime.cpp
  12. 81 0
      Sunrise/src/core/threading/data_mutex.h
  13. 17 0
      Sunrise/src/core/threading/sendable.h
  14. 47 0
      Sunrise/src/core/threading/srw_lock.h
  15. 16 12
      Sunrise/src/middleware/content/packages/reader/package_handle_cache.cpp
  16. 38 71
      Sunrise/src/server/bap/bap_route.cpp
  17. 124 136
      Sunrise/src/server/transport/bap_listener.cpp
  18. 8 12
      Sunrise/src/server/transport/bap_peer_session.cpp
  19. 4 8
      Sunrise/src/server/transport/internal.h
  20. 10 6
      Sunrise/src/state/build_data/abilities/ability_bucket_catalog.cpp
  21. 9 7
      Sunrise/src/state/build_data/collectibles/collectible_catalog.cpp
  22. 8 4
      Sunrise/src/state/build_data/constants/investment_constant_catalog.cpp
  23. 8 6
      Sunrise/src/state/build_data/hash_names/hash_name_catalog.cpp
  24. 9 6
      Sunrise/src/state/build_data/inventory/buckets/inventory_bucket_catalog.cpp
  25. 8 6
      Sunrise/src/state/build_data/items/details/item_detail_catalog.cpp
  26. 11 8
      Sunrise/src/state/build_data/items/item_catalog.cpp
  27. 10 8
      Sunrise/src/state/build_data/items/socket_plugs/socket_plug_catalog.cpp
  28. 9 6
      Sunrise/src/state/build_data/material_requirements/material_requirement_catalog.cpp
  29. 9 6
      Sunrise/src/state/build_data/progressions/progression_catalog.cpp
  30. 13 9
      Sunrise/src/state/build_data/scenarios/scenario_catalog.cpp
  31. 9 5
      Sunrise/src/state/build_data/socket_entry_buckets/socket_entry_bucket_catalog.cpp
  32. 12 9
      Sunrise/src/state/build_data/socket_entry_lists/socket_entry_list_catalog.cpp
  33. 15 13
      Sunrise/src/state/build_data/spawn_sets/spawn_set_catalog.cpp
  34. 0 48
      Sunrise/src/state/build_data/table.h
  35. 18 16
      Sunrise/src/state/build_data/vendors/vendor_catalog.cpp
  36. 4 3
      Sunrise/src/steam/runtime/steam_context_state.cpp
  37. 80 76
      Sunrise/src/steam/runtime/steam_lifecycle.cpp

+ 1 - 1
.clang-tidy

@@ -74,5 +74,5 @@ ExtraArgsBefore:
   - -Wdocumentation
 FormatStyle: file
 CheckOptions:
-  portability-restrict-system-includes.Includes: '-*,Windows.h,WinSock2.h,WS2tcpip.h,MSWSock.h,WinDNS.h,TlHelp32.h,Shellapi.h,bcrypt.h,d3d11.h,detours.h,dxgi.h,wincodec.h,imgui.h,imgui_impl_dx11.h,imgui_impl_win32.h,intrin.h,algorithm,array,atomic,bit,bitset,cctype,charconv,chrono,climits,cmath,cstdarg,cstddef,cstdint,cstdio,cstdlib,cstring,cwchar,limits,memory,new,optional,span,string_view,type_traits,utility,variant,vector'
+  portability-restrict-system-includes.Includes: '-*,Windows.h,WinSock2.h,WS2tcpip.h,MSWSock.h,WinDNS.h,TlHelp32.h,Shellapi.h,bcrypt.h,d3d11.h,detours.h,dxgi.h,wincodec.h,imgui.h,imgui_impl_dx11.h,imgui_impl_win32.h,intrin.h,algorithm,array,atomic,bit,bitset,cctype,charconv,chrono,climits,cmath,concepts,cstdarg,cstddef,cstdint,cstdio,cstdlib,cstring,cwchar,limits,memory,mutex,new,optional,shared_mutex,span,string_view,type_traits,utility,variant,vector'
 ...

+ 6 - 5
Sunrise/src/client/content/investment/investment_refresh.cpp

@@ -1,17 +1,20 @@
 #include <Windows.h>
 
+#include <mutex>
+
 #include "../../../core/ui/busy/busy.h"
 #include "../../../middleware/content/packages/reader/reader.h"
 #include "../../../state/build_data/runtime.h"
 #include "../../../state/runtime/runtime.h"
 #include "../items/packages/build.h"
+#include "core/threading/srw_lock.h"
 #include "internal.h"
 #include "runtime.h"
 
 namespace sunrise::client::content::investment {
 namespace {
 
-SRWLOCK g_refreshLock{SRWLOCK_INIT};
+core::threading::SrwLock g_refreshLock{};
 
 /**
  * @return True when every persistent mapping domain is fully published.
@@ -47,19 +50,18 @@ bool refresh() noexcept {
     if (ready()) {
         // The same lock as the extraction path. A cache write holds its own lock across file
         // calls, so a held thread stopped inside one would deadlock the freeze below.
-        AcquireSRWLockExclusive(&g_refreshLock);
+        const std::lock_guard lock(g_refreshLock);
         const bool persisted = state::ensure_profile_item_identities()
                                && state::ensure_character_subclasses()
                                && state::build_data::persist();
         // Nothing reads a package again until the next boot, so the open files and the held
         // tables go back now rather than at process exit.
         middleware::content::packages::reader::release_caches();
-        ReleaseSRWLockExclusive(&g_refreshLock);
         core::ui::busy::end(core::ui::busy::Task::contentExtraction);
         return persisted;
     }
 
-    AcquireSRWLockExclusive(&g_refreshLock);
+    const std::lock_guard lock(g_refreshLock);
     // The package pass creates parallel readers. Suspending the client while those threads start
     // can block their DLL thread-attach work behind a suspended owner, so the visible preflight
     // runs one frame early and extraction proceeds with the process live.
@@ -73,7 +75,6 @@ bool refresh() noexcept {
     if (complete) {
         core::ui::busy::end(core::ui::busy::Task::contentExtraction);
     }
-    ReleaseSRWLockExclusive(&g_refreshLock);
     return complete;
 }
 

+ 50 - 47
Sunrise/src/client/content/investment/worker/investment_refresh_worker.cpp

@@ -6,6 +6,7 @@
 #include "../internal.h"
 #include "../runtime.h"
 #include "../worker.h"
+#include "core/threading/data_mutex.h"
 
 namespace sunrise::client::content::investment::worker {
 namespace {
@@ -17,73 +18,75 @@ namespace {
  */
 constexpr std::uint64_t kRefreshIntervalMilliseconds = 0;
 
-SRWLOCK g_lifecycleLock{SRWLOCK_INIT};
-bool g_accepting{};
-bool g_complete{};
-bool g_overlayPending{};
-std::uint64_t g_nextEligible{};
+struct Lifecycle {
+    bool accepting{};
+    bool complete{};
+    bool overlayPending{};
+    std::uint64_t nextEligible{};
+};
+
+core::threading::DataMutex<Lifecycle> g_lifecycle{};
 
 } // namespace
 
 /** Allows cooperative investment refresh slices on the caller-owned game thread. */
 void activate() noexcept {
-    AcquireSRWLockExclusive(&g_lifecycleLock);
-    g_accepting = true;
-    g_complete = false;
-    g_overlayPending = false;
-    g_nextEligible = 0;
-    sunrise::core::ui::busy::end(sunrise::core::ui::busy::Task::contentExtraction);
-    ReleaseSRWLockExclusive(&g_lifecycleLock);
+    g_lifecycle.lock([](Lifecycle& lifecycle) {
+        lifecycle.accepting = true;
+        lifecycle.complete = false;
+        lifecycle.overlayPending = false;
+        lifecycle.nextEligible = 0;
+        sunrise::core::ui::busy::end(sunrise::core::ui::busy::Task::contentExtraction);
+    });
 }
 
 /** Runs one due bounded refresh slice on the caller-owned game thread. */
 void service(std::uint64_t nowMilliseconds) noexcept {
-    AcquireSRWLockExclusive(&g_lifecycleLock);
-    if (!g_accepting || g_complete || !sunrise::client::targets::game::content::is_resolved()
-        || nowMilliseconds < g_nextEligible) {
-        ReleaseSRWLockExclusive(&g_lifecycleLock);
-        return;
-    }
-    g_nextEligible = nowMilliseconds + kRefreshIntervalMilliseconds;
-
-    if (sunrise::client::content::investment::requires_package_sweep()) {
-        g_overlayPending = true;
-        if (sunrise::core::ui::busy::raise_early(
-                sunrise::core::ui::busy::Task::contentExtraction)) {
-            ReleaseSRWLockExclusive(&g_lifecycleLock);
+    g_lifecycle.lock([nowMilliseconds](Lifecycle& lifecycle) {
+        if (!lifecycle.accepting || lifecycle.complete
+            || !sunrise::client::targets::game::content::is_resolved()
+            || nowMilliseconds < lifecycle.nextEligible) {
             return;
         }
-    } else if (g_overlayPending) {
-        // A stale preflight must not leave a task raised after another path publishes the rows.
-        sunrise::core::ui::busy::end(sunrise::core::ui::busy::Task::contentExtraction);
-        g_overlayPending = false;
-    }
+        lifecycle.nextEligible = nowMilliseconds + kRefreshIntervalMilliseconds;
 
-    g_complete = sunrise::client::content::investment::refresh();
-    sunrise::client::content::diagnostics::report_readiness();
-    g_overlayPending = false;
-    ReleaseSRWLockExclusive(&g_lifecycleLock);
+        if (sunrise::client::content::investment::requires_package_sweep()) {
+            lifecycle.overlayPending = true;
+            if (sunrise::core::ui::busy::raise_early(
+                    sunrise::core::ui::busy::Task::contentExtraction)) {
+                return;
+            }
+        } else if (lifecycle.overlayPending) {
+            // A stale preflight must not leave a task raised after another path publishes the rows.
+            sunrise::core::ui::busy::end(sunrise::core::ui::busy::Task::contentExtraction);
+            lifecycle.overlayPending = false;
+        }
+
+        lifecycle.complete = sunrise::client::content::investment::refresh();
+        sunrise::client::content::diagnostics::report_readiness();
+        lifecycle.overlayPending = false;
+    });
 }
 
 /** Stops taking refresh slices and clears the pending overlay. */
 void reset() noexcept {
-    AcquireSRWLockExclusive(&g_lifecycleLock);
-    g_accepting = false;
-    g_complete = false;
-    g_overlayPending = false;
-    g_nextEligible = 0;
-    sunrise::core::ui::busy::end(sunrise::core::ui::busy::Task::contentExtraction);
-    ReleaseSRWLockExclusive(&g_lifecycleLock);
+    g_lifecycle.lock([](Lifecycle& lifecycle) {
+        lifecycle.accepting = false;
+        lifecycle.complete = false;
+        lifecycle.overlayPending = false;
+        lifecycle.nextEligible = 0;
+        sunrise::core::ui::busy::end(sunrise::core::ui::busy::Task::contentExtraction);
+    });
 }
 
 /** Makes the next due pump take another refresh slice even though a prior one completed. */
 void request_slice() noexcept {
-    AcquireSRWLockExclusive(&g_lifecycleLock);
-    if (g_accepting) {
-        g_complete = false;
-        g_nextEligible = 0;
-    }
-    ReleaseSRWLockExclusive(&g_lifecycleLock);
+    g_lifecycle.lock([](Lifecycle& lifecycle) {
+        if (lifecycle.accepting) {
+            lifecycle.complete = false;
+            lifecycle.nextEligible = 0;
+        }
+    });
 }
 
 } // namespace sunrise::client::content::investment::worker

+ 4 - 3
Sunrise/src/client/hooks/assert_handler/assert_handler_observer.cpp

@@ -7,10 +7,12 @@
 #include <cstdint>
 #include <cstdio>
 #include <cstring>
+#include <mutex>
 
 #include "../../../core/logging/log.h"
 #include "../../targets/game/assert_handler.h"
 #include "../net_tick_probe/net_tick_probe.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::client::hooks::assert_handler {
 namespace {
@@ -35,7 +37,7 @@ constexpr int kGraphicsHaltCategory = 6;
 /** The handler the game installed, called with the same printf-style arguments the sites use. */
 using NativeHandler = void(__cdecl*)(int, const char*, ...);
 
-SRWLOCK g_lock{SRWLOCK_INIT};
+core::threading::SrwLock g_lock{};
 /** Last message seen, so a message that repeats every frame is counted rather than written. */
 std::array<char, kTextCapacity> g_lastText{};
 std::uint32_t g_repeats{};
@@ -54,7 +56,7 @@ std::uint32_t g_seen{};
  * @return True when the caller writes a log line.
  */
 [[nodiscard]] bool admit(const char* text, std::uint32_t& seen, std::uint32_t& repeats) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     ++g_seen;
     if (std::strcmp(g_lastText.data(), text) == 0) {
         ++g_repeats;
@@ -67,7 +69,6 @@ std::uint32_t g_seen{};
     }
     seen = g_seen;
     repeats = g_repeats;
-    ReleaseSRWLockExclusive(&g_lock);
     return repeats <= kRepeatHead || repeats % kRepeatStride == 0;
 }
 

+ 0 - 3
Sunrise/src/client/hooks/assert_handler/assert_handler_observer.h

@@ -4,9 +4,6 @@
 
 namespace sunrise::client::hooks::assert_handler {
 
-extern SRWLOCK g_lock;
-extern bool g_installed;
-
 /** @return Address of the internal assert handler body. */
 [[nodiscard]] void* handler_entry_point() noexcept;
 

+ 0 - 1
Sunrise/src/client/hooks/egress/internal.h

@@ -47,7 +47,6 @@ enum class HookSlot : std::size_t {
 /** Fixed handle count covers every required and OS-optional egress entry point. */
 inline constexpr std::size_t kHookCount = static_cast<std::size_t>(HookSlot::count);
 
-extern SRWLOCK g_lock;
 extern std::array<hooking::detour::Handle, kHookCount> g_handles;
 
 /**

+ 6 - 12
Sunrise/src/client/hooks/egress/lifecycle/egress_guard_lifecycle.cpp

@@ -1,16 +1,17 @@
 #include <algorithm>
 #include <array>
 #include <cstdio>
+#include <shared_mutex>
 
 #include "../../../../core/logging/log.h"
 #include "../internal.h"
-#include "../platform/abi.h"
 #include "../runtime.h"
+#include "core/threading/srw_lock.h"
 #include "internal.h"
 
 namespace sunrise::client::hooks::egress {
 
-SRWLOCK g_lock{SRWLOCK_INIT};
+core::threading::SrwLock g_lock{};
 std::array<hooking::detour::Handle, kHookCount> g_handles{};
 
 namespace {
@@ -64,13 +65,11 @@ std::size_t g_activeHookCount{};
 
 /** Installs every resolver and socket guard in one process-wide transaction. */
 bool install() noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     if (all_installed()) {
-        ReleaseSRWLockExclusive(&g_lock);
         return true;
     }
     if (any_installed() || !pin_owner_module() || !lifecycle::load_modules()) {
-        ReleaseSRWLockExclusive(&g_lock);
         return false;
     }
 
@@ -82,20 +81,17 @@ bool install() noexcept {
         g_activeHookCount = 0;
         g_batchAttached = false;
         lifecycle::release_modules();
-        ReleaseSRWLockExclusive(&g_lock);
         return false;
     }
     g_activeHookCount = count;
     g_batchAttached = true;
-    ReleaseSRWLockExclusive(&g_lock);
     return true;
 }
 
 /** Emits one line per guarded export, then the batch outcome. */
 void report_installation() noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     if (g_reported) {
-        ReleaseSRWLockExclusive(&g_lock);
         return;
     }
     g_reported = true;
@@ -118,7 +114,6 @@ void report_installation() noexcept {
                              {line.data(), static_cast<std::size_t>(written)});
         }
     }
-    ReleaseSRWLockExclusive(&g_lock);
     std::array<char, 96> summary{};
     const int written = std::snprintf(summary.data(),
                                       summary.size(),
@@ -134,9 +129,8 @@ void report_installation() noexcept {
 
 /** @return True only when every required guard detour is attached. */
 bool is_installed() noexcept {
-    AcquireSRWLockShared(&g_lock);
+    const std::shared_lock lock(g_lock);
     const bool installed = all_installed();
-    ReleaseSRWLockShared(&g_lock);
     return installed;
 }
 

+ 7 - 18
Sunrise/src/client/hooks/graphics/input/graphics_window_input.cpp

@@ -2,11 +2,13 @@
 
 #include <atomic>
 #include <bit>
+#include <shared_mutex>
 
 #include "../../../../core/ui/layout/credits/sunrise_credits_badge.h"
 #include "../../../../core/ui/modules/logs/logs.h"
 #include "../../../../core/ui/runtime/ui_visibility_runtime.h"
 #include "../renderer/renderer.h"
+#include "core/threading/srw_lock.h"
 #include "input.h"
 
 namespace sunrise::client::hooks::graphics::input {
@@ -24,7 +26,7 @@ struct Binding {
 
 Binding g_binding{};
 std::atomic_uint g_activeCallbacks{};
-SRWLOCK g_inputLock{SRWLOCK_INIT};
+core::threading::SrwLock g_inputLock{};
 
 /** Counts active procedure calls so teardown can be retried before module unload. */
 class CallbackGuard final {
@@ -48,9 +50,8 @@ public:
  * @return Original procedure for the matching record, or null when nothing matches.
  */
 [[nodiscard]] WNDPROC original_for(HWND window) noexcept {
-    AcquireSRWLockShared(&g_inputLock);
+    const std::shared_lock lock(g_inputLock);
     const WNDPROC original = g_binding.window == window ? g_binding.original : nullptr;
-    ReleaseSRWLockShared(&g_inputLock);
     return original;
 }
 
@@ -122,15 +123,13 @@ bool install(HWND window) noexcept {
     if (window == nullptr || IsWindow(window) == FALSE) {
         return false;
     }
-    AcquireSRWLockExclusive(&g_inputLock);
+    const std::lock_guard lock(g_inputLock);
     if (g_binding.installed) {
         const bool sameWindow = g_binding.window == window;
-        ReleaseSRWLockExclusive(&g_inputLock);
         return sameWindow;
     }
     if (g_activeCallbacks.load(std::memory_order_acquire) != 0) {
         // A retired procedure keeps its forwarding record until every old call returns.
-        ReleaseSRWLockExclusive(&g_inputLock);
         return false;
     }
 
@@ -138,26 +137,22 @@ bool install(HWND window) noexcept {
     const LONG_PTR original =
         SetWindowLongPtrW(window, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&window_procedure));
     if (original == 0 && GetLastError() != ERROR_SUCCESS) {
-        ReleaseSRWLockExclusive(&g_inputLock);
         return false;
     }
     g_binding = Binding{window, std::bit_cast<WNDPROC>(original), true};
-    ReleaseSRWLockExclusive(&g_inputLock);
     return true;
 }
 
 /** Restores the original procedure only when Sunrise still owns the chain head. */
 bool uninstall() noexcept {
-    AcquireSRWLockExclusive(&g_inputLock);
+    const std::lock_guard lock(g_inputLock);
     if (!g_binding.installed) {
         const bool idle = g_activeCallbacks.load(std::memory_order_acquire) == 0;
-        ReleaseSRWLockExclusive(&g_inputLock);
         return idle;
     }
     if (IsWindow(g_binding.window) == FALSE) {
         g_binding.installed = false;
         const bool idle = g_activeCallbacks.load(std::memory_order_acquire) == 0;
-        ReleaseSRWLockExclusive(&g_inputLock);
         return idle;
     }
 
@@ -165,19 +160,16 @@ bool uninstall() noexcept {
     const LONG_PTR current = GetWindowLongPtrW(g_binding.window, GWLP_WNDPROC);
     const LONG_PTR replacement = reinterpret_cast<LONG_PTR>(&window_procedure);
     if (current == 0 && GetLastError() != ERROR_SUCCESS) {
-        ReleaseSRWLockExclusive(&g_inputLock);
         return false;
     }
     if (current == reinterpret_cast<LONG_PTR>(g_binding.original)) {
         // The window owner already put our forwarding target back itself.
         g_binding.installed = false;
         const bool idle = g_activeCallbacks.load(std::memory_order_acquire) == 0;
-        ReleaseSRWLockExclusive(&g_inputLock);
         return idle;
     }
     if (current != replacement) {
         // A later subclass owns the chain head now, so do not overwrite it.
-        ReleaseSRWLockExclusive(&g_inputLock);
         return false;
     }
 
@@ -185,19 +177,17 @@ bool uninstall() noexcept {
     const LONG_PTR replaced = SetWindowLongPtrW(
         g_binding.window, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_binding.original));
     if (replaced == 0 && GetLastError() != ERROR_SUCCESS) {
-        ReleaseSRWLockExclusive(&g_inputLock);
         return false;
     }
     // Keep the forwarding target until a later install replaces this retired record.
     g_binding.installed = false;
     const bool idle = g_activeCallbacks.load(std::memory_order_acquire) == 0;
-    ReleaseSRWLockExclusive(&g_inputLock);
     return idle;
 }
 
 /** Checks whether Sunrise is still installed, or still sits below a later subclass. */
 bool active(HWND window) noexcept {
-    AcquireSRWLockShared(&g_inputLock);
+    const std::shared_lock lock(g_inputLock);
     bool installed = g_binding.installed && g_binding.window == window && IsWindow(window) != FALSE
                      && IsWindowVisible(window) != FALSE;
     if (installed) {
@@ -209,7 +199,6 @@ bool active(HWND window) noexcept {
             installed = current != reinterpret_cast<LONG_PTR>(g_binding.original);
         }
     }
-    ReleaseSRWLockShared(&g_inputLock);
     return installed;
 }
 

+ 9 - 10
Sunrise/src/core/logging/log.cpp

@@ -7,8 +7,11 @@
 #include <atomic>
 #include <cstdio>
 #include <cstring>
+#include <mutex>
+#include <shared_mutex>
 
 #include "../filesystem/path.h"
+#include "core/threading/srw_lock.h"
 #include "snapshot/internal.h"
 
 namespace sunrise::core::log {
@@ -42,7 +45,7 @@ constexpr std::size_t kEventTextCapacity =
 constexpr std::size_t kStampCapacity = 32;
 
 struct LogState {
-    SRWLOCK lock{SRWLOCK_INIT};
+    threading::SrwLock lock{};
     std::array<std::atomic<Level>, static_cast<std::size_t>(Channel::count)> levels{};
     HANDLE file{INVALID_HANDLE_VALUE};
     /** Tick the sinks opened on. Every line carries its offset from this, so stalls are visible. */
@@ -135,7 +138,7 @@ Settings defaults() noexcept {
 
 /** Applies log thresholds and opens the optional file sink. */
 bool initialize(void* module, const Settings& settings) noexcept {
-    AcquireSRWLockExclusive(&g_log.lock);
+    const std::lock_guard lock(g_log.lock);
     // Resetting under the lifetime lock prevents an admitted writer from repopulating stale view.
     snapshot::internal::reset();
     if (g_log.file != INVALID_HANDLE_VALUE) {
@@ -159,13 +162,13 @@ bool initialize(void* module, const Settings& settings) noexcept {
             level.store(Level::off, std::memory_order_relaxed);
         }
     }
-    ReleaseSRWLockExclusive(&g_log.lock);
     return ready;
 }
 
 /** Closes the optional sink and clears the bounded in-memory view. */
 void shutdown() noexcept {
-    AcquireSRWLockExclusive(&g_log.lock);
+    const std::lock_guard lock(g_log.lock);
+
     g_log.initialized = false;
     for (std::atomic<Level>& level : g_log.levels) {
         level.store(Level::off, std::memory_order_relaxed);
@@ -177,7 +180,6 @@ void shutdown() noexcept {
     }
     // The same lifetime lock excludes writers until both sinks and retained entries are empty.
     snapshot::internal::reset();
-    ReleaseSRWLockExclusive(&g_log.lock);
 }
 
 /** Writes one line straight to the debugger, bypassing the sinks and every threshold. */
@@ -194,9 +196,8 @@ void early(std::string_view event) noexcept {
 
 /** Reports whether an event would be emitted, so callers can skip the cost of building one. */
 bool accepts(Channel channel, Level level) noexcept {
-    AcquireSRWLockShared(&g_log.lock);
+    const std::shared_lock lock(g_log.lock);
     const bool admitted = g_log.initialized && enabled(channel, level);
-    ReleaseSRWLockShared(&g_log.lock);
     return admitted;
 }
 
@@ -208,9 +209,8 @@ void write(Channel channel, Level level, std::string_view event) noexcept {
         return;
     }
 
-    AcquireSRWLockShared(&g_log.lock);
+    const std::shared_lock lock(g_log.lock);
     if (!g_log.initialized || !enabled(channel, level)) {
-        ReleaseSRWLockShared(&g_log.lock);
         return;
     }
 
@@ -248,7 +248,6 @@ void write(Channel channel, Level level, std::string_view event) noexcept {
     g_writers.fetch_sub(1, std::memory_order_acq_rel);
     // Record after sink writes while the shared lifetime lock still excludes shutdown reset.
     snapshot::internal::record(channel, level, std::string_view(line.data(), snapshotLength));
-    ReleaseSRWLockShared(&g_log.lock);
 }
 
 /** Formats and emits one debug event carrying a duration in the ms field. */

+ 34 - 34
Sunrise/src/core/logging/snapshot/log_snapshot_ring.cpp

@@ -4,6 +4,7 @@
 #include <array>
 #include <limits>
 
+#include "core/threading/data_mutex.h"
 #include "internal.h"
 
 namespace sunrise::core::log::snapshot {
@@ -13,14 +14,13 @@ namespace {
 constexpr std::size_t kTextTerminatorBytes = 1;
 
 struct RingState {
-    SRWLOCK lock{SRWLOCK_INIT};
     std::array<Entry, kEntryCapacity> entries{};
     std::size_t nextIndex{};
     std::size_t count{};
     std::uint64_t overwrittenCount{};
 };
 
-RingState g_ring;
+threading::SharedDataMutex<RingState> g_ring;
 
 /** @param channel Value to inspect. @return True for a defined log channel. */
 [[nodiscard]] bool valid_channel(Channel channel) noexcept {
@@ -62,15 +62,15 @@ std::uint64_t Snapshot::overwritten_count() const noexcept {
 /** @return A value-owned chronological copy of all retained events. */
 Snapshot take() noexcept {
     Snapshot result;
-    AcquireSRWLockShared(&g_ring.lock);
-    result.count_ = g_ring.count;
-    result.overwrittenCount_ = g_ring.overwrittenCount;
-    const std::size_t firstIndex =
-        (g_ring.nextIndex + kEntryCapacity - g_ring.count) % kEntryCapacity;
-    for (std::size_t index = 0; index < g_ring.count; ++index) {
-        result.entries_[index] = g_ring.entries[(firstIndex + index) % kEntryCapacity];
-    }
-    ReleaseSRWLockShared(&g_ring.lock);
+    g_ring.lock_read([&result](const RingState& ring) {
+        result.count_ = ring.count;
+        result.overwrittenCount_ = ring.overwrittenCount;
+        const std::size_t firstIndex =
+            (ring.nextIndex + kEntryCapacity - ring.count) % kEntryCapacity;
+        for (std::size_t index = 0; index < ring.count; ++index) {
+            result.entries_[index] = ring.entries[(firstIndex + index) % kEntryCapacity];
+        }
+    });
     return result;
 }
 
@@ -78,12 +78,12 @@ namespace internal {
 
 /** Clears retained events before a new logger lifecycle starts. */
 void reset() noexcept {
-    AcquireSRWLockExclusive(&g_ring.lock);
-    g_ring.entries = {};
-    g_ring.nextIndex = 0;
-    g_ring.count = 0;
-    g_ring.overwrittenCount = 0;
-    ReleaseSRWLockExclusive(&g_ring.lock);
+    g_ring.lock_write([](RingState& ring) {
+        ring.entries = {};
+        ring.nextIndex = 0;
+        ring.count = 0;
+        ring.overwrittenCount = 0;
+    });
 }
 
 /**
@@ -97,23 +97,23 @@ void record(Channel channel, Level level, std::string_view text) noexcept {
         return;
     }
 
-    AcquireSRWLockExclusive(&g_ring.lock);
-    Entry& entry = g_ring.entries[g_ring.nextIndex];
-    entry = {};
-    entry.channel_ = channel;
-    entry.level_ = level;
-    const std::size_t maximumText = entry.text_.size() - kTextTerminatorBytes;
-    entry.textLength_ = (std::min)(text.size(), maximumText);
-    if (entry.textLength_ != 0) {
-        std::copy_n(text.data(), entry.textLength_, entry.text_.data());
-    }
-    g_ring.nextIndex = (g_ring.nextIndex + 1) % kEntryCapacity;
-    if (g_ring.count < kEntryCapacity) {
-        ++g_ring.count;
-    } else if (g_ring.overwrittenCount != (std::numeric_limits<std::uint64_t>::max)()) {
-        ++g_ring.overwrittenCount;
-    }
-    ReleaseSRWLockExclusive(&g_ring.lock);
+    g_ring.lock_write([channel, level, text](RingState& ring) {
+        Entry& entry = ring.entries[ring.nextIndex];
+        entry = {};
+        entry.channel_ = channel;
+        entry.level_ = level;
+        const std::size_t maximumText = entry.text_.size() - kTextTerminatorBytes;
+        entry.textLength_ = (std::min)(text.size(), maximumText);
+        if (entry.textLength_ != 0) {
+            std::copy_n(text.data(), entry.textLength_, entry.text_.data());
+        }
+        ring.nextIndex = (ring.nextIndex + 1) % kEntryCapacity;
+        if (ring.count < kEntryCapacity) {
+            ++ring.count;
+        } else if (ring.overwrittenCount != (std::numeric_limits<std::uint64_t>::max)()) {
+            ++ring.overwrittenCount;
+        }
+    });
 }
 
 } // namespace internal

+ 5 - 10
Sunrise/src/core/runtime/core_runtime.cpp

@@ -7,6 +7,7 @@
 #include <atomic>
 #include <cstdint>
 #include <cstdio>
+#include <mutex>
 #include <string_view>
 
 #include "../../client/runtime/host/game_host_classification.h"
@@ -26,12 +27,13 @@
 #include "../ui/modules/logs/logs.h"
 #include "../ui/modules/registry/ui_module_registry.h"
 #include "../ui/runtime/ui_visibility_runtime.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::core {
 namespace {
 
 std::atomic_bool g_initialized{false};
-SRWLOCK g_runtimeLock{SRWLOCK_INIT};
+threading::SrwLock g_runtimeLock{};
 
 /** The installed public package headers live beside the game executable. */
 constexpr std::wstring_view kInstalledPackagesDirectory = L"packages";
@@ -136,9 +138,8 @@ void report_stage_failure(const char* stage) noexcept {
 
 /** Initializes every runtime layer in dependency order. */
 bool initialize(void* module) noexcept {
-    AcquireSRWLockExclusive(&g_runtimeLock);
+    const std::lock_guard lock(g_runtimeLock);
     if (g_initialized.load(std::memory_order_relaxed)) {
-        ReleaseSRWLockExclusive(&g_runtimeLock);
         return true;
     }
     // Taken before the first stage, so the reported duration covers settings and the sinks too.
@@ -146,7 +147,6 @@ bool initialize(void* module) noexcept {
 
     if (!settings::initialize(module)) {
         // Settings name their own failure; the sinks do not exist yet to carry a second line.
-        ReleaseSRWLockExclusive(&g_runtimeLock);
         return false;
     }
     state::unlocks::publish(settings::get().initialUnlocks);
@@ -200,26 +200,22 @@ bool initialize(void* module) noexcept {
         state::unlocks::clear();
         log::shutdown();
         settings::shutdown();
-        ReleaseSRWLockExclusive(&g_runtimeLock);
         return false;
     }
     g_initialized.store(true, std::memory_order_release);
     log::write(log::Channel::core, log::Level::info, "ev=initialize result=ok");
     log::write_elapsed(log::Channel::core, "ev=initialize phase=complete", startedTick, "ok");
-    ReleaseSRWLockExclusive(&g_runtimeLock);
     return true;
 }
 
 /** Stops every runtime layer in reverse dependency order. */
 bool shutdown() noexcept {
-    AcquireSRWLockExclusive(&g_runtimeLock);
+    const std::lock_guard lock(g_runtimeLock);
     if (!g_initialized.load(std::memory_order_acquire)) {
-        ReleaseSRWLockExclusive(&g_runtimeLock);
         return true;
     }
     if (!client::shutdown()) {
         // Server and State must remain valid while any Client hook is attached.
-        ReleaseSRWLockExclusive(&g_runtimeLock);
         return false;
     }
     g_initialized.store(false, std::memory_order_release);
@@ -237,7 +233,6 @@ bool shutdown() noexcept {
     state::unlocks::clear();
     log::shutdown();
     settings::shutdown();
-    ReleaseSRWLockExclusive(&g_runtimeLock);
     return true;
 }
 

+ 81 - 0
Sunrise/src/core/threading/data_mutex.h

@@ -0,0 +1,81 @@
+#pragma once
+
+#include <concepts>
+#include <mutex>
+#include <shared_mutex>
+#include <utility>
+
+#include "sendable.h"
+#include "srw_lock.h"
+
+namespace sunrise::core::threading {
+
+/** A combination of Mutex + Data. This allows Data types to be written as if they're single
+ * threaded as you'll only have access when the mutex is locked. */
+template <typename Data, typename Mutex = SrwLock> class DataMutex {
+public:
+    explicit DataMutex() noexcept
+        requires std::default_initializable<Data>
+    = default;
+
+    template <typename... Args>
+        requires std::constructible_from<Data, Args...>
+    explicit DataMutex(std::in_place_t, Args&&... args) : data_(std::forward<Args>(args)...) {}
+
+    /** Locks the mutex and calls the given Func */
+    template <std::invocable<Data&> Func, Sendable Return = std::invoke_result_t<Func, Data&>>
+    [[nodiscard]] Return lock(Func&& func) noexcept {
+        const std::lock_guard lock(mutex_);
+        return std::invoke(std::forward<Func>(func), data_);
+    }
+
+    /** Tries to loc the mutex, only calls the given Func if successful */
+    template <std::invocable<Data&> Func> void try_lock(Func&& func) noexcept {
+        std::unique_lock lock(mutex_, std::try_to_lock);
+
+        if (lock.owns_lock()) {
+            std::invoke(std::forward<Func>(func), data_);
+        }
+    }
+
+private:
+    mutable Mutex mutex_;
+    Data data_;
+};
+
+/** Similar to the above but also allows for multple readers. Readers are passed a const Data&,
+ * making accidental writes impossible */
+template <typename Data, typename SharedMutex = SrwLock> class SharedDataMutex {
+public:
+    explicit SharedDataMutex() noexcept
+        requires std::default_initializable<Data>
+    = default;
+
+    template <typename... Args>
+        requires std::constructible_from<Data, Args...>
+    explicit SharedDataMutex(std::in_place_t, Args&&... args)
+        : data_(std::forward<Args>(args)...) {}
+
+    /** Locks the mutex for reading and calls the given Func. Multiple readers can be active at
+     * once */
+    template <std::invocable<const Data&> Func,
+              Sendable Return = std::invoke_result_t<Func, const Data&>>
+    [[nodiscard]] Return lock_read(Func&& func) const noexcept {
+        const std::shared_lock lock(mutex_);
+        return std::invoke(std::forward<Func>(func), data_);
+    }
+
+    /** Locks the mutex for writing and calls the given Func. This is an exclusive lock and
+     * guarantees there are no other readers or writers */
+    template <std::invocable<Data&> Func, Sendable Return = std::invoke_result_t<Func, Data&>>
+    [[nodiscard]] Return lock_write(Func&& func) noexcept {
+        const std::lock_guard lock(mutex_);
+        return std::invoke(std::forward<Func>(func), data_);
+    }
+
+private:
+    mutable SharedMutex mutex_{};
+    Data data_{};
+};
+
+} // namespace sunrise::core::threading

+ 17 - 0
Sunrise/src/core/threading/sendable.h

@@ -0,0 +1,17 @@
+#pragma once
+
+#include <concepts>
+
+namespace sunrise::core::threading {
+
+/** An specializable struct that indicates a type can be sent across thread boundaries */
+template <typename T> struct IsSendable : std::false_type {};
+
+/** Indicates a specific type can be sent across thread boundaries. Integral, loating point, and
+ * void types are always allowed since they're easily copyable. Custom types can be marked as
+ * `Sendable` by specializing `IsSendable` above */
+template <typename T>
+concept Sendable =
+    std::integral<T> || std::floating_point<T> || std::is_void_v<T> || IsSendable<T>::value;
+
+} // namespace sunrise::core::threading

+ 47 - 0
Sunrise/src/core/threading/srw_lock.h

@@ -0,0 +1,47 @@
+#pragma once
+
+#include <WinSock2.h>
+
+namespace sunrise::core::threading {
+
+/** Wrapper to enable std::lock_guard and std::shared_lock for SRWLOCK */
+class SrwLock final {
+public:
+    constexpr explicit SrwLock() noexcept = default;
+
+    SrwLock(const SrwLock&) = delete;
+    SrwLock(SrwLock&&) = delete;
+    SrwLock& operator=(const SrwLock&) = delete;
+    SrwLock& operator=(SrwLock&&) = delete;
+
+    // stl Lockable
+    void lock() noexcept {
+        AcquireSRWLockExclusive(&lock_);
+    }
+
+    [[nodiscard]] bool try_lock() noexcept {
+        return TryAcquireSRWLockExclusive(&lock_);
+    }
+
+    void unlock() noexcept {
+        ReleaseSRWLockExclusive(&lock_);
+    }
+
+    // stl SharedLockable
+    void lock_shared() noexcept {
+        AcquireSRWLockShared(&lock_);
+    }
+
+    [[nodiscard]] bool try_lock_shared() noexcept {
+        return TryAcquireSRWLockShared(&lock_);
+    }
+
+    void unlock_shared() noexcept {
+        ReleaseSRWLockShared(&lock_);
+    }
+
+private:
+    SRWLOCK lock_{SRWLOCK_INIT};
+};
+
+} // namespace sunrise::core::threading

+ 16 - 12
Sunrise/src/middleware/content/packages/reader/package_handle_cache.cpp

@@ -4,6 +4,7 @@
 #include <cwchar>
 #include <limits>
 
+#include "core/threading/data_mutex.h"
 #include "handle_cache.h"
 
 namespace sunrise::middleware::content::packages::reader::handle_cache {
@@ -16,9 +17,12 @@ constexpr std::uint64_t kHashBasis = 14695981039346656037ULL;
 /** Standard 64-bit FNV-1a prime mixes each path character. */
 constexpr std::uint64_t kHashPrime = 1099511628211ULL;
 
-SRWLOCK g_lock{SRWLOCK_INIT};
-std::array<FileSlot, kSharedSlots> g_slots{};
-std::uint64_t g_useCounter{};
+struct CacheState {
+    std::array<FileSlot, kSharedSlots> slots{};
+    std::uint64_t useCounter{};
+};
+
+core::threading::DataMutex<CacheState> g_cache{};
 
 /** @param path Full package path. @return Its key. */
 [[nodiscard]] std::uint64_t path_hash(const Path& path) noexcept {
@@ -123,11 +127,11 @@ bool read(const Path& path, std::uint64_t offset, std::span<std::byte> output) n
     }
     // One lock covers the lookup and the read. The read is positioned on a file the next
     // caller may replace.
-    AcquireSRWLockExclusive(&g_lock);
-    const HANDLE file = acquire(g_slots, g_useCounter, path);
-    const bool complete = file != nullptr && read_positioned(file, offset, output);
-    ReleaseSRWLockExclusive(&g_lock);
-    return complete;
+    return g_cache.lock([&path, offset, output](CacheState& cache) {
+        const HANDLE file = acquire(cache.slots, cache.useCounter, path);
+        const bool complete = file != nullptr && read_positioned(file, offset, output);
+        return complete;
+    });
 }
 
 /** Reads an exact byte range through the files one reader keeps open. */
@@ -145,10 +149,10 @@ bool read(Scratch& scratch,
 
 /** Closes the shared files, which the build passes do when they finish. */
 void release() noexcept {
-    AcquireSRWLockExclusive(&g_lock);
-    close_slots(g_slots);
-    g_useCounter = 0;
-    ReleaseSRWLockExclusive(&g_lock);
+    g_cache.lock([](CacheState& cache) {
+        close_slots(cache.slots);
+        cache.useCounter = 0;
+    });
 }
 
 /** @param scratch Reader whose own files are closed. */

+ 38 - 71
Sunrise/src/server/bap/bap_route.cpp

@@ -4,6 +4,8 @@
 #include <atomic>
 #include <cstdio>
 #include <limits>
+#include <mutex>
+#include <shared_mutex>
 #include <string_view>
 
 #include "../../core/logging/log.h"
@@ -15,6 +17,7 @@
 #include "activity_authority_query_owner.h"
 #include "activity_authority_reset_owner.h"
 #include "activity_mission_seed_lease.h"
+#include "core/threading/srw_lock.h"
 #include "encrypted/bap_connection_publication.h"
 #include "encrypted/push/activity/internal.h"
 #include "internal.h"
@@ -28,7 +31,7 @@ namespace layouts = state::build_data::scenarios;
 namespace roster_message = middleware::bap::activity_message::sensor_auth_update;
 namespace tables = middleware::content::packages::tables;
 
-SRWLOCK g_lock{SRWLOCK_INIT};
+core::threading::SrwLock g_lock{};
 std::array<Session, kSessionCount> g_sessions{};
 Scratch g_scratch{};
 std::uint64_t g_accountGeneration{};
@@ -847,7 +850,7 @@ std::size_t activity_link_count_locked(const state::activity::SessionBinding& bi
 bool consume(const client::network::BapRequest& request,
              client::network::BapResponse& response) noexcept {
     response = {};
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     bool success = false;
     // Polls report whether they reached scratch.
     bool touchesScratch = request.event != client::network::BapEvent::poll;
@@ -870,15 +873,13 @@ bool consume(const client::network::BapRequest& request,
     if (touchesScratch) {
         SecureZeroMemory(&g_scratch, sizeof g_scratch);
     }
-    ReleaseSRWLockExclusive(&g_lock);
     return success;
 }
 
 /** Counts authenticated BAP links that currently own one exact activity generation. */
 std::size_t activity_link_count(const state::activity::SessionBinding& binding) noexcept {
-    AcquireSRWLockShared(&g_lock);
+    const std::shared_lock lock(g_lock);
     const std::size_t count = activity_link_count_locked(binding);
-    ReleaseSRWLockShared(&g_lock);
     return count;
 }
 
@@ -886,7 +887,7 @@ std::size_t activity_link_count(const state::activity::SessionBinding& binding)
 bool activity_link_view(const state::activity::SessionBinding& binding,
                         ActivityLinkView& output) noexcept {
     output = {};
-    AcquireSRWLockShared(&g_lock);
+    const std::shared_lock lock(g_lock);
     const Session* const session = unique_activity_link_locked(binding, output.matchingLinks);
     if (session != nullptr) {
         const auto region = selected_region_locked(*session);
@@ -902,7 +903,6 @@ bool activity_link_view(const state::activity::SessionBinding& binding,
         output.rosterReason = session->activityRosterReason;
         output.playerKey = encrypted::push::activity::published_player_key(*session);
     }
-    ReleaseSRWLockShared(&g_lock);
     return session != nullptr;
 }
 
@@ -911,7 +911,7 @@ ActivityMissionSeedLeaseStatus
 activity_mission_seed_available(const state::activity::SessionBinding& binding,
                                 std::uint32_t scenarioRow,
                                 std::uint64_t expectedGeneration) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     Session* session = nullptr;
     std::size_t matchingLinks = 0;
     ActivityMissionSeedLeaseStatus status =
@@ -919,7 +919,6 @@ activity_mission_seed_available(const state::activity::SessionBinding& binding,
     if (status == ActivityMissionSeedLeaseStatus::ready && session->activityRosterStaged.staged) {
         status = ActivityMissionSeedLeaseStatus::outputBusy;
     }
-    ReleaseSRWLockExclusive(&g_lock);
     return status;
 }
 
@@ -930,7 +929,7 @@ activity_mission_seed_lease(const state::activity::SessionBinding& binding,
                             std::uint64_t expectedGeneration,
                             ActivityMissionSeedLeaseView& output) noexcept {
     output = {};
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     Session* session = nullptr;
     ActivityMissionSeedLeaseStatus status = mission_seed_link_locked(
         binding, scenarioRow, expectedGeneration, session, output.matchingLinks);
@@ -940,7 +939,6 @@ activity_mission_seed_lease(const state::activity::SessionBinding& binding,
     if (status == ActivityMissionSeedLeaseStatus::ready) {
         read_mission_seed_lease(*session, output.matchingLinks, output);
     }
-    ReleaseSRWLockExclusive(&g_lock);
     return status;
 }
 
@@ -949,7 +947,7 @@ ActivityMissionSeedLeaseStatus
 select_activity_mission_seed(const state::activity::SessionBinding& binding,
                              const ActivityMissionSeedPlan& plan,
                              std::uint64_t expectedGeneration) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     Session* session = nullptr;
     std::size_t matchingLinks = 0;
     ActivityMissionSeedLeaseStatus status = mission_seed_link_locked(
@@ -963,7 +961,6 @@ select_activity_mission_seed(const state::activity::SessionBinding& binding,
         if (lease.configured && same_mission_seed_plan(lease.plan, plan)) {
             // The script may select the plan the roster adopted by default. That is a selection.
             lease.scriptSelected = true;
-            ReleaseSRWLockExclusive(&g_lock);
             return ActivityMissionSeedLeaseStatus::ready;
         }
         if (lease.configured && lease.revision == (std::numeric_limits<std::uint64_t>::max)()) {
@@ -983,7 +980,6 @@ select_activity_mission_seed(const state::activity::SessionBinding& binding,
             }
             if (!regionKnown) {
                 if (lease.registeredRegionCount >= lease.registeredRegions.size()) {
-                    ReleaseSRWLockExclusive(&g_lock);
                     return ActivityMissionSeedLeaseStatus::refused;
                 }
                 lease.registeredRegions[lease.registeredRegionCount++] = plan.effectiveRegion;
@@ -1002,7 +998,6 @@ select_activity_mission_seed(const state::activity::SessionBinding& binding,
             lease.scriptSelected = true;
         }
     }
-    ReleaseSRWLockExclusive(&g_lock);
     return status;
 }
 
@@ -1011,13 +1006,12 @@ bool activity_type23_override_available(const state::activity::SessionBinding& b
                                         const activity::host::ScriptableTarget& target,
                                         std::int32_t expectedRegion,
                                         std::uint64_t expectedGeneration) noexcept {
-    AcquireSRWLockShared(&g_lock);
+    const std::shared_lock lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const bool available =
         session != nullptr
         && canonical_type23_available_locked(*session, target, expectedRegion, expectedGeneration);
-    ReleaseSRWLockShared(&g_lock);
     return available;
 }
 
@@ -1027,7 +1021,7 @@ bool current_activity_link_view(std::int32_t localSliceSet,
     output = {};
     const Session* only = nullptr;
     const Session* matched = nullptr;
-    AcquireSRWLockShared(&g_lock);
+    const std::shared_lock lock(g_lock);
     for (const Session& session : g_sessions) {
         if (session.id == 0 || !session.authenticated
             || session.activity.role == ActivityClientRole::none
@@ -1053,7 +1047,6 @@ bool current_activity_link_view(std::int32_t localSliceSet,
         output.effectiveRegion = selected_region_locked(*selected).index;
         output.publicTarget = selected->activity.role == ActivityClientRole::publicTarget;
     }
-    ReleaseSRWLockShared(&g_lock);
     return selected != nullptr;
 }
 
@@ -1061,7 +1054,7 @@ bool current_activity_link_view(std::int32_t localSliceSet,
 bool activity_replication_view(const state::activity::SessionBinding& binding,
                                ActivityReplicationView& output) noexcept {
     output = {};
-    AcquireSRWLockShared(&g_lock);
+    const std::shared_lock lock(g_lock);
     std::size_t count = 0;
     const Session* const session = unique_activity_link_locked(binding, count);
     const bool ready =
@@ -1075,7 +1068,6 @@ bool activity_replication_view(const state::activity::SessionBinding& binding,
         output.memberId = session->activityMemberKey;
         output.replicationEpoch = session->activity.replicationEpoch;
     }
-    ReleaseSRWLockShared(&g_lock);
     return ready;
 }
 
@@ -1086,7 +1078,7 @@ bool activity_replication_view_for_session(std::uint64_t activitySessionId,
     if (activitySessionId == 0) {
         return false;
     }
-    AcquireSRWLockShared(&g_lock);
+    const std::shared_lock lock(g_lock);
     const Session* selected = nullptr;
     std::size_t count = 0;
     for (const Session& session : g_sessions) {
@@ -1110,7 +1102,6 @@ bool activity_replication_view_for_session(std::uint64_t activitySessionId,
         output.memberId = selected->activityMemberKey;
         output.replicationEpoch = selected->activity.replicationEpoch;
     }
-    ReleaseSRWLockShared(&g_lock);
     return count == 1;
 }
 
@@ -1121,7 +1112,7 @@ bool activity_replication_view_for_group(std::uint64_t groupSessionId,
     if (groupSessionId == 0) {
         return false;
     }
-    AcquireSRWLockShared(&g_lock);
+    const std::shared_lock lock(g_lock);
     const Session* selected = nullptr;
     std::size_t count = 0;
     for (const Session& session : g_sessions) {
@@ -1143,7 +1134,6 @@ bool activity_replication_view_for_group(std::uint64_t groupSessionId,
         output.memberId = selected->activityMemberKey;
         output.replicationEpoch = selected->activity.replicationEpoch;
     }
-    ReleaseSRWLockShared(&g_lock);
     return count == 1;
 }
 
@@ -1151,7 +1141,7 @@ bool activity_replication_view_for_group(std::uint64_t groupSessionId,
 bool request_replication_epoch(const state::activity::SessionBinding& binding,
                                std::uint64_t expectedGeneration,
                                std::uint8_t generation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t count = 0;
     Session* const session = unique_mutable_activity_link_locked(binding, count);
     bool queued = session != nullptr && expectedGeneration != 0
@@ -1169,7 +1159,6 @@ bool request_replication_epoch(const state::activity::SessionBinding& binding,
             session->activityKeepaliveDueTick = 0;
         }
     }
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1179,7 +1168,7 @@ request_activity_authority_query(const state::activity::SessionBinding& binding,
                                  std::uint64_t expectedGeneration,
                                  std::int32_t& correlation) noexcept {
     correlation = -1;
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     Session* const session = unique_mutable_activity_link_locked(binding, linkCount);
     ActivityAuthorityQueryStatus status = ActivityAuthorityQueryStatus::noActivityLink;
@@ -1189,7 +1178,6 @@ request_activity_authority_query(const state::activity::SessionBinding& binding,
                            session->activityAuthorityQuery, expectedGeneration, correlation)
                      : ActivityAuthorityQueryStatus::staleActivityClient;
     }
-    ReleaseSRWLockExclusive(&g_lock);
     return status;
 }
 
@@ -1199,7 +1187,7 @@ activity_authority_query_snapshot(const state::activity::SessionBinding& binding
                                   std::uint64_t expectedGeneration,
                                   ActivityAuthorityQuerySnapshot& output) noexcept {
     output = {};
-    AcquireSRWLockShared(&g_lock);
+    const std::shared_lock lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     ActivityAuthorityQueryStatus status = ActivityAuthorityQueryStatus::noActivityLink;
@@ -1209,7 +1197,6 @@ activity_authority_query_snapshot(const state::activity::SessionBinding& binding
                            session->activityAuthorityQuery, expectedGeneration, output)
                      : ActivityAuthorityQueryStatus::staleActivityClient;
     }
-    ReleaseSRWLockShared(&g_lock);
     return status;
 }
 
@@ -1219,7 +1206,7 @@ request_activity_authority_reset(const state::activity::SessionBinding& binding,
                                  std::uint64_t expectedGeneration,
                                  std::int32_t& correlation) noexcept {
     correlation = -1;
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     Session* const session = unique_mutable_activity_link_locked(binding, linkCount);
     ActivityAuthorityResetStatus status = ActivityAuthorityResetStatus::noActivityLink;
@@ -1229,7 +1216,6 @@ request_activity_authority_reset(const state::activity::SessionBinding& binding,
                            session->activityAuthorityReset, expectedGeneration, correlation)
                      : ActivityAuthorityResetStatus::staleActivityClient;
     }
-    ReleaseSRWLockExclusive(&g_lock);
     return status;
 }
 
@@ -1239,7 +1225,7 @@ activity_authority_reset_snapshot(const state::activity::SessionBinding& binding
                                   std::uint64_t expectedGeneration,
                                   ActivityAuthorityResetSnapshot& output) noexcept {
     output = {};
-    AcquireSRWLockShared(&g_lock);
+    const std::shared_lock lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     ActivityAuthorityResetStatus status = ActivityAuthorityResetStatus::noActivityLink;
@@ -1249,7 +1235,6 @@ activity_authority_reset_snapshot(const state::activity::SessionBinding& binding
                            session->activityAuthorityReset, expectedGeneration, output)
                      : ActivityAuthorityResetStatus::staleActivityClient;
     }
-    ReleaseSRWLockShared(&g_lock);
     return status;
 }
 
@@ -1263,7 +1248,7 @@ bool request_activity_type23_override(
     std::int32_t expectedRegion,
     std::uint64_t expectedGeneration,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const bool queued =
@@ -1271,7 +1256,6 @@ bool request_activity_type23_override(
         && canonical_type23_available_locked(*session, target, expectedRegion, expectedGeneration)
         && activity::host::request_type23_override(
             binding, target, channel, value, snap, expectedGeneration, reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1282,14 +1266,13 @@ bool request_activity_lifetime_override(
     std::int32_t expectedRegion,
     std::uint64_t expectedGeneration,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const bool queued = session != nullptr
                         && lifetime_available_locked(*session, expectedRegion, expectedGeneration)
                         && activity::host::request_lifetime_override(
                             binding, lifetimeState, expectedGeneration, reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1306,7 +1289,7 @@ bool request_activity_state_local_type23_override(
     std::uint32_t scenarioRow,
     std::uint32_t stateRow,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const bool queued =
@@ -1326,7 +1309,6 @@ bool request_activity_state_local_type23_override(
                                                                snap,
                                                                expectedGeneration,
                                                                reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1342,7 +1324,7 @@ bool request_activity_sdk_auth_override(
     std::uint32_t scenarioRow,
     std::uint32_t stateRow,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const bool available =
@@ -1367,7 +1349,6 @@ bool request_activity_sdk_auth_override(
                                                                      bitCount,
                                                                      expectedGeneration,
                                                                      reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1377,13 +1358,12 @@ bool request_activity_type31_override(
     const activity::host::ScriptableTarget& target,
     std::int32_t expectedRegion,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const bool queued = expectedRegion >= 0 && session != nullptr
                         && selected_region_locked(*session).index == expectedRegion
                         && activity::host::request_type31_override(binding, target, reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1397,7 +1377,7 @@ bool request_activity_state_local_type31_override(
     std::uint32_t,
     std::uint32_t,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const encrypted::push::activity::EffectiveRegion region =
@@ -1411,7 +1391,6 @@ bool request_activity_state_local_type31_override(
         && valid_state_local_type31_target(target, stateLocalRosterGroup)
         && activity::host::request_state_local_type31_override(
             binding, target, stateLocalRosterGroup, expectedGeneration, reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1425,7 +1404,7 @@ bool request_activity_state_local_sequence_override(
     std::uint32_t,
     std::uint32_t,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const encrypted::push::activity::EffectiveRegion region =
@@ -1441,7 +1420,6 @@ bool request_activity_state_local_sequence_override(
         && target.authSchema == middleware::bap::activity_message::scriptable_auth::kType5Schema
         && activity::host::request_state_local_sequence_override(
             binding, target, stateLocalRosterGroup, expectedGeneration, reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1456,7 +1434,7 @@ bool request_activity_state_local_cinematic_override(
     std::uint32_t,
     std::uint32_t,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const encrypted::push::activity::EffectiveRegion region =
@@ -1472,7 +1450,6 @@ bool request_activity_state_local_cinematic_override(
         && target.authSchema == middleware::bap::activity_message::scriptable_auth::kType6Schema
         && activity::host::request_state_local_cinematic_override(
             binding, target, stateLocalRosterGroup, active, expectedGeneration, reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1487,7 +1464,7 @@ bool request_activity_state_local_performance_override(
     std::uint32_t,
     std::uint32_t,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const encrypted::push::activity::EffectiveRegion region =
@@ -1503,7 +1480,6 @@ bool request_activity_state_local_performance_override(
         && target.authSchema == middleware::bap::activity_message::scriptable_auth::kType42Schema
         && activity::host::request_state_local_performance_override(
             binding, target, stateLocalRosterGroup, stateNameHash, expectedGeneration, reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1517,7 +1493,7 @@ bool request_activity_state_local_authored_scene_override(
     std::uint32_t,
     std::uint32_t,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const encrypted::push::activity::EffectiveRegion region =
@@ -1531,7 +1507,6 @@ bool request_activity_state_local_authored_scene_override(
         && valid_state_local_authored_scene_target(target, stateLocalRosterGroup)
         && activity::host::request_state_local_authored_scene_override(
             binding, target, stateLocalRosterGroup, expectedGeneration, reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1547,7 +1522,7 @@ bool request_activity_state_local_dialogue_override(
     std::uint32_t,
     std::uint32_t,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const encrypted::push::activity::EffectiveRegion region =
@@ -1568,7 +1543,6 @@ bool request_activity_state_local_dialogue_override(
                                                                  authoredCueCount,
                                                                  expectedGeneration,
                                                                  reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1582,7 +1556,7 @@ bool request_activity_state_local_objective_reset(
     std::uint32_t,
     std::uint32_t,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const encrypted::push::activity::EffectiveRegion region =
@@ -1598,7 +1572,6 @@ bool request_activity_state_local_objective_reset(
         && target.authSchema == middleware::bap::activity_message::scriptable_auth::kType3Schema
         && activity::host::request_state_local_objective_reset(
             binding, target, stateLocalRosterGroup, expectedGeneration, reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1612,7 +1585,7 @@ bool request_activity_state_local_task_override(
     std::uint32_t,
     std::uint32_t,
     const activity::host::ScriptableOutputReservation* reservation) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const encrypted::push::activity::EffectiveRegion region =
@@ -1628,7 +1601,6 @@ bool request_activity_state_local_task_override(
         && target.authSchema == middleware::bap::activity_message::scriptable_auth::kType38Schema
         && activity::host::request_state_local_task_override(
             binding, target, stateLocalRosterGroup, expectedGeneration, reservation);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
@@ -1644,7 +1616,7 @@ bool request_activity_squad_override(
     std::uint64_t expectedGeneration,
     const activity::host::ScriptableOutputReservation* reservation,
     std::array<std::int8_t, 4> authoredProfile) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     std::size_t linkCount = 0;
     const Session* const session = unique_activity_link_locked(binding, linkCount);
     const encrypted::push::activity::EffectiveRegion region =
@@ -1664,25 +1636,22 @@ bool request_activity_squad_override(
                                                                   nameHash,
                                                                   reservation,
                                                                   authoredProfile);
-    ReleaseSRWLockExclusive(&g_lock);
     return queued;
 }
 
 /** Cancels one exact typed override revision while excluding activity-link publication. */
 bool cancel_activity_scriptable_override(const state::activity::SessionBinding& binding,
                                          std::uint64_t expectedRevision) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     const bool canceled =
         activity::host::cancel_pending_scriptable_override(binding, expectedRevision);
-    ReleaseSRWLockExclusive(&g_lock);
     return canceled;
 }
 
 /** Cancels a pending raw incident while excluding activity-link publication. */
 bool cancel_activity_host_incident(const state::activity::SessionBinding& binding) noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     const bool canceled = server::activity::host::cancel_pending_incident(binding);
-    ReleaseSRWLockExclusive(&g_lock);
     return canceled;
 }
 
@@ -1691,21 +1660,20 @@ bool cancel_activity_host_incident(const state::activity::SessionBinding& bindin
 bool session_channel(std::uint32_t connectionId,
                      std::array<std::byte, state::kBapNonceSize>& sendNonce,
                      std::array<std::byte, state::kAesKeySize>& sessionKey) noexcept {
-    AcquireSRWLockShared(&g_lock);
+    const std::shared_lock lock(g_lock);
     const Session* const session = session_for(connectionId);
     const bool armed = session != nullptr && session->authenticated;
     if (armed) {
         sendNonce = session->sendNonce;
         sessionKey = session->sessionKey;
     }
-    ReleaseSRWLockShared(&g_lock);
     return armed;
 }
 #endif
 
 /** Securely erases every connection-owned nonce and transform buffer. */
 void shutdown() noexcept {
-    AcquireSRWLockExclusive(&g_lock);
+    const std::lock_guard lock(g_lock);
     for (auto& session : g_sessions) {
         if (session.id != 0
             && session.matchmakingContext.generation != state::matchmaking::kInvalidGeneration) {
@@ -1719,7 +1687,6 @@ void shutdown() noexcept {
     SecureZeroMemory(g_sessions.data(), sizeof g_sessions);
     SecureZeroMemory(&g_scratch, sizeof g_scratch);
     g_accountGeneration = 0;
-    ReleaseSRWLockExclusive(&g_lock);
 }
 
 } // namespace sunrise::server::bap

+ 124 - 136
Sunrise/src/server/transport/bap_listener.cpp

@@ -8,15 +8,14 @@
 #include "../../core/logging/log.h"
 #include "../../core/settings/settings.h"
 #include "../activity/host_runtime.h"
+#include "core/threading/data_mutex.h"
 #include "internal.h"
 
 namespace sunrise::server::transport {
 
-Listener g_listener;
-
 namespace {
 
-SRWLOCK g_listenerLock{SRWLOCK_INIT};
+core::threading::DataMutex<Listener> g_listener;
 
 /** Makes one socket nonblocking. @return True when it can no longer block its caller. */
 [[nodiscard]] bool make_nonblocking(SOCKET socket) noexcept {
@@ -25,21 +24,21 @@ SRWLOCK g_listenerLock{SRWLOCK_INIT};
 }
 
 /** @return Index of the first unused peer slot, or the slot count when all are taken. */
-[[nodiscard]] std::size_t free_slot() noexcept {
-    for (std::size_t slot = 0; slot < g_listener.peers.size(); ++slot) {
-        if (g_listener.peers[slot].socket == INVALID_SOCKET) {
+[[nodiscard]] std::size_t free_slot(const Listener& listener) noexcept {
+    for (std::size_t slot = 0; slot < listener.peers.size(); ++slot) {
+        if (listener.peers[slot].socket == INVALID_SOCKET) {
             return slot;
         }
     }
-    return g_listener.peers.size();
+    return listener.peers.size();
 }
 
 /**
  * Takes one waiting connection into a free slot and opens its Server session.
  * @param slot Peer slot already checked to be free.
  */
-void accept_peer(std::size_t slot) noexcept {
-    const SOCKET accepted = accept(g_listener.acceptor, nullptr, nullptr);
+void accept_peer(Listener& listener, std::size_t slot) noexcept {
+    const SOCKET accepted = accept(listener.acceptor, nullptr, nullptr);
     if (accepted == INVALID_SOCKET) {
         return;
     }
@@ -47,24 +46,24 @@ void accept_peer(std::size_t slot) noexcept {
         closesocket(accepted);
         return;
     }
-    Peer& peer = g_listener.peers[slot];
+    Peer& peer = listener.peers[slot];
     peer.socket = accepted;
     peer.streamSize = 0;
     peer.outputOffset = 0;
     peer.outputSize = 0;
+    peer.connectionId = connection_id(slot);
+
     std::array<char, core::log::kLineCapacity> line{};
-    const int written = std::snprintf(line.data(),
-                                      line.size(),
-                                      "ev=transport stage=accept result=ok conn=%u",
-                                      connection_id(slot));
+    const int written = std::snprintf(
+        line.data(), line.size(), "ev=transport stage=accept result=ok conn=%u", peer.connectionId);
     if (written > 0) {
         const std::size_t length = static_cast<std::size_t>(written) < line.size()
                                        ? static_cast<std::size_t>(written)
                                        : line.size() - 1;
         core::log::write(core::log::Channel::server, core::log::Level::info, {line.data(), length});
     }
-    if (!offer(slot, client::network::BapEvent::open, {})) {
-        close_peer(slot);
+    if (!offer(peer, client::network::BapEvent::open, {})) {
+        close_peer(peer);
     }
 }
 
@@ -72,8 +71,7 @@ void accept_peer(std::size_t slot) noexcept {
  * Reads at most once from one readable peer.
  * @param slot Live peer slot reported readable.
  */
-void receive_peer(std::size_t slot) noexcept {
-    Peer& peer = g_listener.peers[slot];
+void receive_peer(Peer& peer) noexcept {
     const std::size_t free = kStreamCapacity - peer.streamSize;
     if (free == 0) {
         return;
@@ -87,7 +85,7 @@ void receive_peer(std::size_t slot) noexcept {
         return;
     }
     if (received == 0 || WSAGetLastError() != WSAEWOULDBLOCK) {
-        close_peer(slot);
+        close_peer(peer);
     }
 }
 
@@ -96,8 +94,7 @@ void receive_peer(std::size_t slot) noexcept {
  * @param slot Live peer slot.
  * @return True while the peer remains usable.
  */
-[[nodiscard]] bool flush_peer(std::size_t slot) noexcept {
-    Peer& peer = g_listener.peers[slot];
+[[nodiscard]] bool flush_peer(Peer& peer) noexcept {
     if (peer.outputSize == 0) {
         return true;
     }
@@ -120,56 +117,51 @@ void receive_peer(std::size_t slot) noexcept {
  * @param pollDue True on a poll tick.
  */
 void service_peer(
-    std::size_t slot, fd_set& readable, fd_set& writable, bool wasPending, bool pollDue) noexcept {
-    Peer& peer = g_listener.peers[slot];
+    Peer& peer, fd_set& readable, fd_set& writable, bool wasPending, bool pollDue) noexcept {
     bool sent = false;
     if (wasPending && FD_ISSET(peer.socket, &writable)) {
         sent = true;
-        if (!flush_peer(slot)) {
-            close_peer(slot);
+        if (!flush_peer(peer)) {
+            close_peer(peer);
             return;
         }
     }
     if (peer.socket != INVALID_SOCKET && FD_ISSET(peer.socket, &readable)) {
-        receive_peer(slot);
+        receive_peer(peer);
     }
     if (peer.socket == INVALID_SOCKET) {
         return;
     }
-    if (peer.outputSize == 0 && !drain_stream(slot)) {
-        close_peer(slot);
+    if (peer.outputSize == 0 && !drain_stream(peer)) {
+        close_peer(peer);
         return;
     }
-    if (pollDue && peer.outputSize == 0 && !offer(slot, client::network::BapEvent::poll, {})) {
-        close_peer(slot);
+    if (pollDue && peer.outputSize == 0 && !offer(peer, client::network::BapEvent::poll, {})) {
+        close_peer(peer);
         return;
     }
-    if (!wasPending && !sent && peer.outputSize != 0 && !flush_peer(slot)) {
-        close_peer(slot);
+    if (!wasPending && !sent && peer.outputSize != 0 && !flush_peer(peer)) {
+        close_peer(peer);
     }
 }
 
 } // namespace
 
 /** Starts the nonblocking loopback listener on one port. */
-bool initialize_on_port(std::uint16_t port) noexcept {
-    AcquireSRWLockExclusive(&g_listenerLock);
-    if (g_listener.active) {
-        ReleaseSRWLockExclusive(&g_listenerLock);
+bool initialize_on_port(Listener& listener, std::uint16_t port) noexcept {
+    if (listener.active) {
         return true;
     }
     // This DLL initializes before the game touches Winsock, so the listener starts it itself.
     WSADATA winsock{};
     if (WSAStartup(MAKEWORD(2, 2), &winsock) != 0) {
-        ReleaseSRWLockExclusive(&g_listenerLock);
         return false;
     }
-    g_listener.winsockOwned = true;
-    g_listener.acceptor = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
-    if (g_listener.acceptor == INVALID_SOCKET) {
+    listener.winsockOwned = true;
+    listener.acceptor = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+    if (listener.acceptor == INVALID_SOCKET) {
         WSACleanup();
-        g_listener.winsockOwned = false;
-        ReleaseSRWLockExclusive(&g_listenerLock);
+        listener.winsockOwned = false;
         return false;
     }
     sockaddr_in address{};
@@ -177,24 +169,23 @@ bool initialize_on_port(std::uint16_t port) noexcept {
     address.sin_port = htons(port);
     address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
     BOOL reuse = TRUE;
-    (void)setsockopt(g_listener.acceptor,
+    (void)setsockopt(listener.acceptor,
                      SOL_SOCKET,
                      SO_REUSEADDR,
                      reinterpret_cast<const char*>(&reuse),
                      sizeof reuse);
-    if (!make_nonblocking(g_listener.acceptor)
-        || bind(g_listener.acceptor, reinterpret_cast<const sockaddr*>(&address), sizeof address)
+    if (!make_nonblocking(listener.acceptor)
+        || bind(listener.acceptor, reinterpret_cast<const sockaddr*>(&address), sizeof address)
                == SOCKET_ERROR
-        || listen(g_listener.acceptor, static_cast<int>(g_listener.peers.size())) == SOCKET_ERROR) {
-        closesocket(g_listener.acceptor);
-        g_listener.acceptor = INVALID_SOCKET;
+        || listen(listener.acceptor, static_cast<int>(listener.peers.size())) == SOCKET_ERROR) {
+        closesocket(listener.acceptor);
+        listener.acceptor = INVALID_SOCKET;
         WSACleanup();
-        g_listener.winsockOwned = false;
-        ReleaseSRWLockExclusive(&g_listenerLock);
+        listener.winsockOwned = false;
         return false;
     }
-    g_listener.active = true;
-    g_listener.nextPollTick = 0;
+    listener.active = true;
+    listener.nextPollTick = 0;
     std::array<char, 64> line{};
     const int written = std::snprintf(line.data(),
                                       line.size(),
@@ -205,110 +196,107 @@ bool initialize_on_port(std::uint16_t port) noexcept {
                          core::log::Level::info,
                          {line.data(), static_cast<std::size_t>(written)});
     }
-    ReleaseSRWLockExclusive(&g_listenerLock);
     return true;
 }
 
 /** Starts the nonblocking listener on the configured BAP port. */
 bool initialize() noexcept {
-    return initialize_on_port(core::settings::get().server.bapPort);
+    return g_listener.lock([](Listener& listener) {
+        return initialize_on_port(listener, core::settings::get().server.bapPort);
+    });
 }
 
 /** Runs one bounded listener slice on the caller thread. @param now Monotonic tick count. */
 void service(std::uint64_t now) noexcept {
-    if (!TryAcquireSRWLockExclusive(&g_listenerLock)) {
-        return;
-    }
-    if (!g_listener.active) {
-        ReleaseSRWLockExclusive(&g_listenerLock);
-        return;
-    }
+    g_listener.try_lock([now](Listener& listener) {
+        if (!listener.active) {
+            return;
+        }
 
-    fd_set readable;
-    fd_set writable;
-    FD_ZERO(&readable);
-    FD_ZERO(&writable);
-    std::array<bool, client::network::kBapConnectionCount> wasPending{};
-    const std::size_t accepting = free_slot();
-    // With no free slot the acceptor is left out of the set, so a connect waits in the backlog
-    // with no handshake and no other symptom. Report the edge.
-    const bool full = accepting == g_listener.peers.size();
-    if (full != g_listener.slotsFull) {
-        g_listener.slotsFull = full;
-        std::array<char, core::log::kLineCapacity> line{};
-        const int written = std::snprintf(line.data(),
-                                          line.size(),
-                                          "ev=transport stage=accept result=%s slots=%zu",
-                                          full ? "full" : "free",
-                                          g_listener.peers.size());
-        if (written > 0) {
-            core::log::write(core::log::Channel::server,
-                             full ? core::log::Level::warn : core::log::Level::info,
-                             {line.data(), static_cast<std::size_t>(written)});
+        fd_set readable;
+        fd_set writable;
+        FD_ZERO(&readable);
+        FD_ZERO(&writable);
+        std::array<bool, client::network::kBapConnectionCount> wasPending{};
+        const std::size_t accepting = free_slot(listener);
+        // With no free slot the acceptor is left out of the set, so a connect waits in the backlog
+        // with no handshake and no other symptom. Report the edge.
+        const bool full = accepting == listener.peers.size();
+        if (full != listener.slotsFull) {
+            listener.slotsFull = full;
+            std::array<char, core::log::kLineCapacity> line{};
+            const int written = std::snprintf(line.data(),
+                                              line.size(),
+                                              "ev=transport stage=accept result=%s slots=%zu",
+                                              full ? "full" : "free",
+                                              listener.peers.size());
+            if (written > 0) {
+                core::log::write(core::log::Channel::server,
+                                 full ? core::log::Level::warn : core::log::Level::info,
+                                 {line.data(), static_cast<std::size_t>(written)});
+            }
         }
-    }
-    if (!full) {
-        FD_SET(g_listener.acceptor, &readable);
-    }
-    for (std::size_t slot = 0; slot < g_listener.peers.size(); ++slot) {
-        const Peer& peer = g_listener.peers[slot];
-        if (peer.socket == INVALID_SOCKET) {
-            continue;
+        if (!full) {
+            FD_SET(listener.acceptor, &readable);
         }
-        if (peer.streamSize < kStreamCapacity) {
-            FD_SET(peer.socket, &readable);
+        for (std::size_t slot = 0; slot < listener.peers.size(); ++slot) {
+            const Peer& peer = listener.peers[slot];
+            if (peer.socket == INVALID_SOCKET) {
+                continue;
+            }
+            if (peer.streamSize < kStreamCapacity) {
+                FD_SET(peer.socket, &readable);
+            }
+            if (peer.outputSize != 0) {
+                FD_SET(peer.socket, &writable);
+                wasPending[slot] = true;
+            }
         }
-        if (peer.outputSize != 0) {
-            FD_SET(peer.socket, &writable);
-            wasPending[slot] = true;
+        timeval timeout{};
+        if (select(0, &readable, &writable, nullptr, &timeout) == SOCKET_ERROR) {
+            return;
         }
-    }
-    timeval timeout{};
-    if (select(0, &readable, &writable, nullptr, &timeout) == SOCKET_ERROR) {
-        ReleaseSRWLockExclusive(&g_listenerLock);
-        return;
-    }
 
-    const bool timedPoll = g_listener.nextPollTick == 0 || now >= g_listener.nextPollTick;
-    if (timedPoll) {
-        g_listener.nextPollTick = now + static_cast<std::uint64_t>(kServiceIntervalMs);
-    }
-    // The poll is what lets a committed answer out. Holding one for the rest of the
-    // interval costs every queued mission action a full interval of its own.
-    const bool pollDue = timedPoll || activity::host::any_output_pending();
-    if (!full && FD_ISSET(g_listener.acceptor, &readable)) {
-        accept_peer(accepting);
-    }
-    for (std::size_t slot = 0; slot < g_listener.peers.size(); ++slot) {
-        if (g_listener.peers[slot].socket != INVALID_SOCKET) {
-            service_peer(slot, readable, writable, wasPending[slot], pollDue);
+        const bool timedPoll = listener.nextPollTick == 0 || now >= listener.nextPollTick;
+        if (timedPoll) {
+            listener.nextPollTick = now + static_cast<std::uint64_t>(kServiceIntervalMs);
         }
-    }
-    ReleaseSRWLockExclusive(&g_listenerLock);
+        // The poll is what lets a committed answer out. Holding one for the rest of the
+        // interval costs every queued mission action a full interval of its own.
+        const bool pollDue = timedPoll || activity::host::any_output_pending();
+        if (!full && FD_ISSET(listener.acceptor, &readable)) {
+            accept_peer(listener, accepting);
+        }
+        for (std::size_t slot = 0; slot < listener.peers.size(); ++slot) {
+            Peer& peer = listener.peers[slot];
+            if (peer.socket != INVALID_SOCKET) {
+                service_peer(peer, readable, writable, wasPending[slot], pollDue);
+            }
+        }
+    });
 }
 
 /** Closes every socket owned by the listener. */
 void shutdown() noexcept {
-    AcquireSRWLockExclusive(&g_listenerLock);
-    if (!g_listener.active) {
-        ReleaseSRWLockExclusive(&g_listenerLock);
-        return;
-    }
-    g_listener.active = false;
-    if (g_listener.acceptor != INVALID_SOCKET) {
-        closesocket(g_listener.acceptor);
-        g_listener.acceptor = INVALID_SOCKET;
-    }
-    for (std::size_t slot = 0; slot < g_listener.peers.size(); ++slot) {
-        close_peer(slot);
-    }
-    g_listener.nextPollTick = 0;
-    g_listener.slotsFull = false;
-    if (g_listener.winsockOwned) {
-        WSACleanup();
-        g_listener.winsockOwned = false;
-    }
-    ReleaseSRWLockExclusive(&g_listenerLock);
+    g_listener.lock([](Listener& listener) {
+        if (!listener.active) {
+            return;
+        }
+        listener.active = false;
+        if (listener.acceptor != INVALID_SOCKET) {
+            closesocket(listener.acceptor);
+            listener.acceptor = INVALID_SOCKET;
+        }
+        for (Peer& peer : listener.peers) {
+            close_peer(peer);
+        }
+        listener.nextPollTick = 0;
+        listener.slotsFull = false;
+        if (listener.winsockOwned) {
+            WSACleanup();
+            listener.winsockOwned = false;
+        }
+    });
 }
 
 } // namespace sunrise::server::transport

+ 8 - 12
Sunrise/src/server/transport/bap_peer_session.cpp

@@ -27,15 +27,12 @@ namespace {
 } // namespace
 
 /** Offers one event to the Server and stages whatever it produces. */
-bool offer(std::size_t slot,
-           client::network::BapEvent event,
-           std::span<const std::byte> frame) noexcept {
-    Peer& peer = g_listener.peers[slot];
+bool offer(Peer& peer, client::network::BapEvent event, std::span<const std::byte> frame) noexcept {
     if (peer.outputSize != 0) {
         return false;
     }
     client::network::BapResponse response{};
-    const client::network::BapRequest request{event, connection_id(slot), frame, peer.output};
+    const client::network::BapRequest request{event, peer.connectionId, frame, peer.output};
     if (!bap::consume(request, response) || response.size == 0) {
         return true;
     }
@@ -48,8 +45,7 @@ bool offer(std::size_t slot,
 }
 
 /** Removes and offers at most one complete frame from one peer's stream. */
-bool drain_stream(std::size_t slot) noexcept {
-    Peer& peer = g_listener.peers[slot];
+bool drain_stream(Peer& peer) noexcept {
     if (peer.outputSize != 0) {
         return true;
     }
@@ -66,7 +62,7 @@ bool drain_stream(std::size_t slot) noexcept {
     const int count = std::snprintf(line.data(),
                                     line.size(),
                                     "ev=transport stage=frame conn=%u type=%u bytes=%zu",
-                                    connection_id(slot),
+                                    peer.connectionId,
                                     static_cast<unsigned>(pending[1]),
                                     total);
     if (count > 0) {
@@ -76,7 +72,7 @@ bool drain_stream(std::size_t slot) noexcept {
         core::log::write(
             core::log::Channel::server, core::log::Level::debug, {line.data(), length});
     }
-    if (!offer(slot, client::network::BapEvent::frame, pending.first(total))) {
+    if (!offer(peer, client::network::BapEvent::frame, pending.first(total))) {
         return false;
     }
     peer.streamSize -= total;
@@ -101,17 +97,17 @@ bool advance_output(Peer& peer, std::size_t sent) noexcept {
 }
 
 /** Closes one peer and reports its session end to the Server. */
-void close_peer(std::size_t slot) noexcept {
-    Peer& peer = g_listener.peers[slot];
+void close_peer(Peer& peer) noexcept {
     if (peer.socket == INVALID_SOCKET) {
         return;
     }
     client::network::BapResponse response{};
     const client::network::BapRequest request{
-        client::network::BapEvent::close, connection_id(slot), {}, {}};
+        client::network::BapEvent::close, peer.connectionId, {}, {}};
     (void)bap::consume(request, response);
     closesocket(peer.socket);
     peer.socket = INVALID_SOCKET;
+    peer.connectionId = 0;
     peer.streamSize = 0;
     peer.outputOffset = 0;
     peer.outputSize = 0;

+ 4 - 8
Sunrise/src/server/transport/internal.h

@@ -22,6 +22,7 @@ inline constexpr std::size_t kStreamCapacity = client::network::kBapFrameCapacit
 /** One accepted connection with bounded ingress and committed egress storage. */
 struct Peer {
     SOCKET socket{INVALID_SOCKET};
+    std::uint32_t connectionId{};
     std::size_t streamSize{};
     std::array<std::byte, kStreamCapacity> stream{};
     std::size_t outputOffset{};
@@ -40,8 +41,6 @@ struct Listener {
     std::array<Peer, client::network::kBapConnectionCount> peers{};
 };
 
-extern Listener g_listener;
-
 /** A peer slot answers on the connection id the Server indexes its sessions by. */
 [[nodiscard]] constexpr std::uint32_t connection_id(std::size_t slot) noexcept {
     return static_cast<std::uint32_t>(slot + 1);
@@ -53,13 +52,13 @@ extern Listener g_listener;
  * @return True when the response metadata fits the peer buffer.
  */
 [[nodiscard]] bool
-offer(std::size_t slot, client::network::BapEvent event, std::span<const std::byte> frame) noexcept;
+offer(Peer& peer, client::network::BapEvent event, std::span<const std::byte> frame) noexcept;
 
 /**
  * Removes and offers at most one whole frame from one peer's stream.
  * @return True while the buffered prefix is valid.
  */
-[[nodiscard]] bool drain_stream(std::size_t slot) noexcept;
+[[nodiscard]] bool drain_stream(Peer& peer) noexcept;
 
 /**
  * Advances one committed output by an accepted send count.
@@ -70,9 +69,6 @@ offer(std::size_t slot, client::network::BapEvent event, std::span<const std::by
 [[nodiscard]] bool advance_output(Peer& peer, std::size_t sent) noexcept;
 
 /** Closes one peer and reports its session end to the Server. */
-void close_peer(std::size_t slot) noexcept;
-
-/** @param port Host-order loopback port. Zero picks an ephemeral port. */
-[[nodiscard]] bool initialize_on_port(std::uint16_t port) noexcept;
+void close_peer(Peer& peer) noexcept;
 
 } // namespace sunrise::server::transport

+ 10 - 6
Sunrise/src/state/build_data/abilities/ability_bucket_catalog.cpp

@@ -1,11 +1,15 @@
 #include "ability_bucket_catalog.h"
 
+#include <mutex>
+#include <shared_mutex>
+
 #include "../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::abilities {
 namespace {
 
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<Definition, kDefinitionCapacity> g_definitions;
 
 /** @return True when both rows name the same subclass selection. */
@@ -18,7 +22,7 @@ Table<Definition, kDefinitionCapacity> g_definitions;
 
 /** Clears every generated ability bucket row under the catalog lock. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_definitions.clear();
 }
 
@@ -42,7 +46,7 @@ bool replace(std::span<const Definition> definitions) noexcept {
     if (!valid(definitions)) {
         return false;
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     return g_definitions.replace(definitions);
 }
 
@@ -52,7 +56,7 @@ bool find(std::uint16_t socketEntryListIndex,
           Definition& definition) noexcept {
     definition = {};
     const Definition wanted{socketEntryListIndex, selection};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     for (const Definition& row : g_definitions.rows()) {
         if (same_key(row, wanted)) {
             definition = row;
@@ -64,13 +68,13 @@ bool find(std::uint16_t socketEntryListIndex,
 
 /** Copies every row in publication order. */
 bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.snapshot(output, count);
 }
 
 /** @return Number of generated ability bucket rows, read under the lock. */
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.count();
 }
 

+ 9 - 7
Sunrise/src/state/build_data/collectibles/collectible_catalog.cpp

@@ -1,20 +1,22 @@
 #include "collectible_catalog.h"
 
 #include <array>
+#include <shared_mutex>
 
 #include "../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::collectibles {
 namespace {
 
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<Definition, kDefinitionCapacity> g_definitions;
 
 } // namespace
 
 /** Clears the table while no reader can observe a partial replacement. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_definitions.clear();
 }
 
@@ -66,7 +68,7 @@ bool replace(std::span<const Definition> definitions) noexcept {
     if (!valid(definitions)) {
         return false;
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     const std::span<Definition> storage = g_definitions.reset(definitions.size());
     if (storage.size() != definitions.size()) {
         return false;
@@ -81,7 +83,7 @@ bool replace(std::span<const Definition> definitions) noexcept {
 bool find(std::uint16_t collectibleIndex, Definition& definition) noexcept {
     definition = {};
     definition.itemDefinitionIndex = kUnavailableItemDefinitionIndex;
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Definition> rows = g_definitions.rows();
     const bool found = static_cast<std::size_t>(collectibleIndex) < rows.size();
     if (found) {
@@ -95,7 +97,7 @@ bool grants_item(std::uint16_t itemDefinitionIndex) noexcept {
     if (itemDefinitionIndex == kUnavailableItemDefinitionIndex) {
         return false;
     }
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     for (const Definition& definition : g_definitions.rows()) {
         if (definition.itemDefinitionIndex == itemDefinitionIndex) {
             return true;
@@ -106,13 +108,13 @@ bool grants_item(std::uint16_t itemDefinitionIndex) noexcept {
 
 /** Copies the dense rows without exposing catalog storage. */
 bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.snapshot(output, count);
 }
 
 /** @return Number of published rows, read under the catalog lock. */
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.count();
 }
 

+ 8 - 4
Sunrise/src/state/build_data/constants/investment_constant_catalog.cpp

@@ -1,19 +1,23 @@
 #include "investment_constant_catalog.h"
 
+#include <mutex>
+#include <shared_mutex>
+
 #include "../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::constants {
 namespace {
 
 // One row, not a table, so it holds the value directly under the shared Lock.
-Lock g_lock;
+core::threading::SrwLock g_lock;
 InvestmentConstants g_constants{};
 
 } // namespace
 
 /** Clears the published investment constants under the catalog lock. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_constants = {};
 }
 
@@ -22,14 +26,14 @@ bool replace(const InvestmentConstants& value) noexcept {
     if (!valid(value)) {
         return false;
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_constants = value;
     return true;
 }
 
 /** @param value Receives the published constants. @return True when a row is published. */
 bool find(InvestmentConstants& value) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     value = g_constants;
     return value.extracted;
 }

+ 8 - 6
Sunrise/src/state/build_data/hash_names/hash_name_catalog.cpp

@@ -1,13 +1,15 @@
 #include "hash_name_catalog.h"
 
 #include <algorithm>
+#include <shared_mutex>
 
 #include "../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::hash_names {
 namespace {
 
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<Name, kNameCapacity> g_names;
 
 /** @return True when the name is a valid identifier and fits its storage. */
@@ -30,7 +32,7 @@ Table<Name, kNameCapacity> g_names;
 
 /** Clears every resolved bubble name under the catalog lock. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_names.clear();
 }
 
@@ -53,14 +55,14 @@ bool replace(std::span<const Name> names) noexcept {
     if (!valid(names)) {
         return false;
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     return g_names.replace(names);
 }
 
 /** Finds one bubble name by its hash. */
 bool find(std::uint32_t hash, Name& name) noexcept {
     name = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Name> rows = g_names.rows();
     const auto found =
         std::lower_bound(rows.begin(), rows.end(), hash, [](const Name& row, std::uint32_t key) {
@@ -75,13 +77,13 @@ bool find(std::uint32_t hash, Name& name) noexcept {
 
 /** Copies every row in ascending hash order. */
 bool snapshot(std::span<Name> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_names.snapshot(output, count);
 }
 
 /** @return Number of resolved names, read under the lock. */
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_names.count();
 }
 

+ 9 - 6
Sunrise/src/state/build_data/inventory/buckets/inventory_bucket_catalog.cpp

@@ -4,8 +4,11 @@
 #include <array>
 #include <bitset>
 #include <limits>
+#include <mutex>
+#include <shared_mutex>
 
 #include "../../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::inventory::buckets {
 namespace {
@@ -13,7 +16,7 @@ namespace {
 /** An all-one row marks a bucket id with no published descriptor. */
 constexpr std::uint16_t kEmptyLookupRow = (std::numeric_limits<std::uint16_t>::max)();
 
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<Descriptor, kDescriptorCapacity> g_descriptors;
 // Bucket id to descriptor row, rebuilt with the table under the same exclusive hold.
 std::array<std::uint16_t, kDescriptorCapacity> g_lookup{};
@@ -64,7 +67,7 @@ constexpr std::size_t kEquipmentSlotCount = 19;
 
 /** Clears every generated inventory-bucket descriptor under the catalog lock. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_descriptors.clear();
     std::fill(g_lookup.begin(), g_lookup.end(), kEmptyLookupRow);
 }
@@ -103,7 +106,7 @@ bool replace(std::span<const Descriptor> descriptors) noexcept {
         return false;
     }
 
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     std::fill(g_lookup.begin(), g_lookup.end(), kEmptyLookupRow);
     if (!g_descriptors.replace(descriptors)) {
         return false;
@@ -121,7 +124,7 @@ bool find(std::uint8_t bucketId, Descriptor& descriptor) noexcept {
         return false;
     }
 
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Descriptor> rows = g_descriptors.rows();
     const std::uint16_t row = g_lookup[bucketId];
     const bool found = row != kEmptyLookupRow && row < rows.size();
@@ -133,13 +136,13 @@ bool find(std::uint8_t bucketId, Descriptor& descriptor) noexcept {
 
 /** Copies descriptors in publication order, without exposing the catalog storage. */
 bool snapshot(std::span<Descriptor> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_descriptors.snapshot(output, count);
 }
 
 /** @return Number of inventory-bucket descriptors, read under the lock. */
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_descriptors.count();
 }
 

+ 8 - 6
Sunrise/src/state/build_data/items/details/item_detail_catalog.cpp

@@ -4,11 +4,13 @@
 #include <array>
 #include <bitset>
 #include <limits>
+#include <shared_mutex>
 #include <span>
 #include <vector>
 
 #include "../../table.h"
 #include "../item_catalog.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::items::details {
 namespace {
@@ -19,7 +21,7 @@ constexpr std::size_t kNativeDefinitionIndexCapacity =
 /** An all-one row marks a native index with no published configured detail. */
 constexpr std::uint16_t kEmptyLookupRow = (std::numeric_limits<std::uint16_t>::max)();
 
-Lock g_lock;
+core::threading::SrwLock g_lock;
 std::vector<Definition> g_definitions;
 std::size_t g_definitionCount{};
 // Native definition index to detail row, rebuilt with the table under the same exclusive hold.
@@ -83,7 +85,7 @@ static_assert(kDefinitionCapacity < kEmptyLookupRow);
 
 /** Clears every generated configured item detail under the catalog lock. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_definitions.clear();
     g_definitions.shrink_to_fit();
     g_definitionCount = 0;
@@ -114,7 +116,7 @@ bool replace(std::span<const Definition> definitions) noexcept {
 
     std::vector<Definition> staged(definitions.begin(), definitions.end());
 
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     std::fill(g_lookup.begin(), g_lookup.end(), kEmptyLookupRow);
     for (std::size_t index = 0; index < definitions.size(); ++index) {
         g_lookup[definitions[index].definitionIndex] = static_cast<std::uint16_t>(index);
@@ -127,7 +129,7 @@ bool replace(std::span<const Definition> definitions) noexcept {
 /** Finds one configured item detail by native definition index. */
 bool find(std::uint16_t definitionIndex, Definition& definition) noexcept {
     definition = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Definition> rows{g_definitions.data(), g_definitionCount};
     const std::uint16_t row = g_lookup[definitionIndex];
     const bool found = row != kEmptyLookupRow && row < rows.size();
@@ -139,7 +141,7 @@ bool find(std::uint16_t definitionIndex, Definition& definition) noexcept {
 
 /** Copies details in publication order, without exposing the catalog storage. */
 bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     count = 0;
     if (output.size() < g_definitionCount) {
         return false;
@@ -153,7 +155,7 @@ bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
 
 /** @return Number of configured item details, read under the lock. */
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitionCount;
 }
 

+ 11 - 8
Sunrise/src/state/build_data/items/item_catalog.cpp

@@ -3,8 +3,11 @@
 #include <algorithm>
 #include <array>
 #include <limits>
+#include <mutex>
+#include <shared_mutex>
 
 #include "../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::items {
 namespace {
@@ -20,7 +23,7 @@ constexpr std::uint64_t kHashPrime = 1099511628211ULL;
 /** Four definition-hash bytes precede the bucket byte in the lookup key. */
 constexpr std::size_t kDefinitionHashByteCount = sizeof(std::uint32_t);
 
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<Definition, kDefinitionCapacity> g_definitions;
 // Open-addressed probes into the dense rows, rebuilt with them under the same exclusive hold.
 std::array<std::uint16_t, kLookupCapacity> g_lookup{};
@@ -87,7 +90,7 @@ void insert_hash_lookup(const Definition& definition) noexcept {
 
 /** Clears every generated item mapping under the catalog lock. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_definitions.clear();
     std::fill(g_lookup.begin(), g_lookup.end(), kEmptyLookupRow);
     std::fill(g_hashLookup.begin(), g_hashLookup.end(), kEmptyLookupRow);
@@ -114,7 +117,7 @@ bool replace(std::span<const Definition> definitions) noexcept {
     if (!valid(definitions)) {
         return false;
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     std::fill(g_lookup.begin(), g_lookup.end(), kEmptyLookupRow);
     std::fill(g_hashLookup.begin(), g_hashLookup.end(), kEmptyLookupRow);
     // valid() proved each index appears once, so every row lands in its own slot.
@@ -139,7 +142,7 @@ bool find_hash(std::uint32_t definitionHash, Definition& definition) noexcept {
     const std::size_t start = start_hash_slot(definitionHash);
     std::uint16_t match = kEmptyLookupRow;
     bool ambiguous = false;
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Definition> rows = g_definitions.rows();
     for (std::size_t probe = 0; probe < g_hashLookup.size(); ++probe) {
         const std::uint16_t row = g_hashLookup[(start + probe) & (g_hashLookup.size() - 1)];
@@ -167,7 +170,7 @@ bool find(std::uint32_t definitionHash, std::uint8_t bucketId, Definition& defin
     const std::size_t start = start_slot(key);
     std::uint16_t match = kEmptyLookupRow;
     bool ambiguous = false;
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Definition> rows = g_definitions.rows();
     for (std::size_t probe = 0; probe < g_lookup.size(); ++probe) {
         const std::uint16_t row = g_lookup[(start + probe) & (g_lookup.size() - 1)];
@@ -189,7 +192,7 @@ bool find(std::uint32_t definitionHash, std::uint8_t bucketId, Definition& defin
 /** Finds one dense installed-build row by its native definition index. */
 bool find_index(std::uint16_t definitionIndex, Definition& definition) noexcept {
     definition = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Definition> rows = g_definitions.rows();
     const bool found = static_cast<std::size_t>(definitionIndex) < rows.size();
     if (found) {
@@ -200,13 +203,13 @@ bool find_index(std::uint16_t definitionIndex, Definition& definition) noexcept
 
 /** Copies the dense rows in native-index order, without exposing the catalog storage. */
 bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.snapshot(output, count);
 }
 
 /** @return Number of installed-build item mappings, read under the lock. */
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.count();
 }
 

+ 10 - 8
Sunrise/src/state/build_data/items/socket_plugs/socket_plug_catalog.cpp

@@ -2,13 +2,15 @@
 
 #include <algorithm>
 #include <bitset>
+#include <shared_mutex>
 
 #include "../../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::items::socket_plugs {
 namespace {
 
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<Rule, kRuleCapacity> g_rules;
 Table<Pool, kPoolCapacity> g_pools;
 Table<Member, kMemberCapacity> g_members;
@@ -24,7 +26,7 @@ std::bitset<details::kDefinitionCapacity> g_membership;
 
 /** Clears the complete socket-plug catalog under one exclusive hold. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_rules.clear();
     g_pools.clear();
     g_members.clear();
@@ -78,7 +80,7 @@ bool replace(std::span<const Rule> rules,
     for (const Member member : members) {
         membership.set(member);
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     if (!g_rules.replace(rules) || !g_pools.replace(pools) || !g_members.replace(members)) {
         return false;
     }
@@ -93,7 +95,7 @@ bool allowed(std::uint16_t itemDefinitionIndex,
     if (lane >= kLaneCapacity) {
         return false;
     }
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const auto rules = g_rules.rows();
     const auto pools = g_pools.rows();
     const auto members = g_members.rows();
@@ -120,7 +122,7 @@ bool visit_pool(std::uint16_t itemDefinitionIndex,
     if (lane >= kLaneCapacity || visitor == nullptr) {
         return false;
     }
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const auto rules = g_rules.rows();
     const auto pools = g_pools.rows();
     const auto members = g_members.rows();
@@ -145,7 +147,7 @@ bool visit_pool(std::uint16_t itemDefinitionIndex,
 
 /** Answers whether one definition occurs in any installed ordinary-socket plug pool. */
 bool contains(Member plugDefinitionIndex) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return plugDefinitionIndex < g_membership.size() && g_membership.test(plugDefinitionIndex);
 }
 
@@ -159,14 +161,14 @@ bool snapshot(std::span<Rule> rules,
     ruleCount = 0;
     poolCount = 0;
     memberCount = 0;
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_rules.snapshot(rules, ruleCount) && g_pools.snapshot(pools, poolCount)
            && g_members.snapshot(members, memberCount);
 }
 
 /** Reports the published rule count under the catalog lock. */
 std::size_t rule_count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_rules.count();
 }
 

+ 9 - 6
Sunrise/src/state/build_data/material_requirements/material_requirement_catalog.cpp

@@ -1,19 +1,22 @@
 #include "material_requirement_catalog.h"
 
 #include <array>
+#include <mutex>
+#include <shared_mutex>
 
 #include "../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::material_requirements {
 namespace {
 
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<Definition, kDefinitionCapacity> g_definitions;
 
 } // namespace
 
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_definitions.clear();
 }
 
@@ -68,7 +71,7 @@ bool replace(std::span<const Definition> definitions) noexcept {
     if (!valid(definitions)) {
         return false;
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     const std::span<Definition> storage = g_definitions.reset(definitions.size());
     if (storage.size() != definitions.size()) {
         return false;
@@ -81,7 +84,7 @@ bool replace(std::span<const Definition> definitions) noexcept {
 
 bool find(std::uint16_t requirementSetIndex, Definition& definition) noexcept {
     definition = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Definition> rows = g_definitions.rows();
     const bool found = static_cast<std::size_t>(requirementSetIndex) < rows.size();
     if (found) {
@@ -91,12 +94,12 @@ bool find(std::uint16_t requirementSetIndex, Definition& definition) noexcept {
 }
 
 bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.snapshot(output, count);
 }
 
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.count();
 }
 

+ 9 - 6
Sunrise/src/state/build_data/progressions/progression_catalog.cpp

@@ -1,18 +1,21 @@
 #include "progression_catalog.h"
 
+#include <shared_mutex>
+
 #include "../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::progressions {
 namespace {
 
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<Definition, kDefinitionCapacity> g_definitions;
 
 } // namespace
 
 /** Clears every generated progression definition under the catalog lock. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_definitions.clear();
 }
 
@@ -34,14 +37,14 @@ bool replace(std::span<const Definition> definitions) noexcept {
     if (!valid(definitions)) {
         return false;
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     return g_definitions.replace(definitions);
 }
 
 /** Lists the definition index each slot of one scope's record array carries. */
 bool slots(Scope scope, std::span<std::uint16_t> output, std::size_t& count) noexcept {
     count = 0;
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Definition> rows = g_definitions.rows();
     bool complete = !rows.empty();
     for (const Definition& row : rows) {
@@ -65,13 +68,13 @@ bool slots(Scope scope, std::span<std::uint16_t> output, std::size_t& count) noe
 
 /** Copies every row in native definition order. */
 bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.snapshot(output, count);
 }
 
 /** @return Number of generated progression definitions, read under the lock. */
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.count();
 }
 

+ 13 - 9
Sunrise/src/state/build_data/scenarios/scenario_catalog.cpp

@@ -1,12 +1,16 @@
 #include "scenario_catalog.h"
 
+#include <mutex>
+#include <shared_mutex>
+
 #include "../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::scenarios {
 namespace {
 
 // One lock covers both tables: a reader must never see new layouts against old roster groups.
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<Definition, kDefinitionCapacity> g_definitions;
 Table<RosterGroup, kRosterGroupCapacity> g_groups;
 
@@ -83,7 +87,7 @@ Table<RosterGroup, kRosterGroupCapacity> g_groups;
 
 /** Clears every extracted destination layout and roster group under the catalog lock. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_definitions.clear();
     g_groups.clear();
 }
@@ -121,7 +125,7 @@ bool replace(std::span<const Definition> definitions,
     if (!valid(definitions, groups)) {
         return false;
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     // Both run, with no short-circuit, so the pair cannot be left half replaced. valid() already
     // checked each against its size, which is the only reason either can refuse.
     const bool storedDefinitions = g_definitions.replace(definitions);
@@ -132,7 +136,7 @@ bool replace(std::span<const Definition> definitions,
 /** Copies one roster group by table index. */
 bool group(std::size_t index, RosterGroup& group) noexcept {
     group = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const RosterGroup> rows = g_groups.rows();
     const bool present = index < rows.size();
     if (present) {
@@ -143,13 +147,13 @@ bool group(std::size_t index, RosterGroup& group) noexcept {
 
 /** @return Published roster group count. */
 std::size_t group_count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_groups.count();
 }
 
 /** Copies every roster group in extraction order. */
 bool snapshot_groups(std::span<RosterGroup> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_groups.snapshot(output, count);
 }
 
@@ -159,7 +163,7 @@ bool find(std::string_view name, Definition& definition) noexcept {
     if (name.empty() || name.size() > kNameCapacity) {
         return false;
     }
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     for (const Definition& row : g_definitions.rows()) {
         if (name_of(row) == name) {
             definition = row;
@@ -171,13 +175,13 @@ bool find(std::string_view name, Definition& definition) noexcept {
 
 /** Copies every row in extraction order. */
 bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.snapshot(output, count);
 }
 
 /** @return The number of extracted destination layouts, read under the lock. */
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.count();
 }
 

+ 9 - 5
Sunrise/src/state/build_data/socket_entry_buckets/socket_entry_bucket_catalog.cpp

@@ -1,18 +1,22 @@
 #include "socket_entry_bucket_catalog.h"
 
+#include <mutex>
+#include <shared_mutex>
+
 #include "../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::socket_entry_buckets {
 namespace {
 
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<Definition, kDefinitionCapacity> g_definitions;
 
 } // namespace
 
 /** Clears every resolved entry-bucket row under the catalog lock. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_definitions.clear();
 }
 
@@ -36,14 +40,14 @@ bool replace(std::span<const Definition> definitions) noexcept {
     if (!valid(definitions)) {
         return false;
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     return g_definitions.replace(definitions);
 }
 
 /** Finds one socket-entry list's resolved per-entry ability-bucket destinations. */
 bool find(std::uint16_t socketEntryListIndex, Definition& definition) noexcept {
     definition = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     for (const Definition& row : g_definitions.rows()) {
         if (row.socketEntryListIndex == socketEntryListIndex) {
             definition = row;
@@ -55,7 +59,7 @@ bool find(std::uint16_t socketEntryListIndex, Definition& definition) noexcept {
 
 /** @return Number of resolved entry-bucket rows, read under the lock. */
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.count();
 }
 

+ 12 - 9
Sunrise/src/state/build_data/socket_entry_lists/socket_entry_list_catalog.cpp

@@ -1,14 +1,17 @@
 #include "socket_entry_list_catalog.h"
 
 #include <array>
+#include <mutex>
+#include <shared_mutex>
 
 #include "../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::socket_entry_lists {
 namespace {
 
 // One lock covers both tables: an entry table is only meaningful against its own list rows.
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<Definition, kDefinitionCapacity> g_definitions;
 Table<EntryTable, kEntryTableCapacity> g_entryTables;
 
@@ -28,7 +31,7 @@ Table<EntryTable, kEntryTableCapacity> g_entryTables;
 
 /** Clears every generated socket-entry-list mapping under the catalog lock. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_definitions.clear();
     g_entryTables.clear();
 }
@@ -56,7 +59,7 @@ bool replace(std::span<const Definition> definitions) noexcept {
         return false;
     }
 
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     // valid() proved every index in the input range appears once, so each row lands once.
     const std::span<Definition> storage = g_definitions.reset(definitions.size());
     if (storage.size() != definitions.size()) {
@@ -71,7 +74,7 @@ bool replace(std::span<const Definition> definitions) noexcept {
 /** Finds one socket-entry-list mapping by native definition index. */
 bool find(std::uint16_t definitionIndex, Definition& definition) noexcept {
     definition = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Definition> rows = g_definitions.rows();
     const bool found = definitionIndex < rows.size();
     if (found) {
@@ -82,13 +85,13 @@ bool find(std::uint16_t definitionIndex, Definition& definition) noexcept {
 
 /** Copies dense native-index rows without handing out mutable catalog storage. */
 bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.snapshot(output, count);
 }
 
 /** @return The number of complete socket-entry-list mappings, read under the lock. */
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.count();
 }
 
@@ -115,14 +118,14 @@ bool replace_entry_tables(std::span<const EntryTable> tables) noexcept {
     if (!valid_entry_tables(tables)) {
         return false;
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     return g_entryTables.replace(tables);
 }
 
 /** Finds one list's per-entry selection inputs. */
 bool find_entry_table(std::uint16_t definitionIndex, EntryTable& table) noexcept {
     table = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     for (const EntryTable& row : g_entryTables.rows()) {
         if (row.definitionIndex == definitionIndex) {
             table = row;
@@ -134,7 +137,7 @@ bool find_entry_table(std::uint16_t definitionIndex, EntryTable& table) noexcept
 
 /** Copies every kept entry table. */
 bool snapshot_entry_tables(std::span<EntryTable> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_entryTables.snapshot(output, count);
 }
 

+ 15 - 13
Sunrise/src/state/build_data/spawn_sets/spawn_set_catalog.cpp

@@ -3,9 +3,11 @@
 #include <algorithm>
 #include <cmath>
 #include <limits>
+#include <shared_mutex>
 #include <string_view>
 
 #include "../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::spawn_sets {
 namespace {
@@ -15,7 +17,7 @@ constexpr float kPositionBound = 1.0e9F;
 
 // One lock covers all three tables. A stem names a hash range and a point names a stem row, so
 // replacing one alone would leave a reader resolving past the end.
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<Stem, kStemCapacity> g_stems;
 Table<NameHash, kNameHashCapacity> g_nameHashes;
 Table<Point, kPointCapacity> g_points;
@@ -98,7 +100,7 @@ Table<Point, kPointCapacity> g_points;
 
 /** Clears every extracted stem, name-hash and point row under the catalog lock. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_stems.clear();
     g_nameHashes.clear();
     g_points.clear();
@@ -146,7 +148,7 @@ bool replace(std::span<const Stem> stems,
     if (!valid(stems, nameHashes) || !valid_points(points, stems)) {
         return false;
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     // All three run with no short-circuit, so the set is never left half replaced.
     const bool storedStems = g_stems.replace(stems);
     const bool storedHashes = g_nameHashes.replace(nameHashes);
@@ -161,7 +163,7 @@ bool nearest_point(std::string_view stem,
                    float& distance) noexcept {
     point = {};
     distance = 0.0F;
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     std::size_t stemIndex = 0;
     if (!stem_index_locked(stem, stemIndex)) {
         return false;
@@ -188,20 +190,20 @@ bool nearest_point(std::string_view stem,
 
 /** Copies the whole point bank. */
 bool snapshot_points(std::span<Point> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_points.snapshot(output, count);
 }
 
 /** @return The point row count, read under the lock. */
 std::size_t point_count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_points.count();
 }
 
 /** Finds one stem by its normalized name. */
 bool find(std::string_view name, Stem& stem) noexcept {
     stem = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Stem> rows = g_stems.rows();
     const auto found =
         std::lower_bound(rows.begin(), rows.end(), name, [](const Stem& row, auto key) {
@@ -217,7 +219,7 @@ bool find(std::string_view name, Stem& stem) noexcept {
 /** Finds one spawn-name hash inside one stem. */
 bool find_hash(std::string_view stem, std::uint32_t value, NameHash& nameHash) noexcept {
     nameHash = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Stem> stemRows = g_stems.rows();
     const auto foundStem =
         std::lower_bound(stemRows.begin(), stemRows.end(), stem, [](const Stem& row, auto key) {
@@ -241,7 +243,7 @@ bool find_hash(std::string_view stem, std::uint32_t value, NameHash& nameHash) n
 /** Copies the name-hash rows one stem owns. */
 bool stem_hashes(const Stem& stem, std::span<NameHash> output, std::size_t& count) noexcept {
     count = 0;
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const NameHash> bank = g_nameHashes.rows();
     const std::size_t offset = stem.nameHashOffset;
     const std::size_t rows = stem.nameHashCount;
@@ -257,25 +259,25 @@ bool stem_hashes(const Stem& stem, std::span<NameHash> output, std::size_t& coun
 
 /** Copies every stem row in ascending name order. */
 bool snapshot(std::span<Stem> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_stems.snapshot(output, count);
 }
 
 /** Copies the whole flat name-hash bank. */
 bool snapshot_hashes(std::span<NameHash> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_nameHashes.snapshot(output, count);
 }
 
 /** @return The stem row count, read under the lock. */
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_stems.count();
 }
 
 /** @return The name-hash row count, read under the lock. */
 std::size_t hash_count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_nameHashes.count();
 }
 

+ 0 - 48
Sunrise/src/state/build_data/table.h

@@ -9,54 +9,6 @@
 
 namespace sunrise::state::build_data {
 
-/**
- * Reader/writer lock guarding one domain's published rows.
- * The lock sits apart from the storage so a domain with several arrays holds one lock across all
- * of them. A reader then never sees one array replaced and another not.
- */
-class Lock final {
-public:
-    /** Holds the lock for reading until it leaves scope. */
-    class Shared final {
-    public:
-        explicit Shared(const Lock& owner) noexcept : owner_(owner) {
-            AcquireSRWLockShared(&owner_.lock_);
-        }
-        ~Shared() {
-            ReleaseSRWLockShared(&owner_.lock_);
-        }
-        Shared(const Shared&) = delete;
-        Shared(Shared&&) = delete;
-        Shared& operator=(const Shared&) = delete;
-        Shared& operator=(Shared&&) = delete;
-
-    private:
-        const Lock& owner_;
-    };
-
-    /** Holds the lock for writing until it leaves scope. */
-    class Exclusive final {
-    public:
-        explicit Exclusive(Lock& owner) noexcept : owner_(owner) {
-            AcquireSRWLockExclusive(&owner_.lock_);
-        }
-        ~Exclusive() {
-            ReleaseSRWLockExclusive(&owner_.lock_);
-        }
-        Exclusive(const Exclusive&) = delete;
-        Exclusive(Exclusive&&) = delete;
-        Exclusive& operator=(const Exclusive&) = delete;
-        Exclusive& operator=(Exclusive&&) = delete;
-
-    private:
-        Lock& owner_;
-    };
-
-private:
-    // The acquire calls take a non-const pointer, and a shared hold does not modify the rows.
-    mutable SRWLOCK lock_{SRWLOCK_INIT};
-};
-
 /**
  * Fixed row storage for one published table.
  * The caller must already hold the domain's Lock: exclusive to write, shared to read. Storage is

+ 18 - 16
Sunrise/src/state/build_data/vendors/vendor_catalog.cpp

@@ -1,15 +1,17 @@
 #include "vendor_catalog.h"
 
 #include <algorithm>
+#include <shared_mutex>
 
 #include "../table.h"
+#include "core/threading/srw_lock.h"
 
 namespace sunrise::state::build_data::vendors {
 namespace {
 
 // One lock covers all four tables. A definition names its rows by range, so a reader must
 // never see one table replaced and another not.
-Lock g_lock;
+core::threading::SrwLock g_lock;
 Table<IndexEntry, kIndexCapacity> g_index;
 Table<Definition, kDefinitionCapacity> g_definitions;
 Table<SaleRow, kSaleRowCapacity> g_saleRows;
@@ -147,7 +149,7 @@ template <typename Row>
 
 /** Clears the index, every held definition, and both row banks under the catalog lock. */
 void clear() noexcept {
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     g_index.clear();
     g_definitions.clear();
     g_saleRows.clear();
@@ -195,7 +197,7 @@ bool replace(std::span<const IndexEntry> index,
     if (!valid(index, definitions, saleRows, installedRows)) {
         return false;
     }
-    const Lock::Exclusive guard(g_lock);
+    const std::lock_guard guard(g_lock);
     // All four run with no short-circuit, so the set cannot be left half replaced. Capacity is
     // the only reason one can refuse, and valid() already checked it.
     const bool storedIndex = g_index.replace(index);
@@ -208,7 +210,7 @@ bool replace(std::span<const IndexEntry> index,
 /** Finds one index row by the vendor definition hash. */
 bool find_hash(std::uint32_t definitionHash, IndexEntry& entry) noexcept {
     entry = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const IndexEntry> rows = g_index.rows();
     const auto found =
         std::find_if(rows.begin(), rows.end(), [definitionHash](const IndexEntry& row) {
@@ -229,7 +231,7 @@ bool find_hash(std::uint32_t definitionHash, IndexEntry& entry) noexcept {
 /** Reads one index row by its position. */
 bool find_index(std::uint16_t index, IndexEntry& entry) noexcept {
     entry = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const IndexEntry> rows = g_index.rows();
     const bool present = index < rows.size();
     if (present) {
@@ -241,7 +243,7 @@ bool find_index(std::uint16_t index, IndexEntry& entry) noexcept {
 /** Finds one held definition by the vendor definition hash. */
 bool find(std::uint32_t definitionHash, Definition& definition) noexcept {
     definition = {};
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     const std::span<const Definition> rows = g_definitions.rows();
     const auto found =
         std::find_if(rows.begin(), rows.end(), [definitionHash](const Definition& row) {
@@ -258,7 +260,7 @@ bool find(std::uint32_t definitionHash, Definition& definition) noexcept {
 bool sale_rows(const Definition& definition,
                std::span<SaleRow> output,
                std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return copy_range(
         g_saleRows.rows(), definition.saleRowOffset, definition.saleCount, output, count);
 }
@@ -267,7 +269,7 @@ bool sale_rows(const Definition& definition,
 bool installed_rows(const Definition& definition,
                     std::span<InstalledRow> output,
                     std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return copy_range(g_installedRows.rows(),
                       definition.installedRowOffset,
                       definition.installedCount,
@@ -277,49 +279,49 @@ bool installed_rows(const Definition& definition,
 
 /** Copies every index row in ascending index order. */
 bool snapshot_index(std::span<IndexEntry> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_index.snapshot(output, count);
 }
 
 /** Copies every held definition in ascending index order. */
 bool snapshot_definitions(std::span<Definition> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.snapshot(output, count);
 }
 
 /** Copies the whole flat sale bank. */
 bool snapshot_sale_rows(std::span<SaleRow> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_saleRows.snapshot(output, count);
 }
 
 /** Copies the whole flat installed bank. */
 bool snapshot_installed_rows(std::span<InstalledRow> output, std::size_t& count) noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_installedRows.snapshot(output, count);
 }
 
 /** @return The index row count, read under the lock. */
 std::size_t count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_index.count();
 }
 
 /** @return The held definition count, read under the lock. */
 std::size_t definition_count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_definitions.count();
 }
 
 /** @return The flat sale bank row count, read under the lock. */
 std::size_t sale_row_count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_saleRows.count();
 }
 
 /** @return The flat installed bank row count, read under the lock. */
 std::size_t installed_row_count() noexcept {
-    const Lock::Shared guard(g_lock);
+    const std::shared_lock guard(g_lock);
     return g_installedRows.count();
 }
 

+ 4 - 3
Sunrise/src/steam/runtime/steam_context_state.cpp

@@ -5,8 +5,10 @@
 #include <atomic>
 #include <cstddef>
 #include <cstring>
+#include <mutex>
 
 #include "../interfaces/steam_interface_factory.h"
+#include "core/threading/srw_lock.h"
 #include "internal.h"
 #include "runtime.h"
 
@@ -32,7 +34,7 @@ enum class ContextField : std::size_t {
     interface = 2,   // Cached context interface pointer.
 };
 
-SRWLOCK g_contextLock{SRWLOCK_INIT};
+core::threading::SrwLock g_contextLock;
 std::atomic_uintptr_t g_contextGeneration{kFirstContextGeneration};
 std::atomic<DWORD> g_appId{};
 std::atomic<ApiCall> g_nextApiCall{kFirstApiCall};
@@ -77,7 +79,7 @@ void* context_init(void* data) noexcept {
     std::uintptr_t storedGeneration{};
     std::memcpy(&storedGeneration, &fields[generationIndex], sizeof(storedGeneration));
 
-    AcquireSRWLockExclusive(&g_contextLock);
+    const std::lock_guard lock(g_contextLock);
     if (storedGeneration != generation) {
         fields[interfaceIndex] = nullptr;
         const auto initializer = reinterpret_cast<void (*)(void*)>(fields[initializerIndex]);
@@ -88,7 +90,6 @@ void* context_init(void* data) noexcept {
         std::memcpy(&fields[generationIndex], &generation, sizeof(generation));
     }
     void* result = &fields[interfaceIndex];
-    ReleaseSRWLockExclusive(&g_contextLock);
     return result;
 }
 

+ 80 - 76
Sunrise/src/steam/runtime/steam_lifecycle.cpp

@@ -10,6 +10,7 @@
 #include "../../core/runtime/core_runtime.h"
 #include "../../core/runtime/host_environment.h"
 #include "callbacks/callback_registry.h"
+#include "core/threading/data_mutex.h"
 #include "internal.h"
 #include "runtime.h"
 #include "steam_context_state.h"
@@ -20,12 +21,15 @@ namespace {
 /** The only delay-loaded module allowed to start the platform Client group. */
 constexpr wchar_t kNetworkingModuleName[] = L"steamnetworkingsockets.dll";
 
-SRWLOCK g_lifecycleLock{SRWLOCK_INIT};
+struct Lifecycle {
+    bool mainActivationDone{};
+    bool mainActivationResult{};
+    bool graphicsActivationAttempted{};
+    bool platformActivationAttempted{};
+};
+
+core::threading::SharedDataMutex<Lifecycle> g_lifecycle;
 std::atomic_bool g_initialized{false};
-bool g_mainActivationDone{};
-bool g_mainActivationResult{};
-bool g_graphicsActivationAttempted{};
-bool g_platformActivationAttempted{};
 
 /**
  * Finds the loaded image that owns a caught return address. It takes no module reference.
@@ -55,60 +59,58 @@ bool initialize(void* module) noexcept {
     if (!client::hooks::egress::install()) {
         return false;
     }
-    AcquireSRWLockExclusive(&g_lifecycleLock);
-    if (g_initialized.load(std::memory_order_acquire)) {
-        ReleaseSRWLockExclusive(&g_lifecycleLock);
+
+    return g_lifecycle.lock_write([module](Lifecycle&) {
+        if (g_initialized.load(std::memory_order_acquire)) {
+            return true;
+        }
+        if (!core::initialize(module)) {
+            return false;
+        }
+        // Base generation (_0) packages register during bootload, before the first callback pump,
+        // so package trust must attach at Steam init rather than in the main-image hook sweep.
+        if (!client::hooks::package_trust::install()) {
+            core::log::write(core::log::Channel::client,
+                             core::log::Level::error,
+                             "ev=steam_init stage=package_trust result=fail");
+            (void)core::shutdown();
+            return false;
+        }
+        advance_context_generation();
+        g_initialized.store(true, std::memory_order_release);
+        core::log::write(
+            core::log::Channel::client, core::log::Level::info, "ev=steam_init result=ok");
+        // The guard attaches above, before Core logging exists, so its outcome is reported here.
+        client::hooks::egress::report_installation();
         return true;
-    }
-    if (!core::initialize(module)) {
-        ReleaseSRWLockExclusive(&g_lifecycleLock);
-        return false;
-    }
-    // Base generation (_0) packages register during bootload, before the first callback pump, so
-    // package trust must attach at Steam init rather than in the main-image hook sweep.
-    if (!client::hooks::package_trust::install()) {
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::error,
-                         "ev=steam_init stage=package_trust result=fail");
-        (void)core::shutdown();
-        ReleaseSRWLockExclusive(&g_lifecycleLock);
-        return false;
-    }
-    advance_context_generation();
-    g_initialized.store(true, std::memory_order_release);
-    core::log::write(core::log::Channel::client, core::log::Level::info, "ev=steam_init result=ok");
-    // The guard attaches above, before Core logging exists, so its outcome is reported here.
-    client::hooks::egress::report_installation();
-    ReleaseSRWLockExclusive(&g_lifecycleLock);
-    return true;
+    });
 }
 
 /** Stops callback delivery and clears Steam state. */
 bool shutdown() noexcept {
-    AcquireSRWLockExclusive(&g_lifecycleLock);
-    const bool hadRuntime = g_initialized.load(std::memory_order_acquire) || core::is_initialized();
-    if (!hadRuntime) {
-        ReleaseSRWLockExclusive(&g_lifecycleLock);
+    return g_lifecycle.lock_write([](Lifecycle& lifecycle) {
+        const bool hadRuntime =
+            g_initialized.load(std::memory_order_acquire) || core::is_initialized();
+        if (!hadRuntime) {
+            return true;
+        }
+        if (!core::shutdown()) {
+            core::log::write(core::log::Channel::client,
+                             core::log::Level::error,
+                             "ev=steam_shutdown stage=core result=fail");
+            return false;
+        }
+
+        // Callback pointers are released only after Client hooks stop producing events.
+        runtime::callbacks::clear();
+        g_initialized.store(false, std::memory_order_release);
+        lifecycle.mainActivationDone = false;
+        lifecycle.mainActivationResult = false;
+        lifecycle.graphicsActivationAttempted = false;
+        lifecycle.platformActivationAttempted = false;
+        advance_context_generation();
         return true;
-    }
-    if (!core::shutdown()) {
-        core::log::write(core::log::Channel::client,
-                         core::log::Level::error,
-                         "ev=steam_shutdown stage=core result=fail");
-        ReleaseSRWLockExclusive(&g_lifecycleLock);
-        return false;
-    }
-
-    // Callback pointers are released only after Client hooks stop producing events.
-    runtime::callbacks::clear();
-    g_initialized.store(false, std::memory_order_release);
-    g_mainActivationDone = false;
-    g_mainActivationResult = false;
-    g_graphicsActivationAttempted = false;
-    g_platformActivationAttempted = false;
-    advance_context_generation();
-    ReleaseSRWLockExclusive(&g_lifecycleLock);
-    return true;
+    });
 }
 
 /** @return True. The in-process Steam provider stays up for the whole DLL lifetime. */
@@ -122,33 +124,33 @@ namespace sunrise::steam::runtime {
 
 /** Runs main-image activation once, from a caller that proves the game is loaded. */
 bool activate_main_once() noexcept {
-    AcquireSRWLockExclusive(&g_lifecycleLock);
-    if (!g_mainActivationDone && core::is_initialized()) {
-        g_mainActivationDone = true;
-        g_mainActivationResult = client::activate_main_once();
-    }
-    const bool result = g_mainActivationResult;
-    ReleaseSRWLockExclusive(&g_lifecycleLock);
-    return result;
+    return g_lifecycle.lock_write([](Lifecycle& lifecycle) {
+        if (!lifecycle.mainActivationDone && core::is_initialized()) {
+            lifecycle.mainActivationDone = true;
+            lifecycle.mainActivationResult = client::activate_main_once();
+        }
+
+        const bool result = lifecycle.mainActivationResult;
+        return result;
+    });
 }
 
 /** @return True while the main-image sweep has not run yet. */
 bool main_activation_pending() noexcept {
-    AcquireSRWLockShared(&g_lifecycleLock);
-    const bool pending = !g_mainActivationDone;
-    ReleaseSRWLockShared(&g_lifecycleLock);
+    const bool pending = g_lifecycle.lock_read(
+        [](const Lifecycle& lifecycle) { return !lifecycle.mainActivationDone; });
     // Matches the activation's Core test. A failed Core must not raise an endless overlay.
     return pending && core::is_initialized();
 }
 
 /** Installs the presentation hooks once, from the callback pump, before the game sweep. */
 void activate_graphics_once() noexcept {
-    AcquireSRWLockExclusive(&g_lifecycleLock);
-    if (!g_graphicsActivationAttempted && core::is_initialized()) {
-        g_graphicsActivationAttempted = true;
-        (void)client::activate_graphics_once();
-    }
-    ReleaseSRWLockExclusive(&g_lifecycleLock);
+    g_lifecycle.lock_write([](Lifecycle& lifecycle) {
+        if (!lifecycle.graphicsActivationAttempted && core::is_initialized()) {
+            lifecycle.graphicsActivationAttempted = true;
+            (void)client::activate_graphics_once();
+        }
+    });
 }
 
 /** Activates the platform Client group at its exact interface request boundary. */
@@ -158,12 +160,14 @@ void activate_platform_once(const void* callerAddress) noexcept {
         return;
     }
 
-    AcquireSRWLockExclusive(&g_lifecycleLock);
-    if (!g_platformActivationAttempted && g_initialized.load(std::memory_order_acquire)) {
-        g_platformActivationAttempted = true;
-        (void)client::activate_platform_once(callerModule);
-    }
-    ReleaseSRWLockExclusive(&g_lifecycleLock);
+    g_lifecycle.lock_write([callerModule](Lifecycle& lifecycle) {
+        if (!lifecycle.platformActivationAttempted
+            && g_initialized.load(std::memory_order_acquire)) {
+
+            lifecycle.platformActivationAttempted = true;
+            (void)client::activate_platform_once(callerModule);
+        }
+    });
 }
 
 } // namespace sunrise::steam::runtime