diff --git a/Standalone/Steamworks.NET.Standard.csproj b/Standalone/Steamworks.NET.Standard.csproj index 7f7eb5a4..4b373410 100644 --- a/Standalone/Steamworks.NET.Standard.csproj +++ b/Standalone/Steamworks.NET.Standard.csproj @@ -8,6 +8,7 @@ Windows;OSX-Linux false git + low diff --git a/Standalone/Steamworks.NET.Standard.sln b/Standalone/Steamworks.NET.Standard.sln index 16f6cc6e..07a8b195 100644 --- a/Standalone/Steamworks.NET.Standard.sln +++ b/Standalone/Steamworks.NET.Standard.sln @@ -1,44 +1,22 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29009.5 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35617.110 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steamworks.NET.Standard", "Steamworks.NET.Standard.csproj", "{F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - OSX-Linux|Any CPU = OSX-Linux|Any CPU OSX-Linux|x64 = OSX-Linux|x64 OSX-Linux|x86 = OSX-Linux|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - Windows|Any CPU = Windows|Any CPU Windows|x64 = Windows|x64 Windows|x86 = Windows|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Debug|Any CPU.ActiveCfg = Windows|x86 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Debug|Any CPU.Build.0 = Windows|x86 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Debug|x64.ActiveCfg = Windows|x64 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Debug|x64.Build.0 = Windows|x64 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Debug|x86.ActiveCfg = Windows|x86 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Debug|x86.Build.0 = Windows|x86 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.OSX-Linux|Any CPU.ActiveCfg = OSX-Linux|x86 {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.OSX-Linux|x64.ActiveCfg = OSX-Linux|x64 {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.OSX-Linux|x64.Build.0 = OSX-Linux|x64 {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.OSX-Linux|x86.ActiveCfg = OSX-Linux|x86 {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.OSX-Linux|x86.Build.0 = OSX-Linux|x86 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Release|Any CPU.ActiveCfg = Windows|x86 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Release|Any CPU.Build.0 = Windows|x86 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Release|x64.ActiveCfg = Windows|x64 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Release|x64.Build.0 = Windows|x64 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Release|x86.ActiveCfg = Windows|x86 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Release|x86.Build.0 = Windows|x86 - {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Windows|Any CPU.ActiveCfg = Windows|x86 {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Windows|x64.ActiveCfg = Windows|x64 {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Windows|x64.Build.0 = Windows|x64 {F34F403D-1D4D-4DC6-BBAE-DA31190EC88C}.Windows|x86.ActiveCfg = Windows|x86 diff --git a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs index 47bcaecf..af87cba6 100644 --- a/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs +++ b/com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs @@ -1,4 +1,4 @@ -// This file is provided under The MIT License as part of Steamworks.NET. +// This file is provided under The MIT License as part of Steamworks.NET. // Copyright (c) 2013-2022 Riley Labrecque // Please see the included LICENSE.txt for additional information. @@ -27,7 +27,9 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; namespace Steamworks { public static class CallbackDispatcher { @@ -48,6 +50,8 @@ public static void ExceptionHandler(Exception e) { private static object m_sync = new object(); private static IntPtr m_pCallbackMsg; private static int m_initCount; + [ThreadStatic] + private static /* nullable */ CallResultBuffer s_callResultBuffer; public static bool IsInitialized { get { return m_initCount > 0; } @@ -69,11 +73,125 @@ internal static void Shutdown() { 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 = 2048; + 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 2048 + uint newBufferSize = requiredBufferSize; + if (newBufferSize % 2048 != 0u) { + uint newBufferPageCount = (newBufferSize / 2048u) + 1u; + newBufferSize = newBufferPageCount * 2048u; + } + + 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); +#else + 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; @@ -160,18 +278,67 @@ 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); +#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)) { + + 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 +346,6 @@ internal static void RunFrame(bool isGameServer) { } } } - Marshal.FreeHGlobal(pTmpCallResult); } else { List callbacksCopy = null; lock (m_sync) { @@ -203,9 +369,20 @@ internal static void RunFrame(bool isGameServer) { } } + /// + /// 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(); } @@ -288,6 +465,7 @@ internal override Type GetCallbackType() { return typeof(T); } + /// internal override void OnRunCallback(IntPtr pvParam) { try { m_Func((T)Marshal.PtrToStructure(pvParam, typeof(T))); @@ -302,8 +480,18 @@ internal override void SetUnregistered() { } } + /// + /// 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(); } @@ -381,6 +569,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) {