From 78ca95f7181a9aed1b2261659413174385a36ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=E9=B8=BD?= <43724908+Akarinnnnn@users.noreply.github.com> Date: Wed, 5 Feb 2025 00:58:50 +0800 Subject: [PATCH 1/6] Reduced HGlobal allocation times in CallResult scenario. Changed .NET Standard project to make use of new(.NET 5+) API, Unity will also get benefit from it. Compatibility to Framework and old Unity is handled by conditional compilcation. --- Standalone/Steamworks.NET.Standard.csproj | 2 +- .../Runtime/CallbackDispatcher.cs | 110 ++++++++++++++++-- 2 files changed, 101 insertions(+), 11 deletions(-) diff --git a/Standalone/Steamworks.NET.Standard.csproj b/Standalone/Steamworks.NET.Standard.csproj index 9a7cce63..8eccf2ae 100644 --- a/Standalone/Steamworks.NET.Standard.csproj +++ b/Standalone/Steamworks.NET.Standard.csproj @@ -1,7 +1,7 @@  - netstandard2.1 + netstandard2.1;net8.0 Steamworks Steamworks.NET x64;x86 diff --git a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs index 47bcaecf..acececef 100644 --- a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs +++ b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs @@ -27,6 +27,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Steamworks { @@ -48,6 +49,9 @@ public static void ExceptionHandler(Exception e) { private static object m_sync = new object(); private static IntPtr m_pCallbackMsg; private static int m_initCount; + // 4096 * 3 is enough for most api calls, + private static int s_currentCallResultBufferSize = 4096 * 3; + private static IntPtr s_pCallResultBuffer = Marshal.AllocHGlobal(s_currentCallResultBufferSize); public static bool IsInitialized { get { return m_initCount > 0; } @@ -69,12 +73,54 @@ internal static void Shutdown() { if (m_initCount == 0) { UnregisterAll(); Marshal.FreeHGlobal(m_pCallbackMsg); + Marshal.FreeHGlobal(s_pCallResultBuffer); m_pCallbackMsg = IntPtr.Zero; + s_pCallResultBuffer = IntPtr.Zero; } } } - internal static void Register(Callback cb) { + private static IntPtr CheckCallResultBufferSize(uint requiredBufferSize) + { + if (s_currentCallResultBufferSize >= requiredBufferSize) + return s_pCallResultBuffer; // buffer is enough, this happens mostly + + // have to resize buffer + lock (m_sync) + { + // double check buffer size to avoid resize the buffer smaller + if (s_currentCallResultBufferSize < requiredBufferSize) + return s_pCallResultBuffer; + + // round buffer size to next multiple of 4096 +#if NET6_0_OR_GREATER // decided to use NET6_0 here is for Unity builds, same as 5 below + // System.Numerics is not always available for using + uint newBufferSize = System.Numerics.BitOperations.RoundUpToPowerOf2(requiredBufferSize); +#else + uint newBufferSize = requiredBufferSize; + if ((newBufferSize & 0x1FFF) != 4096) + newBufferSize = (newBufferSize + 4095) & 0xFFFFF000; +#endif + if (newBufferSize > int.MaxValue) { + newBufferSize = requiredBufferSize; + // not to use enlarged size since we don't have enough space + if (newBufferSize > int.MaxValue) { + throw new NotSupportedException("The param size of a call result is larger than 2GiB"); + } + } + +#if NET5_0_OR_GREATER + s_pCallResultBuffer = Marshal.ReAllocHGlobal(s_pCallResultBuffer, (nint)newBufferSize); +#else + Marshal.FreeHGlobal(s_pCallResultBuffer); + s_pCallResultBuffer = IntPtr.Zero; // is this necessary? + s_pCallResultBuffer = Marshal.AllocHGlobal((int)newBufferSize); +#endif + return s_pCallResultBuffer; + } + } + + internal static void Register(Callback cb) { int iCallback = CallbackIdentities.GetCallbackIdentity(cb.GetCallbackType()); var callbacksRegistry = cb.IsGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; lock (m_sync) { @@ -160,18 +206,40 @@ internal static void RunFrame(bool isGameServer) { NativeMethods.SteamAPI_ManualDispatch_RunFrame(hSteamPipe); var callbacksRegistry = isGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; while (NativeMethods.SteamAPI_ManualDispatch_GetNextCallback(hSteamPipe, m_pCallbackMsg)) { +#if NET5_0_OR_GREATER + // Do not modify the fields inside, or will violate some .NET runtime constraint! + ref CallbackMsg_t callbackMsg = ref Unsafe.Unbox(Marshal.PtrToStructure(m_pCallbackMsg, typeof(CallbackMsg_t))); +#else CallbackMsg_t callbackMsg = (CallbackMsg_t)Marshal.PtrToStructure(m_pCallbackMsg, typeof(CallbackMsg_t)); +#endif try { // Check for dispatching API call results if (callbackMsg.m_iCallback == SteamAPICallCompleted_t.k_iCallback) { +#if NET5_0_OR_GREATER + // Same as above! + ref SteamAPICallCompleted_t callCompletedCb = ref Unsafe.Unbox( + Marshal.PtrToStructure(callbackMsg.m_pubParam, typeof(SteamAPICallCompleted_t)) + ); +#else SteamAPICallCompleted_t callCompletedCb = (SteamAPICallCompleted_t)Marshal.PtrToStructure(callbackMsg.m_pubParam, typeof(SteamAPICallCompleted_t)); - IntPtr pTmpCallResult = Marshal.AllocHGlobal((int)callCompletedCb.m_cubParam); - bool bFailed; - if (NativeMethods.SteamAPI_ManualDispatch_GetAPICallResult(hSteamPipe, callCompletedCb.m_hAsyncCall, pTmpCallResult, (int)callCompletedCb.m_cubParam, callCompletedCb.m_iCallback, out bFailed)) { +#endif + // same threading assumption as CallbackMsg_t: only one thread will call RunFrame at same time + // so reuse cached buffer directly + IntPtr pTmpCallResult = CheckCallResultBufferSize(callCompletedCb.m_cubParam); + bool bFailed; + + if (NativeMethods.SteamAPI_ManualDispatch_GetAPICallResult( + hSteamPipe, callCompletedCb.m_hAsyncCall, pTmpCallResult, + (int)callCompletedCb.m_cubParam, callCompletedCb.m_iCallback, + out bFailed)) { lock (m_sync) { List callResults; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_0_OR_GREATER + if (m_registeredCallResults.Remove(callCompletedCb.m_hAsyncCall.m_SteamAPICall, out callResults)) { +#else // compatibility to old Unity and .NET Framework project if (m_registeredCallResults.TryGetValue((ulong)callCompletedCb.m_hAsyncCall, out callResults)) { - m_registeredCallResults.Remove((ulong)callCompletedCb.m_hAsyncCall); + m_registeredCallResults.Remove((ulong)callCompletedCb.m_hAsyncCall); +#endif foreach (var cr in callResults) { cr.OnRunCallResult(pTmpCallResult, bFailed, (ulong)callCompletedCb.m_hAsyncCall); cr.SetUnregistered(); @@ -179,7 +247,6 @@ internal static void RunFrame(bool isGameServer) { } } } - Marshal.FreeHGlobal(pTmpCallResult); } else { List callbacksCopy = null; lock (m_sync) { @@ -203,10 +270,21 @@ internal static void RunFrame(bool isGameServer) { } } - public abstract class Callback { + /// + /// Internals of Steamworks.NET, not meant to use directly + /// + // Akarinnnn: I think the reason of this type is not interface, is historical burden + public abstract class Callback { public abstract bool IsGameServer { get; } internal abstract Type GetCallbackType(); - internal abstract void OnRunCallback(IntPtr pvParam); + /// + /// + /// Some changes made to dispatcher leads only valid during invocation + /// + /// + /// Result struct buffer that valid while invocation, + /// must use to retrieve before return + internal abstract void OnRunCallback(IntPtr pvParam); internal abstract void SetUnregistered(); } @@ -288,7 +366,8 @@ internal override Type GetCallbackType() { return typeof(T); } - internal override void OnRunCallback(IntPtr pvParam) { + /// + internal override void OnRunCallback(IntPtr pvParam) { try { m_Func((T)Marshal.PtrToStructure(pvParam, typeof(T))); } @@ -302,9 +381,19 @@ internal override void SetUnregistered() { } } + /// + /// Internals of Steamworks.NET, not meant to use directly + /// public abstract class CallResult { internal abstract Type GetCallbackType(); - internal abstract void OnRunCallResult(IntPtr pvParam, bool bFailed, ulong hSteamAPICall); + /// + /// + /// Some changes made to dispatcher leads only valid during invocation + /// + /// + /// Result struct buffer that valid while invocation, + /// must use to retrieve before return + internal abstract void OnRunCallResult(IntPtr pvParam, bool bFailed, ulong hSteamAPICall); internal abstract void SetUnregistered(); } @@ -381,6 +470,7 @@ internal override Type GetCallbackType() { return typeof(T); } + /// internal override void OnRunCallResult(IntPtr pvParam, bool bFailed, ulong hSteamAPICall_) { SteamAPICall_t hSteamAPICall = (SteamAPICall_t)hSteamAPICall_; if (hSteamAPICall == m_hAPICall) { From 2cc4bbb204527671d0234f4634953c12f6e02be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=E9=B8=BD?= <43724908+Akarinnnnn@users.noreply.github.com> Date: Wed, 5 Feb 2025 01:06:12 +0800 Subject: [PATCH 2/6] Fixed an HGlobal leak in CallbackDispatcher. Leaked HGlobal is allocated by member initialization statement, removed it. --- com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs index acececef..16a1e6ba 100644 --- a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs +++ b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs @@ -51,7 +51,7 @@ public static void ExceptionHandler(Exception e) { private static int m_initCount; // 4096 * 3 is enough for most api calls, private static int s_currentCallResultBufferSize = 4096 * 3; - private static IntPtr s_pCallResultBuffer = Marshal.AllocHGlobal(s_currentCallResultBufferSize); + private static IntPtr s_pCallResultBuffer; public static bool IsInitialized { get { return m_initCount > 0; } From 53a97136fb70588d6acfd611ed8474bc9fc017dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=E9=B8=BD?= <43724908+Akarinnnnn@users.noreply.github.com> Date: Fri, 7 Feb 2025 00:20:48 +0800 Subject: [PATCH 3/6] Encapsule shared buffer into a class to handle threading stuff --- .../Runtime/CallbackDispatcher.cs | 945 ++++++++++-------- 1 file changed, 532 insertions(+), 413 deletions(-) diff --git a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs index 16a1e6ba..f830e569 100644 --- a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs +++ b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs @@ -12,271 +12,390 @@ #if !DISABLESTEAMWORKS #if UNITY_3_5 || UNITY_4_0 || UNITY_4_1 || UNITY_4_2 || UNITY_4_3 || UNITY_4_5 || UNITY_4_6 - #error Unsupported Unity platform. Steamworks.NET requires Unity 4.7 or higher. + #error Unsupported Unity platform. Steamworks.NET requires Unity 4.7 or higher. #elif UNITY_4_7 || UNITY_5 || UNITY_2017 || UNITY_2017_1_OR_NEWER - #if UNITY_EDITOR_WIN || (UNITY_STANDALONE_WIN && !UNITY_EDITOR) - #define WINDOWS_BUILD - #endif + #if UNITY_EDITOR_WIN || (UNITY_STANDALONE_WIN && !UNITY_EDITOR) + #define WINDOWS_BUILD + #endif #elif STEAMWORKS_WIN - #define WINDOWS_BUILD + #define WINDOWS_BUILD #elif STEAMWORKS_LIN_OSX - // So that we don't enter the else block below. + // So that we don't enter the else block below. #else - #error You need to define STEAMWORKS_WIN, or STEAMWORKS_LIN_OSX. Refer to the readme for more details. + #error You need to define STEAMWORKS_WIN, or STEAMWORKS_LIN_OSX. Refer to the readme for more details. #endif using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; namespace Steamworks { - public static class CallbackDispatcher { - // We catch exceptions inside callbacks and reroute them here. - // For some reason throwing an exception causes RunCallbacks() to break otherwise. - // If you have a custom ExceptionHandler in your engine you can register it here manually until we get something more elegant hooked up. - public static void ExceptionHandler(Exception e) { + public static class CallbackDispatcher { + // We catch exceptions inside callbacks and reroute them here. + // For some reason throwing an exception causes RunCallbacks() to break otherwise. + // If you have a custom ExceptionHandler in your engine you can register it here manually until we get something more elegant hooked up. + public static void ExceptionHandler(Exception e) { #if UNITY_STANDALONE - UnityEngine.Debug.LogException(e); + UnityEngine.Debug.LogException(e); #elif STEAMWORKS_WIN || STEAMWORKS_LIN_OSX - Console.WriteLine(e.Message); + Console.WriteLine(e.Message); #endif - } - - private static Dictionary> m_registeredCallbacks = new Dictionary>(); - private static Dictionary> m_registeredGameServerCallbacks = new Dictionary>(); - private static Dictionary> m_registeredCallResults = new Dictionary>(); - private static object m_sync = new object(); - private static IntPtr m_pCallbackMsg; - private static int m_initCount; - // 4096 * 3 is enough for most api calls, - private static int s_currentCallResultBufferSize = 4096 * 3; - private static IntPtr s_pCallResultBuffer; - - public static bool IsInitialized { - get { return m_initCount > 0; } - } - - internal static void Initialize() { - lock (m_sync) { - if (m_initCount == 0) { - NativeMethods.SteamAPI_ManualDispatch_Init(); - m_pCallbackMsg = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(CallbackMsg_t))); - } - ++m_initCount; - } - } - - internal static void Shutdown() { - lock (m_sync) { - --m_initCount; - if (m_initCount == 0) { - UnregisterAll(); - Marshal.FreeHGlobal(m_pCallbackMsg); - Marshal.FreeHGlobal(s_pCallResultBuffer); - m_pCallbackMsg = IntPtr.Zero; - s_pCallResultBuffer = IntPtr.Zero; - } - } - } - - private static IntPtr CheckCallResultBufferSize(uint requiredBufferSize) - { - if (s_currentCallResultBufferSize >= requiredBufferSize) - return s_pCallResultBuffer; // buffer is enough, this happens mostly + } - // have to resize buffer - lock (m_sync) - { - // double check buffer size to avoid resize the buffer smaller - if (s_currentCallResultBufferSize < requiredBufferSize) - return s_pCallResultBuffer; - - // round buffer size to next multiple of 4096 -#if NET6_0_OR_GREATER // decided to use NET6_0 here is for Unity builds, same as 5 below - // System.Numerics is not always available for using - uint newBufferSize = System.Numerics.BitOperations.RoundUpToPowerOf2(requiredBufferSize); + private static Dictionary> m_registeredCallbacks = new Dictionary>(); + private static Dictionary> m_registeredGameServerCallbacks = new Dictionary>(); + private static Dictionary> m_registeredCallResults = new Dictionary>(); + private static object m_sync = new object(); + private static IntPtr m_pCallbackMsg; + private static int m_initCount; + [ThreadStatic] +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER // C# 8 Nullable annotation +#nullable enable + private static CallResultBuffer? s_callResultBuffer; +#nullable restore #else - uint newBufferSize = requiredBufferSize; - if ((newBufferSize & 0x1FFF) != 4096) - newBufferSize = (newBufferSize + 4095) & 0xFFFFF000; + private static /* nullable */ CallResultBuffer s_callResultBuffer; #endif - if (newBufferSize > int.MaxValue) { - newBufferSize = requiredBufferSize; - // not to use enlarged size since we don't have enough space - if (newBufferSize > int.MaxValue) { - throw new NotSupportedException("The param size of a call result is larger than 2GiB"); + public static bool IsInitialized { + get { return m_initCount > 0; } + } + + internal static void Initialize() { + lock (m_sync) { + if (m_initCount == 0) { + NativeMethods.SteamAPI_ManualDispatch_Init(); + m_pCallbackMsg = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(CallbackMsg_t))); + } + ++m_initCount; + } + } + + internal static void Shutdown() { + lock (m_sync) { + --m_initCount; + if (m_initCount == 0) { + UnregisterAll(); + Marshal.FreeHGlobal(m_pCallbackMsg); + s_callResultBuffer?.Dispose(); + s_callResultBuffer = null; + m_pCallbackMsg = IntPtr.Zero; + } + } + } + + private sealed class CallResultBuffer : IDisposable + { + private const int DefaultBufferSize = 4096 * 3; + private const int TooLargeSizeThreshold = (int)(DefaultBufferSize * 1.2); + // shrink if buffer too large counter reached this amount: + private const int ShrinkBufferThreshold = 330; + + private volatile int bufferTooLargeCounter = 0; + + private int currentCallResultBufferSize; + private IntPtr pCallResultBuffer; + + private bool disposedValue; + + /// + /// Acquire buffer. See exception details before use. + /// + /// Exact desired buffer size for receiving result. + /// Unmanaged buffer for receiving result + /// Route to + /// Recreate an new instance if thrown + /// Recreate an new instance if thrown + public IntPtr AcquireBuffer(uint requiredBufferSize) + { + CheckIsDisposed(); // also checks if this reference is null + + if (currentCallResultBufferSize >= requiredBufferSize) + { + // buffer is enough, this case will happen mostly + + // check is there a large struct incoming + if (requiredBufferSize >= TooLargeSizeThreshold) + { + // yes and guess we will have some big structs in near future + // keep big buffer now and reset counter + + // thread-safe set + var currentCounterValue = bufferTooLargeCounter; + while (Interlocked.CompareExchange(ref bufferTooLargeCounter, 0, currentCounterValue) != currentCounterValue) + { + // this thread failed the race, try again + currentCounterValue = bufferTooLargeCounter; + } + + return pCallResultBuffer; + } + // check counter to see should we shrink, do thread-safe get + else if (Interlocked.Increment(ref bufferTooLargeCounter) < ShrinkBufferThreshold) + { + return pCallResultBuffer; } } + // have to resize buffer + lock (this) + { + CheckIsDisposed(); + // double check buffer size to avoid resize the buffer smaller + if (currentCallResultBufferSize < requiredBufferSize) { + return pCallResultBuffer; + } + + requiredBufferSize = Math.Min(requiredBufferSize, DefaultBufferSize); + + // round buffer size to next multiple of 4096 + uint newBufferSize = requiredBufferSize; + if ((newBufferSize & 0x1FFF) != 4096) + newBufferSize = (newBufferSize + 4095) & 0xFFFFF000; + + if (newBufferSize > int.MaxValue) + { + // not to use enlarged size since we don't have enough space + newBufferSize = requiredBufferSize; + if (newBufferSize > int.MaxValue) + { + // this exception should route to ExceptionHandler() + throw new NotSupportedException("The param size of a call result is larger than 2GiB"); + } + } + #if NET5_0_OR_GREATER - s_pCallResultBuffer = Marshal.ReAllocHGlobal(s_pCallResultBuffer, (nint)newBufferSize); + pCallResultBuffer = Marshal.ReAllocHGlobal(pCallResultBuffer, (nint)newBufferSize); #else - Marshal.FreeHGlobal(s_pCallResultBuffer); - s_pCallResultBuffer = IntPtr.Zero; // is this necessary? - s_pCallResultBuffer = Marshal.AllocHGlobal((int)newBufferSize); + Marshal.FreeHGlobal(pCallResultBuffer); + pCallResultBuffer = IntPtr.Zero; // is this necessary? + pCallResultBuffer = Marshal.AllocHGlobal((int)newBufferSize); #endif - return s_pCallResultBuffer; + return pCallResultBuffer; + } + } + + + private void CheckIsDisposed() + { + if (disposedValue) + { + throw new ObjectDisposedException(GetType().FullName, "Attempt to use a released call-result buffer."); + } + } + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + lock (this) + { + { + Marshal.FreeHGlobal(pCallResultBuffer); + disposedValue = true; + } + } + } + } + + ~CallResultBuffer() + { + Dispose(disposing: false); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); } } + internal static void Register(Callback cb) { - int iCallback = CallbackIdentities.GetCallbackIdentity(cb.GetCallbackType()); - var callbacksRegistry = cb.IsGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; - lock (m_sync) { - List callbacksList; - if (!callbacksRegistry.TryGetValue(iCallback, out callbacksList)) { - callbacksList = new List(); - callbacksRegistry.Add(iCallback, callbacksList); - } - - callbacksList.Add(cb); - } - } - - internal static void Register(SteamAPICall_t asyncCall, CallResult cr) { - lock (m_sync) { - List callResultsList; - if (!m_registeredCallResults.TryGetValue((ulong)asyncCall, out callResultsList)) { - callResultsList = new List(); - m_registeredCallResults.Add((ulong)asyncCall, callResultsList); - } - - callResultsList.Add(cr); - } - } - - internal static void Unregister(Callback cb) { - int iCallback = CallbackIdentities.GetCallbackIdentity(cb.GetCallbackType()); - var callbacksRegistry = cb.IsGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; - lock (m_sync) { - List callbacksList; - if (callbacksRegistry.TryGetValue(iCallback, out callbacksList)) { - callbacksList.Remove(cb); - if (callbacksList.Count == 0) - callbacksRegistry.Remove(iCallback); - } - } - } - - internal static void Unregister(SteamAPICall_t asyncCall, CallResult cr) { - lock (m_sync) { - List callResultsList; - if (m_registeredCallResults.TryGetValue((ulong)asyncCall, out callResultsList)) { - callResultsList.Remove(cr); - if (callResultsList.Count == 0) - m_registeredCallResults.Remove((ulong)asyncCall); - } - } - } - - private static void UnregisterAll() { - List callbacks = new List(); - List callResults = new List(); - lock (m_sync) { - foreach (var pair in m_registeredCallbacks) { - callbacks.AddRange(pair.Value); - } - m_registeredCallbacks.Clear(); - - foreach (var pair in m_registeredGameServerCallbacks) { - callbacks.AddRange(pair.Value); - } - m_registeredGameServerCallbacks.Clear(); - - foreach (var pair in m_registeredCallResults) { - callResults.AddRange(pair.Value); - } - m_registeredCallResults.Clear(); - - foreach (var callback in callbacks) { - callback.SetUnregistered(); - } - - foreach (var callResult in callResults) { - callResult.SetUnregistered(); - } - } - } - - internal static void RunFrame(bool isGameServer) { - if (!IsInitialized) throw new InvalidOperationException("Callback dispatcher is not initialized."); - - HSteamPipe hSteamPipe = (HSteamPipe)(isGameServer ? NativeMethods.SteamGameServer_GetHSteamPipe() : NativeMethods.SteamAPI_GetHSteamPipe()); - NativeMethods.SteamAPI_ManualDispatch_RunFrame(hSteamPipe); - var callbacksRegistry = isGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; - while (NativeMethods.SteamAPI_ManualDispatch_GetNextCallback(hSteamPipe, m_pCallbackMsg)) { + int iCallback = CallbackIdentities.GetCallbackIdentity(cb.GetCallbackType()); + var callbacksRegistry = cb.IsGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; + lock (m_sync) { + List callbacksList; + if (!callbacksRegistry.TryGetValue(iCallback, out callbacksList)) { + callbacksList = new List(); + callbacksRegistry.Add(iCallback, callbacksList); + } + + callbacksList.Add(cb); + } + } + + internal static void Register(SteamAPICall_t asyncCall, CallResult cr) { + lock (m_sync) { + List callResultsList; + if (!m_registeredCallResults.TryGetValue((ulong)asyncCall, out callResultsList)) { + callResultsList = new List(); + m_registeredCallResults.Add((ulong)asyncCall, callResultsList); + } + + callResultsList.Add(cr); + } + } + + internal static void Unregister(Callback cb) { + int iCallback = CallbackIdentities.GetCallbackIdentity(cb.GetCallbackType()); + var callbacksRegistry = cb.IsGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; + lock (m_sync) { + List callbacksList; + if (callbacksRegistry.TryGetValue(iCallback, out callbacksList)) { + callbacksList.Remove(cb); + if (callbacksList.Count == 0) + callbacksRegistry.Remove(iCallback); + } + } + } + + internal static void Unregister(SteamAPICall_t asyncCall, CallResult cr) { + lock (m_sync) { + List callResultsList; + if (m_registeredCallResults.TryGetValue((ulong)asyncCall, out callResultsList)) { + callResultsList.Remove(cr); + if (callResultsList.Count == 0) + m_registeredCallResults.Remove((ulong)asyncCall); + } + } + } + + private static void UnregisterAll() { + List callbacks = new List(); + List callResults = new List(); + lock (m_sync) { + foreach (var pair in m_registeredCallbacks) { + callbacks.AddRange(pair.Value); + } + m_registeredCallbacks.Clear(); + + foreach (var pair in m_registeredGameServerCallbacks) { + callbacks.AddRange(pair.Value); + } + m_registeredGameServerCallbacks.Clear(); + + foreach (var pair in m_registeredCallResults) { + callResults.AddRange(pair.Value); + } + m_registeredCallResults.Clear(); + + foreach (var callback in callbacks) { + callback.SetUnregistered(); + } + + foreach (var callResult in callResults) { + callResult.SetUnregistered(); + } + } + } + + internal static void RunFrame(bool isGameServer) { + if (!IsInitialized) throw new InvalidOperationException("Callback dispatcher is not initialized."); + + HSteamPipe hSteamPipe = (HSteamPipe)(isGameServer ? NativeMethods.SteamGameServer_GetHSteamPipe() : NativeMethods.SteamAPI_GetHSteamPipe()); + NativeMethods.SteamAPI_ManualDispatch_RunFrame(hSteamPipe); + var callbacksRegistry = isGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; + while (NativeMethods.SteamAPI_ManualDispatch_GetNextCallback(hSteamPipe, m_pCallbackMsg)) { #if NET5_0_OR_GREATER - // Do not modify the fields inside, or will violate some .NET runtime constraint! - ref CallbackMsg_t callbackMsg = ref Unsafe.Unbox(Marshal.PtrToStructure(m_pCallbackMsg, typeof(CallbackMsg_t))); + // Do not modify the fields inside, or will violate some .NET runtime constraint! + ref CallbackMsg_t callbackMsg = ref Unsafe.Unbox(Marshal.PtrToStructure(m_pCallbackMsg, typeof(CallbackMsg_t))); #else - CallbackMsg_t callbackMsg = (CallbackMsg_t)Marshal.PtrToStructure(m_pCallbackMsg, typeof(CallbackMsg_t)); + CallbackMsg_t callbackMsg = (CallbackMsg_t)Marshal.PtrToStructure(m_pCallbackMsg, typeof(CallbackMsg_t)); #endif - try { - // Check for dispatching API call results - if (callbackMsg.m_iCallback == SteamAPICallCompleted_t.k_iCallback) { + try { + // Check for dispatching API call results + if (callbackMsg.m_iCallback == SteamAPICallCompleted_t.k_iCallback) { #if NET5_0_OR_GREATER - // Same as above! - ref SteamAPICallCompleted_t callCompletedCb = ref Unsafe.Unbox( - Marshal.PtrToStructure(callbackMsg.m_pubParam, typeof(SteamAPICallCompleted_t)) - ); + // Same as above! + ref SteamAPICallCompleted_t callCompletedCb = ref Unsafe.Unbox( + Marshal.PtrToStructure(callbackMsg.m_pubParam, typeof(SteamAPICallCompleted_t)) + ); #else - SteamAPICallCompleted_t callCompletedCb = (SteamAPICallCompleted_t)Marshal.PtrToStructure(callbackMsg.m_pubParam, typeof(SteamAPICallCompleted_t)); + SteamAPICallCompleted_t callCompletedCb = (SteamAPICallCompleted_t)Marshal.PtrToStructure(callbackMsg.m_pubParam, typeof(SteamAPICallCompleted_t)); #endif - // same threading assumption as CallbackMsg_t: only one thread will call RunFrame at same time - // so reuse cached buffer directly - IntPtr pTmpCallResult = CheckCallResultBufferSize(callCompletedCb.m_cubParam); + // threading safe issues in allocating call-result buffer is handled by AcquireBuffer() + IntPtr pTmpCallResult; + CallResultBuffer bufferHolder = s_callResultBuffer; + try { + // In most cases s_callResultBuffer will have valid value, + // by moving rare cases(recreate buffer holder) to exception path, + // should avoid generating creation code into usage branch + // and keep usage branch clear. + pTmpCallResult = bufferHolder.AcquireBuffer(callCompletedCb.m_cubParam); + } + catch (NotSupportedException ex) { + ExceptionHandler(ex); + continue; + } catch (ObjectDisposedException) { + var bufferHolderNew = new CallResultBuffer(); + pTmpCallResult = bufferHolder.AcquireBuffer(callCompletedCb.m_cubParam); + + // try set shared buffer to newly created one, accept race failure + Interlocked.CompareExchange(ref s_callResultBuffer, bufferHolderNew, bufferHolder); + // avoid new instance from being gc collected + bufferHolder = bufferHolderNew; + } catch (NullReferenceException) { + // keep same as above + var bufferHolderNew = new CallResultBuffer(); + pTmpCallResult = bufferHolder.AcquireBuffer(callCompletedCb.m_cubParam); + + Interlocked.CompareExchange(ref s_callResultBuffer, bufferHolderNew, bufferHolder); + bufferHolder = bufferHolderNew; + } + bool bFailed; - if (NativeMethods.SteamAPI_ManualDispatch_GetAPICallResult( - hSteamPipe, callCompletedCb.m_hAsyncCall, pTmpCallResult, - (int)callCompletedCb.m_cubParam, callCompletedCb.m_iCallback, - out bFailed)) { - lock (m_sync) { - List callResults; + if (NativeMethods.SteamAPI_ManualDispatch_GetAPICallResult( + hSteamPipe, callCompletedCb.m_hAsyncCall, pTmpCallResult, + (int)callCompletedCb.m_cubParam, callCompletedCb.m_iCallback, + out bFailed)) { + lock (m_sync) { + List callResults; #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_0_OR_GREATER - if (m_registeredCallResults.Remove(callCompletedCb.m_hAsyncCall.m_SteamAPICall, out callResults)) { + if (m_registeredCallResults.Remove(callCompletedCb.m_hAsyncCall.m_SteamAPICall, out callResults)) { #else // compatibility to old Unity and .NET Framework project - if (m_registeredCallResults.TryGetValue((ulong)callCompletedCb.m_hAsyncCall, out callResults)) { - m_registeredCallResults.Remove((ulong)callCompletedCb.m_hAsyncCall); + if (m_registeredCallResults.TryGetValue((ulong)callCompletedCb.m_hAsyncCall, out callResults)) { + m_registeredCallResults.Remove((ulong)callCompletedCb.m_hAsyncCall); #endif - foreach (var cr in callResults) { - cr.OnRunCallResult(pTmpCallResult, bFailed, (ulong)callCompletedCb.m_hAsyncCall); - cr.SetUnregistered(); - } - } - } - } - } else { - List callbacksCopy = null; - lock (m_sync) { - List callbacks = null; - if (callbacksRegistry.TryGetValue(callbackMsg.m_iCallback, out callbacks)) { - callbacksCopy = new List(callbacks); - } - } - if (callbacksCopy != null) { - foreach (var callback in callbacksCopy) { - callback.OnRunCallback(callbackMsg.m_pubParam); - } - } - } - } catch (Exception e) { - ExceptionHandler(e); - } finally { - NativeMethods.SteamAPI_ManualDispatch_FreeLastCallback(hSteamPipe); - } - } - } - } + foreach (var cr in callResults) { + cr.OnRunCallResult(pTmpCallResult, bFailed, (ulong)callCompletedCb.m_hAsyncCall); + cr.SetUnregistered(); + } + } + } + } + } else { + List callbacksCopy = null; + lock (m_sync) { + List callbacks = null; + if (callbacksRegistry.TryGetValue(callbackMsg.m_iCallback, out callbacks)) { + callbacksCopy = new List(callbacks); + } + } + if (callbacksCopy != null) { + foreach (var callback in callbacksCopy) { + callback.OnRunCallback(callbackMsg.m_pubParam); + } + } + } + } catch (Exception e) { + ExceptionHandler(e); + } finally { + NativeMethods.SteamAPI_ManualDispatch_FreeLastCallback(hSteamPipe); + } + } + } + } /// /// Internals of Steamworks.NET, not meant to use directly /// - // Akarinnnn: I think the reason of this type is not interface, is historical burden + // Akarinnnn: I think the reason of this type is not interface, is historical burden public abstract class Callback { - public abstract bool IsGameServer { get; } - internal abstract Type GetCallbackType(); + public abstract bool IsGameServer { get; } + internal abstract Type GetCallbackType(); /// /// /// Some changes made to dispatcher leads only valid during invocation @@ -285,208 +404,208 @@ public abstract class Callback { /// Result struct buffer that valid while invocation, /// must use to retrieve before return internal abstract void OnRunCallback(IntPtr pvParam); - internal abstract void SetUnregistered(); - } - - public sealed class Callback : Callback, IDisposable { - public delegate void DispatchDelegate(T param); - private event DispatchDelegate m_Func; - - private bool m_bGameServer; - private bool m_bIsRegistered; - - private bool m_bDisposed = false; - - /// - /// Creates a new Callback. You must be calling SteamAPI.RunCallbacks() to retrieve the callbacks. - /// Returns a handle to the Callback. - /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. - /// - public static Callback Create(DispatchDelegate func) { - return new Callback(func, bGameServer: false); - } - - /// - /// Creates a new GameServer Callback. You must be calling GameServer.RunCallbacks() to retrieve the callbacks. - /// Returns a handle to the Callback. - /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. - /// - public static Callback CreateGameServer(DispatchDelegate func) { - return new Callback(func, bGameServer: true); - } - - public Callback(DispatchDelegate func, bool bGameServer = false) { - m_bGameServer = bGameServer; - Register(func); - } - - ~Callback() { - Dispose(); - } - - public void Dispose() { - if (m_bDisposed) { - return; - } - - GC.SuppressFinalize(this); - - if (m_bIsRegistered) - Unregister(); - - m_bDisposed = true; - } - - // Manual registration of the callback - public void Register(DispatchDelegate func) { - if (func == null) { - throw new Exception("Callback function must not be null."); - } - - if (m_bIsRegistered) { - Unregister(); - } - - m_Func = func; - - CallbackDispatcher.Register(this); - m_bIsRegistered = true; - } - - public void Unregister() { - CallbackDispatcher.Unregister(this); - m_bIsRegistered = false; - } - - public override bool IsGameServer { - get { return m_bGameServer; } - } - - internal override Type GetCallbackType() { - return typeof(T); - } + internal abstract void SetUnregistered(); + } + + public sealed class Callback : Callback, IDisposable { + public delegate void DispatchDelegate(T param); + private event DispatchDelegate m_Func; + + private bool m_bGameServer; + private bool m_bIsRegistered; + + private bool m_bDisposed = false; + + /// + /// Creates a new Callback. You must be calling SteamAPI.RunCallbacks() to retrieve the callbacks. + /// Returns a handle to the Callback. + /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. + /// + public static Callback Create(DispatchDelegate func) { + return new Callback(func, bGameServer: false); + } + + /// + /// Creates a new GameServer Callback. You must be calling GameServer.RunCallbacks() to retrieve the callbacks. + /// Returns a handle to the Callback. + /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. + /// + public static Callback CreateGameServer(DispatchDelegate func) { + return new Callback(func, bGameServer: true); + } + + public Callback(DispatchDelegate func, bool bGameServer = false) { + m_bGameServer = bGameServer; + Register(func); + } + + ~Callback() { + Dispose(); + } + + public void Dispose() { + if (m_bDisposed) { + return; + } + + GC.SuppressFinalize(this); + + if (m_bIsRegistered) + Unregister(); + + m_bDisposed = true; + } + + // Manual registration of the callback + public void Register(DispatchDelegate func) { + if (func == null) { + throw new Exception("Callback function must not be null."); + } + + if (m_bIsRegistered) { + Unregister(); + } + + m_Func = func; + + CallbackDispatcher.Register(this); + m_bIsRegistered = true; + } + + public void Unregister() { + CallbackDispatcher.Unregister(this); + m_bIsRegistered = false; + } + + public override bool IsGameServer { + get { return m_bGameServer; } + } + + internal override Type GetCallbackType() { + return typeof(T); + } /// internal override void OnRunCallback(IntPtr pvParam) { - try { - m_Func((T)Marshal.PtrToStructure(pvParam, typeof(T))); - } - catch (Exception e) { - CallbackDispatcher.ExceptionHandler(e); - } - } - - internal override void SetUnregistered() { - m_bIsRegistered = false; - } - } - - /// - /// Internals of Steamworks.NET, not meant to use directly - /// - public abstract class CallResult { - internal abstract Type GetCallbackType(); + try { + m_Func((T)Marshal.PtrToStructure(pvParam, typeof(T))); + } + catch (Exception e) { + CallbackDispatcher.ExceptionHandler(e); + } + } + + internal override void SetUnregistered() { + m_bIsRegistered = false; + } + } + + /// + /// Internals of Steamworks.NET, not meant to use directly + /// + public abstract class CallResult { + internal abstract Type GetCallbackType(); /// /// /// Some changes made to dispatcher leads only valid during invocation /// /// /// Result struct buffer that valid while invocation, - /// must use to retrieve before return + /// must use to retrieve before return internal abstract void OnRunCallResult(IntPtr pvParam, bool bFailed, ulong hSteamAPICall); - internal abstract void SetUnregistered(); - } - - public sealed class CallResult : CallResult, IDisposable { - public delegate void APIDispatchDelegate(T param, bool bIOFailure); - private event APIDispatchDelegate m_Func; - - private SteamAPICall_t m_hAPICall = SteamAPICall_t.Invalid; - public SteamAPICall_t Handle { get { return m_hAPICall; } } - - private bool m_bDisposed = false; - - /// - /// Creates a new async CallResult. You must be calling SteamAPI.RunCallbacks() to retrieve the callback. - /// Returns a handle to the CallResult. - /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. - /// - public static CallResult Create(APIDispatchDelegate func = null) { - return new CallResult(func); - } - - public CallResult(APIDispatchDelegate func = null) { - m_Func = func; - } - - ~CallResult() { - Dispose(); - } - - public void Dispose() { - if (m_bDisposed) { - return; - } - - GC.SuppressFinalize(this); - - Cancel(); - - m_bDisposed = true; - } - - public void Set(SteamAPICall_t hAPICall, APIDispatchDelegate func = null) { - // Unlike the official SDK we let the user assign a single function during creation, - // and allow them to skip having to do so every time that they call .Set() - if (func != null) { - m_Func = func; - } - - if (m_Func == null) { - throw new Exception("CallResult function was null, you must either set it in the CallResult Constructor or via Set()"); - } - - if (m_hAPICall != SteamAPICall_t.Invalid) { - CallbackDispatcher.Unregister(m_hAPICall, this); - } - - m_hAPICall = hAPICall; - - if (hAPICall != SteamAPICall_t.Invalid) { - CallbackDispatcher.Register(hAPICall, this); - } - } - - public bool IsActive() { - return (m_hAPICall != SteamAPICall_t.Invalid); - } - - public void Cancel() { - if (IsActive()) - CallbackDispatcher.Unregister(m_hAPICall, this); - } - - internal override Type GetCallbackType() { - return typeof(T); - } - - /// - internal override void OnRunCallResult(IntPtr pvParam, bool bFailed, ulong hSteamAPICall_) { - SteamAPICall_t hSteamAPICall = (SteamAPICall_t)hSteamAPICall_; - if (hSteamAPICall == m_hAPICall) { - try { - m_Func((T)Marshal.PtrToStructure(pvParam, typeof(T)), bFailed); - } - catch (Exception e) { - CallbackDispatcher.ExceptionHandler(e); - } - } - } - - internal override void SetUnregistered() { - m_hAPICall = SteamAPICall_t.Invalid; - } - } + internal abstract void SetUnregistered(); + } + + public sealed class CallResult : CallResult, IDisposable { + public delegate void APIDispatchDelegate(T param, bool bIOFailure); + private event APIDispatchDelegate m_Func; + + private SteamAPICall_t m_hAPICall = SteamAPICall_t.Invalid; + public SteamAPICall_t Handle { get { return m_hAPICall; } } + + private bool m_bDisposed = false; + + /// + /// Creates a new async CallResult. You must be calling SteamAPI.RunCallbacks() to retrieve the callback. + /// Returns a handle to the CallResult. + /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. + /// + public static CallResult Create(APIDispatchDelegate func = null) { + return new CallResult(func); + } + + public CallResult(APIDispatchDelegate func = null) { + m_Func = func; + } + + ~CallResult() { + Dispose(); + } + + public void Dispose() { + if (m_bDisposed) { + return; + } + + GC.SuppressFinalize(this); + + Cancel(); + + m_bDisposed = true; + } + + public void Set(SteamAPICall_t hAPICall, APIDispatchDelegate func = null) { + // Unlike the official SDK we let the user assign a single function during creation, + // and allow them to skip having to do so every time that they call .Set() + if (func != null) { + m_Func = func; + } + + if (m_Func == null) { + throw new Exception("CallResult function was null, you must either set it in the CallResult Constructor or via Set()"); + } + + if (m_hAPICall != SteamAPICall_t.Invalid) { + CallbackDispatcher.Unregister(m_hAPICall, this); + } + + m_hAPICall = hAPICall; + + if (hAPICall != SteamAPICall_t.Invalid) { + CallbackDispatcher.Register(hAPICall, this); + } + } + + public bool IsActive() { + return (m_hAPICall != SteamAPICall_t.Invalid); + } + + public void Cancel() { + if (IsActive()) + CallbackDispatcher.Unregister(m_hAPICall, this); + } + + internal override Type GetCallbackType() { + return typeof(T); + } + + /// + internal override void OnRunCallResult(IntPtr pvParam, bool bFailed, ulong hSteamAPICall_) { + SteamAPICall_t hSteamAPICall = (SteamAPICall_t)hSteamAPICall_; + if (hSteamAPICall == m_hAPICall) { + try { + m_Func((T)Marshal.PtrToStructure(pvParam, typeof(T)), bFailed); + } + catch (Exception e) { + CallbackDispatcher.ExceptionHandler(e); + } + } + } + + internal override void SetUnregistered() { + m_hAPICall = SteamAPICall_t.Invalid; + } + } } #endif // !DISABLESTEAMWORKS From 5dec732031c7d8fd21d3b7f90965e2e23d644f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=E9=B8=BD?= <43724908+Akarinnnnn@users.noreply.github.com> Date: Fri, 7 Feb 2025 01:58:34 +0800 Subject: [PATCH 4/6] Fixed call-result buffer allocation logic, added a debug purpose configuration. --- Standalone/Steamworks.NET.Standard.csproj | 12 ++++++++++-- .../Runtime/CallbackDispatcher.cs | 9 +++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Standalone/Steamworks.NET.Standard.csproj b/Standalone/Steamworks.NET.Standard.csproj index 8eccf2ae..e9fa5045 100644 --- a/Standalone/Steamworks.NET.Standard.csproj +++ b/Standalone/Steamworks.NET.Standard.csproj @@ -5,9 +5,10 @@ Steamworks Steamworks.NET x64;x86 - Windows;OSX-Linux + Windows;OSX-Linux;WindowsDebug false git + low @@ -22,7 +23,7 @@ true snupkg - + bin\x86\Windows\ TRACE;STEAMWORKS_WIN;STEAMWORKS_X86 @@ -31,6 +32,13 @@ x86 prompt + + + bin\$(Platform)\$(Configuration)\ + TRACE;STEAMWORKS_WIN;STEAMWORKS_X64 + + false + bin\x86\OSX-Linux\ diff --git a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs index f830e569..eca5f1dc 100644 --- a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs +++ b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs @@ -143,11 +143,11 @@ public IntPtr AcquireBuffer(uint requiredBufferSize) { CheckIsDisposed(); // double check buffer size to avoid resize the buffer smaller - if (currentCallResultBufferSize < requiredBufferSize) { + if (currentCallResultBufferSize >= requiredBufferSize) { return pCallResultBuffer; } - requiredBufferSize = Math.Min(requiredBufferSize, DefaultBufferSize); + requiredBufferSize = Math.Max(requiredBufferSize, DefaultBufferSize); // round buffer size to next multiple of 4096 uint newBufferSize = requiredBufferSize; @@ -172,6 +172,7 @@ public IntPtr AcquireBuffer(uint requiredBufferSize) pCallResultBuffer = IntPtr.Zero; // is this necessary? pCallResultBuffer = Marshal.AllocHGlobal((int)newBufferSize); #endif + currentCallResultBufferSize = (int)newBufferSize; return pCallResultBuffer; } } @@ -330,7 +331,7 @@ internal static void RunFrame(bool isGameServer) { continue; } catch (ObjectDisposedException) { var bufferHolderNew = new CallResultBuffer(); - pTmpCallResult = bufferHolder.AcquireBuffer(callCompletedCb.m_cubParam); + pTmpCallResult = bufferHolderNew.AcquireBuffer(callCompletedCb.m_cubParam); // try set shared buffer to newly created one, accept race failure Interlocked.CompareExchange(ref s_callResultBuffer, bufferHolderNew, bufferHolder); @@ -339,7 +340,7 @@ internal static void RunFrame(bool isGameServer) { } catch (NullReferenceException) { // keep same as above var bufferHolderNew = new CallResultBuffer(); - pTmpCallResult = bufferHolder.AcquireBuffer(callCompletedCb.m_cubParam); + pTmpCallResult = bufferHolderNew.AcquireBuffer(callCompletedCb.m_cubParam); Interlocked.CompareExchange(ref s_callResultBuffer, bufferHolderNew, bufferHolder); bufferHolder = bufferHolderNew; From 9e2887f1c107ced158102528f976af480b061c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=E9=B8=BD?= <43724908+Akarinnnnn@users.noreply.github.com> Date: Sat, 8 Feb 2025 22:29:13 +0800 Subject: [PATCH 5/6] Make code style in CallbackDispatcher.cs consist. --- .../Runtime/CallbackDispatcher.cs | 57 +++++++------------ 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs index eca5f1dc..559141ce 100644 --- a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs +++ b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs @@ -85,8 +85,7 @@ internal static void Shutdown() { } } - private sealed class CallResultBuffer : IDisposable - { + private sealed class CallResultBuffer : IDisposable { private const int DefaultBufferSize = 4096 * 3; private const int TooLargeSizeThreshold = (int)(DefaultBufferSize * 1.2); // shrink if buffer too large counter reached this amount: @@ -107,24 +106,20 @@ private sealed class CallResultBuffer : IDisposable /// Route to /// Recreate an new instance if thrown /// Recreate an new instance if thrown - public IntPtr AcquireBuffer(uint requiredBufferSize) - { + public IntPtr AcquireBuffer(uint requiredBufferSize) { CheckIsDisposed(); // also checks if this reference is null - if (currentCallResultBufferSize >= requiredBufferSize) - { + if (currentCallResultBufferSize >= requiredBufferSize) { // buffer is enough, this case will happen mostly // check is there a large struct incoming - if (requiredBufferSize >= TooLargeSizeThreshold) - { + if (requiredBufferSize >= TooLargeSizeThreshold) { // yes and guess we will have some big structs in near future // keep big buffer now and reset counter // thread-safe set var currentCounterValue = bufferTooLargeCounter; - while (Interlocked.CompareExchange(ref bufferTooLargeCounter, 0, currentCounterValue) != currentCounterValue) - { + while (Interlocked.CompareExchange(ref bufferTooLargeCounter, 0, currentCounterValue) != currentCounterValue) { // this thread failed the race, try again currentCounterValue = bufferTooLargeCounter; } @@ -132,15 +127,13 @@ public IntPtr AcquireBuffer(uint requiredBufferSize) return pCallResultBuffer; } // check counter to see should we shrink, do thread-safe get - else if (Interlocked.Increment(ref bufferTooLargeCounter) < ShrinkBufferThreshold) - { + else if (Interlocked.Increment(ref bufferTooLargeCounter) < ShrinkBufferThreshold) { return pCallResultBuffer; } } // have to resize buffer - lock (this) - { + lock (this) { CheckIsDisposed(); // double check buffer size to avoid resize the buffer smaller if (currentCallResultBufferSize >= requiredBufferSize) { @@ -151,15 +144,14 @@ public IntPtr AcquireBuffer(uint requiredBufferSize) // round buffer size to next multiple of 4096 uint newBufferSize = requiredBufferSize; - if ((newBufferSize & 0x1FFF) != 4096) + if ((newBufferSize & 0x1FFF) != 4096) { newBufferSize = (newBufferSize + 4095) & 0xFFFFF000; + } - if (newBufferSize > int.MaxValue) - { + if (newBufferSize > int.MaxValue) { // not to use enlarged size since we don't have enough space newBufferSize = requiredBufferSize; - if (newBufferSize > int.MaxValue) - { + if (newBufferSize > int.MaxValue) { // this exception should route to ExceptionHandler() throw new NotSupportedException("The param size of a call result is larger than 2GiB"); } @@ -178,35 +170,26 @@ public IntPtr AcquireBuffer(uint requiredBufferSize) } - private void CheckIsDisposed() - { - if (disposedValue) - { + private void CheckIsDisposed() { + if (disposedValue) { throw new ObjectDisposedException(GetType().FullName, "Attempt to use a released call-result buffer."); } } - private void Dispose(bool disposing) - { - if (!disposedValue) - { - lock (this) - { - { - Marshal.FreeHGlobal(pCallResultBuffer); - disposedValue = true; - } + private void Dispose(bool disposing) { + if (!disposedValue) { + lock (this) { + Marshal.FreeHGlobal(pCallResultBuffer); + disposedValue = true; } } } - ~CallResultBuffer() - { + ~CallResultBuffer() { Dispose(disposing: false); } - public void Dispose() - { + public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } From 28742c5fe382164f2d6c80dc9f4e9012362a6ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=E9=B8=BD?= <43724908+Akarinnnnn@users.noreply.github.com> Date: Sun, 9 Feb 2025 02:01:53 +0800 Subject: [PATCH 6/6] Keep same indentation style as old CallbackDispatcher.cs. --- Standalone/Steamworks.NET.Standard.csproj | 2 +- .../Runtime/CallbackDispatcher.cs | 1088 ++++++++--------- 2 files changed, 545 insertions(+), 545 deletions(-) diff --git a/Standalone/Steamworks.NET.Standard.csproj b/Standalone/Steamworks.NET.Standard.csproj index e9fa5045..65f0ff04 100644 --- a/Standalone/Steamworks.NET.Standard.csproj +++ b/Standalone/Steamworks.NET.Standard.csproj @@ -23,7 +23,7 @@ true snupkg - + bin\x86\Windows\ TRACE;STEAMWORKS_WIN;STEAMWORKS_X86 diff --git a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs index 559141ce..5a4d8da1 100644 --- a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs +++ b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs @@ -12,17 +12,17 @@ #if !DISABLESTEAMWORKS #if UNITY_3_5 || UNITY_4_0 || UNITY_4_1 || UNITY_4_2 || UNITY_4_3 || UNITY_4_5 || UNITY_4_6 - #error Unsupported Unity platform. Steamworks.NET requires Unity 4.7 or higher. + #error Unsupported Unity platform. Steamworks.NET requires Unity 4.7 or higher. #elif UNITY_4_7 || UNITY_5 || UNITY_2017 || UNITY_2017_1_OR_NEWER - #if UNITY_EDITOR_WIN || (UNITY_STANDALONE_WIN && !UNITY_EDITOR) - #define WINDOWS_BUILD - #endif + #if UNITY_EDITOR_WIN || (UNITY_STANDALONE_WIN && !UNITY_EDITOR) + #define WINDOWS_BUILD + #endif #elif STEAMWORKS_WIN - #define WINDOWS_BUILD + #define WINDOWS_BUILD #elif STEAMWORKS_LIN_OSX - // So that we don't enter the else block below. + // So that we don't enter the else block below. #else - #error You need to define STEAMWORKS_WIN, or STEAMWORKS_LIN_OSX. Refer to the readme for more details. + #error You need to define STEAMWORKS_WIN, or STEAMWORKS_LIN_OSX. Refer to the readme for more details. #endif using System; @@ -32,564 +32,564 @@ using System.Threading; namespace Steamworks { - public static class CallbackDispatcher { - // We catch exceptions inside callbacks and reroute them here. - // For some reason throwing an exception causes RunCallbacks() to break otherwise. - // If you have a custom ExceptionHandler in your engine you can register it here manually until we get something more elegant hooked up. - public static void ExceptionHandler(Exception e) { + public static class CallbackDispatcher { + // We catch exceptions inside callbacks and reroute them here. + // For some reason throwing an exception causes RunCallbacks() to break otherwise. + // If you have a custom ExceptionHandler in your engine you can register it here manually until we get something more elegant hooked up. + public static void ExceptionHandler(Exception e) { #if UNITY_STANDALONE - UnityEngine.Debug.LogException(e); + UnityEngine.Debug.LogException(e); #elif STEAMWORKS_WIN || STEAMWORKS_LIN_OSX - Console.WriteLine(e.Message); + Console.WriteLine(e.Message); #endif - } - - private static Dictionary> m_registeredCallbacks = new Dictionary>(); - private static Dictionary> m_registeredGameServerCallbacks = new Dictionary>(); - private static Dictionary> m_registeredCallResults = new Dictionary>(); - private static object m_sync = new object(); - private static IntPtr m_pCallbackMsg; - private static int m_initCount; - [ThreadStatic] + } + + private static Dictionary> m_registeredCallbacks = new Dictionary>(); + private static Dictionary> m_registeredGameServerCallbacks = new Dictionary>(); + private static Dictionary> m_registeredCallResults = new Dictionary>(); + private static object m_sync = new object(); + private static IntPtr m_pCallbackMsg; + private static int m_initCount; + [ThreadStatic] #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER // C# 8 Nullable annotation #nullable enable - private static CallResultBuffer? s_callResultBuffer; + private static CallResultBuffer? s_callResultBuffer; #nullable restore #else - private static /* nullable */ CallResultBuffer s_callResultBuffer; + private static /* nullable */ CallResultBuffer s_callResultBuffer; #endif - public static bool IsInitialized { - get { return m_initCount > 0; } - } - - internal static void Initialize() { - lock (m_sync) { - if (m_initCount == 0) { - NativeMethods.SteamAPI_ManualDispatch_Init(); - m_pCallbackMsg = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(CallbackMsg_t))); - } - ++m_initCount; - } - } - - internal static void Shutdown() { - lock (m_sync) { - --m_initCount; - if (m_initCount == 0) { - UnregisterAll(); - Marshal.FreeHGlobal(m_pCallbackMsg); - s_callResultBuffer?.Dispose(); - s_callResultBuffer = null; - m_pCallbackMsg = IntPtr.Zero; - } - } - } - - private sealed class CallResultBuffer : IDisposable { - private const int DefaultBufferSize = 4096 * 3; - private const int TooLargeSizeThreshold = (int)(DefaultBufferSize * 1.2); - // shrink if buffer too large counter reached this amount: - private const int ShrinkBufferThreshold = 330; - - private volatile int bufferTooLargeCounter = 0; - - private int currentCallResultBufferSize; - private IntPtr pCallResultBuffer; - - private bool disposedValue; - - /// - /// Acquire buffer. See exception details before use. - /// - /// Exact desired buffer size for receiving result. - /// Unmanaged buffer for receiving result - /// Route to - /// Recreate an new instance if thrown - /// Recreate an new instance if thrown - public IntPtr AcquireBuffer(uint requiredBufferSize) { - CheckIsDisposed(); // also checks if this reference is null - - if (currentCallResultBufferSize >= requiredBufferSize) { - // buffer is enough, this case will happen mostly - - // check is there a large struct incoming - if (requiredBufferSize >= TooLargeSizeThreshold) { - // yes and guess we will have some big structs in near future - // keep big buffer now and reset counter - - // thread-safe set - var currentCounterValue = bufferTooLargeCounter; - while (Interlocked.CompareExchange(ref bufferTooLargeCounter, 0, currentCounterValue) != currentCounterValue) { - // this thread failed the race, try again - currentCounterValue = bufferTooLargeCounter; - } - - return pCallResultBuffer; - } - // check counter to see should we shrink, do thread-safe get - else if (Interlocked.Increment(ref bufferTooLargeCounter) < ShrinkBufferThreshold) { - return pCallResultBuffer; - } - } - - // have to resize buffer - lock (this) { - CheckIsDisposed(); - // double check buffer size to avoid resize the buffer smaller - if (currentCallResultBufferSize >= requiredBufferSize) { - return pCallResultBuffer; - } - - requiredBufferSize = Math.Max(requiredBufferSize, DefaultBufferSize); - - // round buffer size to next multiple of 4096 - uint newBufferSize = requiredBufferSize; - if ((newBufferSize & 0x1FFF) != 4096) { - newBufferSize = (newBufferSize + 4095) & 0xFFFFF000; - } - - if (newBufferSize > int.MaxValue) { - // not to use enlarged size since we don't have enough space - newBufferSize = requiredBufferSize; - if (newBufferSize > int.MaxValue) { - // this exception should route to ExceptionHandler() - throw new NotSupportedException("The param size of a call result is larger than 2GiB"); - } - } + public static bool IsInitialized { + get { return m_initCount > 0; } + } + + internal static void Initialize() { + lock (m_sync) { + if (m_initCount == 0) { + NativeMethods.SteamAPI_ManualDispatch_Init(); + m_pCallbackMsg = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(CallbackMsg_t))); + } + ++m_initCount; + } + } + + internal static void Shutdown() { + lock (m_sync) { + --m_initCount; + if (m_initCount == 0) { + UnregisterAll(); + Marshal.FreeHGlobal(m_pCallbackMsg); + s_callResultBuffer?.Dispose(); + s_callResultBuffer = null; + m_pCallbackMsg = IntPtr.Zero; + } + } + } + + private sealed class CallResultBuffer : IDisposable { + private const int DefaultBufferSize = 4096 * 3; + private const int TooLargeSizeThreshold = (int)(DefaultBufferSize * 1.2); + // shrink if buffer too large counter reached this amount: + private const int ShrinkBufferThreshold = 330; + + private volatile int bufferTooLargeCounter = 0; + + private int currentCallResultBufferSize; + private IntPtr pCallResultBuffer; + + private bool disposedValue; + + /// + /// Acquire buffer. See exception details before use. + /// + /// Exact desired buffer size for receiving result. + /// Unmanaged buffer for receiving result + /// Route to + /// Recreate an new instance if thrown + /// Recreate an new instance if thrown + public IntPtr AcquireBuffer(uint requiredBufferSize) { + CheckIsDisposed(); // also checks if this reference is null + + if (currentCallResultBufferSize >= requiredBufferSize) { + // buffer is enough, this case will happen mostly + + // check is there a large struct incoming + if (requiredBufferSize >= TooLargeSizeThreshold) { + // yes and guess we will have some big structs in near future + // keep big buffer now and reset counter + + // thread-safe set + var currentCounterValue = bufferTooLargeCounter; + while (Interlocked.CompareExchange(ref bufferTooLargeCounter, 0, currentCounterValue) != currentCounterValue) { + // this thread failed the race, try again + currentCounterValue = bufferTooLargeCounter; + } + + return pCallResultBuffer; + } + // check counter to see should we shrink, do thread-safe get + else if (Interlocked.Increment(ref bufferTooLargeCounter) < ShrinkBufferThreshold) { + return pCallResultBuffer; + } + } + + // have to resize buffer + lock (this) { + CheckIsDisposed(); + // double check buffer size to avoid resize the buffer smaller + if (currentCallResultBufferSize >= requiredBufferSize) { + return pCallResultBuffer; + } + + requiredBufferSize = Math.Max(requiredBufferSize, DefaultBufferSize); + + // round buffer size to next multiple of 4096 + uint newBufferSize = requiredBufferSize; + if ((newBufferSize & 0x1FFF) != 4096) { + newBufferSize = (newBufferSize + 4095) & 0xFFFFF000; + } + + if (newBufferSize > int.MaxValue) { + // not to use enlarged size since we don't have enough space + newBufferSize = requiredBufferSize; + if (newBufferSize > int.MaxValue) { + // this exception should route to ExceptionHandler() + throw new NotSupportedException("The param size of a call result is larger than 2GiB"); + } + } #if NET5_0_OR_GREATER - pCallResultBuffer = Marshal.ReAllocHGlobal(pCallResultBuffer, (nint)newBufferSize); + pCallResultBuffer = Marshal.ReAllocHGlobal(pCallResultBuffer, (nint)newBufferSize); #else - Marshal.FreeHGlobal(pCallResultBuffer); - pCallResultBuffer = IntPtr.Zero; // is this necessary? - pCallResultBuffer = Marshal.AllocHGlobal((int)newBufferSize); + Marshal.FreeHGlobal(pCallResultBuffer); + pCallResultBuffer = IntPtr.Zero; // is this necessary? + pCallResultBuffer = Marshal.AllocHGlobal((int)newBufferSize); #endif - currentCallResultBufferSize = (int)newBufferSize; - return pCallResultBuffer; - } - } - - - private void CheckIsDisposed() { - if (disposedValue) { - throw new ObjectDisposedException(GetType().FullName, "Attempt to use a released call-result buffer."); - } - } - - private void Dispose(bool disposing) { - if (!disposedValue) { - lock (this) { - Marshal.FreeHGlobal(pCallResultBuffer); - disposedValue = true; - } - } - } - - ~CallResultBuffer() { - Dispose(disposing: false); - } - - public void Dispose() { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - } - - - internal static void Register(Callback cb) { - int iCallback = CallbackIdentities.GetCallbackIdentity(cb.GetCallbackType()); - var callbacksRegistry = cb.IsGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; - lock (m_sync) { - List callbacksList; - if (!callbacksRegistry.TryGetValue(iCallback, out callbacksList)) { - callbacksList = new List(); - callbacksRegistry.Add(iCallback, callbacksList); - } - - callbacksList.Add(cb); - } - } - - internal static void Register(SteamAPICall_t asyncCall, CallResult cr) { - lock (m_sync) { - List callResultsList; - if (!m_registeredCallResults.TryGetValue((ulong)asyncCall, out callResultsList)) { - callResultsList = new List(); - m_registeredCallResults.Add((ulong)asyncCall, callResultsList); - } - - callResultsList.Add(cr); - } - } - - internal static void Unregister(Callback cb) { - int iCallback = CallbackIdentities.GetCallbackIdentity(cb.GetCallbackType()); - var callbacksRegistry = cb.IsGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; - lock (m_sync) { - List callbacksList; - if (callbacksRegistry.TryGetValue(iCallback, out callbacksList)) { - callbacksList.Remove(cb); - if (callbacksList.Count == 0) - callbacksRegistry.Remove(iCallback); - } - } - } - - internal static void Unregister(SteamAPICall_t asyncCall, CallResult cr) { - lock (m_sync) { - List callResultsList; - if (m_registeredCallResults.TryGetValue((ulong)asyncCall, out callResultsList)) { - callResultsList.Remove(cr); - if (callResultsList.Count == 0) - m_registeredCallResults.Remove((ulong)asyncCall); - } - } - } - - private static void UnregisterAll() { - List callbacks = new List(); - List callResults = new List(); - lock (m_sync) { - foreach (var pair in m_registeredCallbacks) { - callbacks.AddRange(pair.Value); - } - m_registeredCallbacks.Clear(); - - foreach (var pair in m_registeredGameServerCallbacks) { - callbacks.AddRange(pair.Value); - } - m_registeredGameServerCallbacks.Clear(); - - foreach (var pair in m_registeredCallResults) { - callResults.AddRange(pair.Value); - } - m_registeredCallResults.Clear(); - - foreach (var callback in callbacks) { - callback.SetUnregistered(); - } - - foreach (var callResult in callResults) { - callResult.SetUnregistered(); - } - } - } - - internal static void RunFrame(bool isGameServer) { - if (!IsInitialized) throw new InvalidOperationException("Callback dispatcher is not initialized."); - - HSteamPipe hSteamPipe = (HSteamPipe)(isGameServer ? NativeMethods.SteamGameServer_GetHSteamPipe() : NativeMethods.SteamAPI_GetHSteamPipe()); - NativeMethods.SteamAPI_ManualDispatch_RunFrame(hSteamPipe); - var callbacksRegistry = isGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; - while (NativeMethods.SteamAPI_ManualDispatch_GetNextCallback(hSteamPipe, m_pCallbackMsg)) { + currentCallResultBufferSize = (int)newBufferSize; + return pCallResultBuffer; + } + } + + + private void CheckIsDisposed() { + if (disposedValue) { + throw new ObjectDisposedException(GetType().FullName, "Attempt to use a released call-result buffer."); + } + } + + private void Dispose(bool disposing) { + if (!disposedValue) { + lock (this) { + Marshal.FreeHGlobal(pCallResultBuffer); + disposedValue = true; + } + } + } + + ~CallResultBuffer() { + Dispose(disposing: false); + } + + public void Dispose() { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } + + + internal static void Register(Callback cb) { + int iCallback = CallbackIdentities.GetCallbackIdentity(cb.GetCallbackType()); + var callbacksRegistry = cb.IsGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; + lock (m_sync) { + List callbacksList; + if (!callbacksRegistry.TryGetValue(iCallback, out callbacksList)) { + callbacksList = new List(); + callbacksRegistry.Add(iCallback, callbacksList); + } + + callbacksList.Add(cb); + } + } + + internal static void Register(SteamAPICall_t asyncCall, CallResult cr) { + lock (m_sync) { + List callResultsList; + if (!m_registeredCallResults.TryGetValue((ulong)asyncCall, out callResultsList)) { + callResultsList = new List(); + m_registeredCallResults.Add((ulong)asyncCall, callResultsList); + } + + callResultsList.Add(cr); + } + } + + internal static void Unregister(Callback cb) { + int iCallback = CallbackIdentities.GetCallbackIdentity(cb.GetCallbackType()); + var callbacksRegistry = cb.IsGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; + lock (m_sync) { + List callbacksList; + if (callbacksRegistry.TryGetValue(iCallback, out callbacksList)) { + callbacksList.Remove(cb); + if (callbacksList.Count == 0) + callbacksRegistry.Remove(iCallback); + } + } + } + + internal static void Unregister(SteamAPICall_t asyncCall, CallResult cr) { + lock (m_sync) { + List callResultsList; + if (m_registeredCallResults.TryGetValue((ulong)asyncCall, out callResultsList)) { + callResultsList.Remove(cr); + if (callResultsList.Count == 0) + m_registeredCallResults.Remove((ulong)asyncCall); + } + } + } + + private static void UnregisterAll() { + List callbacks = new List(); + List callResults = new List(); + lock (m_sync) { + foreach (var pair in m_registeredCallbacks) { + callbacks.AddRange(pair.Value); + } + m_registeredCallbacks.Clear(); + + foreach (var pair in m_registeredGameServerCallbacks) { + callbacks.AddRange(pair.Value); + } + m_registeredGameServerCallbacks.Clear(); + + foreach (var pair in m_registeredCallResults) { + callResults.AddRange(pair.Value); + } + m_registeredCallResults.Clear(); + + foreach (var callback in callbacks) { + callback.SetUnregistered(); + } + + foreach (var callResult in callResults) { + callResult.SetUnregistered(); + } + } + } + + internal static void RunFrame(bool isGameServer) { + if (!IsInitialized) throw new InvalidOperationException("Callback dispatcher is not initialized."); + + HSteamPipe hSteamPipe = (HSteamPipe)(isGameServer ? NativeMethods.SteamGameServer_GetHSteamPipe() : NativeMethods.SteamAPI_GetHSteamPipe()); + NativeMethods.SteamAPI_ManualDispatch_RunFrame(hSteamPipe); + var callbacksRegistry = isGameServer ? m_registeredGameServerCallbacks : m_registeredCallbacks; + while (NativeMethods.SteamAPI_ManualDispatch_GetNextCallback(hSteamPipe, m_pCallbackMsg)) { #if NET5_0_OR_GREATER - // Do not modify the fields inside, or will violate some .NET runtime constraint! - ref CallbackMsg_t callbackMsg = ref Unsafe.Unbox(Marshal.PtrToStructure(m_pCallbackMsg, typeof(CallbackMsg_t))); + // Do not modify the fields inside, or will violate some .NET runtime constraint! + ref CallbackMsg_t callbackMsg = ref Unsafe.Unbox(Marshal.PtrToStructure(m_pCallbackMsg, typeof(CallbackMsg_t))); #else - CallbackMsg_t callbackMsg = (CallbackMsg_t)Marshal.PtrToStructure(m_pCallbackMsg, typeof(CallbackMsg_t)); + CallbackMsg_t callbackMsg = (CallbackMsg_t)Marshal.PtrToStructure(m_pCallbackMsg, typeof(CallbackMsg_t)); #endif - try { - // Check for dispatching API call results - if (callbackMsg.m_iCallback == SteamAPICallCompleted_t.k_iCallback) { + try { + // Check for dispatching API call results + if (callbackMsg.m_iCallback == SteamAPICallCompleted_t.k_iCallback) { #if NET5_0_OR_GREATER - // Same as above! - ref SteamAPICallCompleted_t callCompletedCb = ref Unsafe.Unbox( - Marshal.PtrToStructure(callbackMsg.m_pubParam, typeof(SteamAPICallCompleted_t)) - ); + // Same as above! + ref SteamAPICallCompleted_t callCompletedCb = ref Unsafe.Unbox( + Marshal.PtrToStructure(callbackMsg.m_pubParam, typeof(SteamAPICallCompleted_t)) + ); #else - SteamAPICallCompleted_t callCompletedCb = (SteamAPICallCompleted_t)Marshal.PtrToStructure(callbackMsg.m_pubParam, typeof(SteamAPICallCompleted_t)); + SteamAPICallCompleted_t callCompletedCb = (SteamAPICallCompleted_t)Marshal.PtrToStructure(callbackMsg.m_pubParam, typeof(SteamAPICallCompleted_t)); #endif - // threading safe issues in allocating call-result buffer is handled by AcquireBuffer() - IntPtr pTmpCallResult; - CallResultBuffer bufferHolder = s_callResultBuffer; - try { - // In most cases s_callResultBuffer will have valid value, - // by moving rare cases(recreate buffer holder) to exception path, - // should avoid generating creation code into usage branch - // and keep usage branch clear. - pTmpCallResult = bufferHolder.AcquireBuffer(callCompletedCb.m_cubParam); - } - catch (NotSupportedException ex) { - ExceptionHandler(ex); - continue; - } catch (ObjectDisposedException) { - var bufferHolderNew = new CallResultBuffer(); - pTmpCallResult = bufferHolderNew.AcquireBuffer(callCompletedCb.m_cubParam); - - // try set shared buffer to newly created one, accept race failure - Interlocked.CompareExchange(ref s_callResultBuffer, bufferHolderNew, bufferHolder); - // avoid new instance from being gc collected - bufferHolder = bufferHolderNew; - } catch (NullReferenceException) { - // keep same as above - var bufferHolderNew = new CallResultBuffer(); - pTmpCallResult = bufferHolderNew.AcquireBuffer(callCompletedCb.m_cubParam); - - Interlocked.CompareExchange(ref s_callResultBuffer, bufferHolderNew, bufferHolder); - bufferHolder = bufferHolderNew; - } - - bool bFailed; - - if (NativeMethods.SteamAPI_ManualDispatch_GetAPICallResult( - hSteamPipe, callCompletedCb.m_hAsyncCall, pTmpCallResult, - (int)callCompletedCb.m_cubParam, callCompletedCb.m_iCallback, - out bFailed)) { - lock (m_sync) { - List callResults; + // threading safe issues in allocating call-result buffer is handled by AcquireBuffer() + IntPtr pTmpCallResult; + CallResultBuffer bufferHolder = s_callResultBuffer; + try { + // In most cases s_callResultBuffer will have valid value, + // by moving rare cases(recreate buffer holder) to exception path, + // should avoid generating creation code into usage branch + // and keep usage branch clear. + pTmpCallResult = bufferHolder.AcquireBuffer(callCompletedCb.m_cubParam); + } + catch (NotSupportedException ex) { + ExceptionHandler(ex); + continue; + } catch (ObjectDisposedException) { + var bufferHolderNew = new CallResultBuffer(); + pTmpCallResult = bufferHolderNew.AcquireBuffer(callCompletedCb.m_cubParam); + + // try set shared buffer to newly created one, accept race failure + Interlocked.CompareExchange(ref s_callResultBuffer, bufferHolderNew, bufferHolder); + // avoid new instance from being gc collected + bufferHolder = bufferHolderNew; + } catch (NullReferenceException) { + // keep same as above + var bufferHolderNew = new CallResultBuffer(); + pTmpCallResult = bufferHolderNew.AcquireBuffer(callCompletedCb.m_cubParam); + + Interlocked.CompareExchange(ref s_callResultBuffer, bufferHolderNew, bufferHolder); + bufferHolder = bufferHolderNew; + } + + bool bFailed; + + if (NativeMethods.SteamAPI_ManualDispatch_GetAPICallResult( + hSteamPipe, callCompletedCb.m_hAsyncCall, pTmpCallResult, + (int)callCompletedCb.m_cubParam, callCompletedCb.m_iCallback, + out bFailed)) { + lock (m_sync) { + List callResults; #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_0_OR_GREATER - if (m_registeredCallResults.Remove(callCompletedCb.m_hAsyncCall.m_SteamAPICall, out callResults)) { + if (m_registeredCallResults.Remove(callCompletedCb.m_hAsyncCall.m_SteamAPICall, out callResults)) { #else // compatibility to old Unity and .NET Framework project - if (m_registeredCallResults.TryGetValue((ulong)callCompletedCb.m_hAsyncCall, out callResults)) { - m_registeredCallResults.Remove((ulong)callCompletedCb.m_hAsyncCall); + if (m_registeredCallResults.TryGetValue((ulong)callCompletedCb.m_hAsyncCall, out callResults)) { + m_registeredCallResults.Remove((ulong)callCompletedCb.m_hAsyncCall); #endif - foreach (var cr in callResults) { - cr.OnRunCallResult(pTmpCallResult, bFailed, (ulong)callCompletedCb.m_hAsyncCall); - cr.SetUnregistered(); - } - } - } - } - } else { - List callbacksCopy = null; - lock (m_sync) { - List callbacks = null; - if (callbacksRegistry.TryGetValue(callbackMsg.m_iCallback, out callbacks)) { - callbacksCopy = new List(callbacks); - } - } - if (callbacksCopy != null) { - foreach (var callback in callbacksCopy) { - callback.OnRunCallback(callbackMsg.m_pubParam); - } - } - } - } catch (Exception e) { - ExceptionHandler(e); - } finally { - NativeMethods.SteamAPI_ManualDispatch_FreeLastCallback(hSteamPipe); - } - } - } - } - - /// - /// Internals of Steamworks.NET, not meant to use directly - /// - // Akarinnnn: I think the reason of this type is not interface, is historical burden - public abstract class Callback { - public abstract bool IsGameServer { get; } - internal abstract Type GetCallbackType(); - /// - /// - /// Some changes made to dispatcher leads only valid during invocation - /// - /// - /// Result struct buffer that valid while invocation, - /// must use to retrieve before return - internal abstract void OnRunCallback(IntPtr pvParam); - internal abstract void SetUnregistered(); - } - - public sealed class Callback : Callback, IDisposable { - public delegate void DispatchDelegate(T param); - private event DispatchDelegate m_Func; - - private bool m_bGameServer; - private bool m_bIsRegistered; - - private bool m_bDisposed = false; - - /// - /// Creates a new Callback. You must be calling SteamAPI.RunCallbacks() to retrieve the callbacks. - /// Returns a handle to the Callback. - /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. - /// - public static Callback Create(DispatchDelegate func) { - return new Callback(func, bGameServer: false); - } - - /// - /// Creates a new GameServer Callback. You must be calling GameServer.RunCallbacks() to retrieve the callbacks. - /// Returns a handle to the Callback. - /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. - /// - public static Callback CreateGameServer(DispatchDelegate func) { - return new Callback(func, bGameServer: true); - } - - public Callback(DispatchDelegate func, bool bGameServer = false) { - m_bGameServer = bGameServer; - Register(func); - } - - ~Callback() { - Dispose(); - } - - public void Dispose() { - if (m_bDisposed) { - return; - } - - GC.SuppressFinalize(this); - - if (m_bIsRegistered) - Unregister(); - - m_bDisposed = true; - } - - // Manual registration of the callback - public void Register(DispatchDelegate func) { - if (func == null) { - throw new Exception("Callback function must not be null."); - } - - if (m_bIsRegistered) { - Unregister(); - } - - m_Func = func; - - CallbackDispatcher.Register(this); - m_bIsRegistered = true; - } - - public void Unregister() { - CallbackDispatcher.Unregister(this); - m_bIsRegistered = false; - } - - public override bool IsGameServer { - get { return m_bGameServer; } - } - - internal override Type GetCallbackType() { - return typeof(T); - } - - /// - internal override void OnRunCallback(IntPtr pvParam) { - try { - m_Func((T)Marshal.PtrToStructure(pvParam, typeof(T))); - } - catch (Exception e) { - CallbackDispatcher.ExceptionHandler(e); - } - } - - internal override void SetUnregistered() { - m_bIsRegistered = false; - } - } - - /// - /// Internals of Steamworks.NET, not meant to use directly - /// - public abstract class CallResult { - internal abstract Type GetCallbackType(); - /// - /// - /// Some changes made to dispatcher leads only valid during invocation - /// - /// - /// Result struct buffer that valid while invocation, - /// must use to retrieve before return - internal abstract void OnRunCallResult(IntPtr pvParam, bool bFailed, ulong hSteamAPICall); - internal abstract void SetUnregistered(); - } - - public sealed class CallResult : CallResult, IDisposable { - public delegate void APIDispatchDelegate(T param, bool bIOFailure); - private event APIDispatchDelegate m_Func; - - private SteamAPICall_t m_hAPICall = SteamAPICall_t.Invalid; - public SteamAPICall_t Handle { get { return m_hAPICall; } } - - private bool m_bDisposed = false; - - /// - /// Creates a new async CallResult. You must be calling SteamAPI.RunCallbacks() to retrieve the callback. - /// Returns a handle to the CallResult. - /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. - /// - public static CallResult Create(APIDispatchDelegate func = null) { - return new CallResult(func); - } - - public CallResult(APIDispatchDelegate func = null) { - m_Func = func; - } - - ~CallResult() { - Dispose(); - } - - public void Dispose() { - if (m_bDisposed) { - return; - } - - GC.SuppressFinalize(this); - - Cancel(); - - m_bDisposed = true; - } - - public void Set(SteamAPICall_t hAPICall, APIDispatchDelegate func = null) { - // Unlike the official SDK we let the user assign a single function during creation, - // and allow them to skip having to do so every time that they call .Set() - if (func != null) { - m_Func = func; - } - - if (m_Func == null) { - throw new Exception("CallResult function was null, you must either set it in the CallResult Constructor or via Set()"); - } - - if (m_hAPICall != SteamAPICall_t.Invalid) { - CallbackDispatcher.Unregister(m_hAPICall, this); - } - - m_hAPICall = hAPICall; - - if (hAPICall != SteamAPICall_t.Invalid) { - CallbackDispatcher.Register(hAPICall, this); - } - } - - public bool IsActive() { - return (m_hAPICall != SteamAPICall_t.Invalid); - } - - public void Cancel() { - if (IsActive()) - CallbackDispatcher.Unregister(m_hAPICall, this); - } - - internal override Type GetCallbackType() { - return typeof(T); - } - - /// - internal override void OnRunCallResult(IntPtr pvParam, bool bFailed, ulong hSteamAPICall_) { - SteamAPICall_t hSteamAPICall = (SteamAPICall_t)hSteamAPICall_; - if (hSteamAPICall == m_hAPICall) { - try { - m_Func((T)Marshal.PtrToStructure(pvParam, typeof(T)), bFailed); - } - catch (Exception e) { - CallbackDispatcher.ExceptionHandler(e); - } - } - } - - internal override void SetUnregistered() { - m_hAPICall = SteamAPICall_t.Invalid; - } - } + foreach (var cr in callResults) { + cr.OnRunCallResult(pTmpCallResult, bFailed, (ulong)callCompletedCb.m_hAsyncCall); + cr.SetUnregistered(); + } + } + } + } + } else { + List callbacksCopy = null; + lock (m_sync) { + List callbacks = null; + if (callbacksRegistry.TryGetValue(callbackMsg.m_iCallback, out callbacks)) { + callbacksCopy = new List(callbacks); + } + } + if (callbacksCopy != null) { + foreach (var callback in callbacksCopy) { + callback.OnRunCallback(callbackMsg.m_pubParam); + } + } + } + } catch (Exception e) { + ExceptionHandler(e); + } finally { + NativeMethods.SteamAPI_ManualDispatch_FreeLastCallback(hSteamPipe); + } + } + } + } + + /// + /// Internals of Steamworks.NET, not meant to use directly + /// + // Akarinnnn: I think the reason of this type is not interface, is historical burden + public abstract class Callback { + public abstract bool IsGameServer { get; } + internal abstract Type GetCallbackType(); + /// + /// + /// Some changes made to dispatcher leads only valid during invocation + /// + /// + /// Result struct buffer that valid while invocation, + /// must use to retrieve before return + internal abstract void OnRunCallback(IntPtr pvParam); + internal abstract void SetUnregistered(); + } + + public sealed class Callback : Callback, IDisposable { + public delegate void DispatchDelegate(T param); + private event DispatchDelegate m_Func; + + private bool m_bGameServer; + private bool m_bIsRegistered; + + private bool m_bDisposed = false; + + /// + /// Creates a new Callback. You must be calling SteamAPI.RunCallbacks() to retrieve the callbacks. + /// Returns a handle to the Callback. + /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. + /// + public static Callback Create(DispatchDelegate func) { + return new Callback(func, bGameServer: false); + } + + /// + /// Creates a new GameServer Callback. You must be calling GameServer.RunCallbacks() to retrieve the callbacks. + /// Returns a handle to the Callback. + /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. + /// + public static Callback CreateGameServer(DispatchDelegate func) { + return new Callback(func, bGameServer: true); + } + + public Callback(DispatchDelegate func, bool bGameServer = false) { + m_bGameServer = bGameServer; + Register(func); + } + + ~Callback() { + Dispose(); + } + + public void Dispose() { + if (m_bDisposed) { + return; + } + + GC.SuppressFinalize(this); + + if (m_bIsRegistered) + Unregister(); + + m_bDisposed = true; + } + + // Manual registration of the callback + public void Register(DispatchDelegate func) { + if (func == null) { + throw new Exception("Callback function must not be null."); + } + + if (m_bIsRegistered) { + Unregister(); + } + + m_Func = func; + + CallbackDispatcher.Register(this); + m_bIsRegistered = true; + } + + public void Unregister() { + CallbackDispatcher.Unregister(this); + m_bIsRegistered = false; + } + + public override bool IsGameServer { + get { return m_bGameServer; } + } + + internal override Type GetCallbackType() { + return typeof(T); + } + + /// + internal override void OnRunCallback(IntPtr pvParam) { + try { + m_Func((T)Marshal.PtrToStructure(pvParam, typeof(T))); + } + catch (Exception e) { + CallbackDispatcher.ExceptionHandler(e); + } + } + + internal override void SetUnregistered() { + m_bIsRegistered = false; + } + } + + /// + /// Internals of Steamworks.NET, not meant to use directly + /// + public abstract class CallResult { + internal abstract Type GetCallbackType(); + /// + /// + /// Some changes made to dispatcher leads only valid during invocation + /// + /// + /// Result struct buffer that valid while invocation, + /// must use to retrieve before return + internal abstract void OnRunCallResult(IntPtr pvParam, bool bFailed, ulong hSteamAPICall); + internal abstract void SetUnregistered(); + } + + public sealed class CallResult : CallResult, IDisposable { + public delegate void APIDispatchDelegate(T param, bool bIOFailure); + private event APIDispatchDelegate m_Func; + + private SteamAPICall_t m_hAPICall = SteamAPICall_t.Invalid; + public SteamAPICall_t Handle { get { return m_hAPICall; } } + + private bool m_bDisposed = false; + + /// + /// Creates a new async CallResult. You must be calling SteamAPI.RunCallbacks() to retrieve the callback. + /// Returns a handle to the CallResult. + /// This MUST be assigned to a member variable to prevent the GC from cleaning it up. + /// + public static CallResult Create(APIDispatchDelegate func = null) { + return new CallResult(func); + } + + public CallResult(APIDispatchDelegate func = null) { + m_Func = func; + } + + ~CallResult() { + Dispose(); + } + + public void Dispose() { + if (m_bDisposed) { + return; + } + + GC.SuppressFinalize(this); + + Cancel(); + + m_bDisposed = true; + } + + public void Set(SteamAPICall_t hAPICall, APIDispatchDelegate func = null) { + // Unlike the official SDK we let the user assign a single function during creation, + // and allow them to skip having to do so every time that they call .Set() + if (func != null) { + m_Func = func; + } + + if (m_Func == null) { + throw new Exception("CallResult function was null, you must either set it in the CallResult Constructor or via Set()"); + } + + if (m_hAPICall != SteamAPICall_t.Invalid) { + CallbackDispatcher.Unregister(m_hAPICall, this); + } + + m_hAPICall = hAPICall; + + if (hAPICall != SteamAPICall_t.Invalid) { + CallbackDispatcher.Register(hAPICall, this); + } + } + + public bool IsActive() { + return (m_hAPICall != SteamAPICall_t.Invalid); + } + + public void Cancel() { + if (IsActive()) + CallbackDispatcher.Unregister(m_hAPICall, this); + } + + internal override Type GetCallbackType() { + return typeof(T); + } + + /// + internal override void OnRunCallResult(IntPtr pvParam, bool bFailed, ulong hSteamAPICall_) { + SteamAPICall_t hSteamAPICall = (SteamAPICall_t)hSteamAPICall_; + if (hSteamAPICall == m_hAPICall) { + try { + m_Func((T)Marshal.PtrToStructure(pvParam, typeof(T)), bFailed); + } + catch (Exception e) { + CallbackDispatcher.ExceptionHandler(e); + } + } + } + + internal override void SetUnregistered() { + m_hAPICall = SteamAPICall_t.Invalid; + } + } } #endif // !DISABLESTEAMWORKS