Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Standalone/Steamworks.NET.Standard.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<Configurations>Windows;OSX-Linux</Configurations>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<RepositoryType>git</RepositoryType>
<NuGetAuditLevel>low</NuGetAuditLevel>
</PropertyGroup>

<PropertyGroup Label="Nuget PM">
Expand Down
26 changes: 2 additions & 24 deletions Standalone/Steamworks.NET.Standard.sln
Original file line number Diff line number Diff line change
@@ -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
Expand Down
199 changes: 194 additions & 5 deletions com.rlabrecque.steamworks.net/Runtime/CallbackDispatcher.cs
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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 {
Expand All @@ -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; }
Expand All @@ -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;

/// <summary>
/// Acquire buffer. See exception details before use.
/// </summary>
/// <param name="requiredBufferSize">Exact desired buffer size for receiving result.</param>
/// <returns>Unmanaged buffer for receiving result</returns>
/// <exception cref="NotSupportedException">Route to <see cref="ExceptionHandler(Exception)"/></exception>
/// <exception cref="ObjectDisposedException">Recreate an new instance if thrown</exception>
/// <exception cref="NullReferenceException">Recreate an new instance if thrown</exception>
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;
Expand Down Expand Up @@ -160,26 +278,74 @@ 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<CallbackMsg_t>(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<SteamAPICallCompleted_t>(
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<CallResult> 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();
}
}
}
}
Marshal.FreeHGlobal(pTmpCallResult);
} else {
List<Callback> callbacksCopy = null;
lock (m_sync) {
Expand All @@ -203,9 +369,20 @@ internal static void RunFrame(bool isGameServer) {
}
}

/// <summary>
/// Internals of Steamworks.NET, not meant to use directly
/// </summary>
// 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();
/// <devdoc>
/// <remarks>
/// Some changes made to dispatcher leads <paramref name="pvParam"/> only valid during <see cref="OnRunCallback(IntPtr)"/> invocation
/// </remarks>
/// </devdoc>
/// <param name="pvParam">Result struct buffer that valid while invocation,
/// must use <see cref="Marshal.PtrToStructure(IntPtr, Type)"/> to retrieve before return</param>
internal abstract void OnRunCallback(IntPtr pvParam);
internal abstract void SetUnregistered();
}
Expand Down Expand Up @@ -288,6 +465,7 @@ internal override Type GetCallbackType() {
return typeof(T);
}

/// <inheritdoc/>
internal override void OnRunCallback(IntPtr pvParam) {
try {
m_Func((T)Marshal.PtrToStructure(pvParam, typeof(T)));
Expand All @@ -302,8 +480,18 @@ internal override void SetUnregistered() {
}
}

/// <summary>
/// Internals of Steamworks.NET, not meant to use directly
/// </summary>
public abstract class CallResult {
internal abstract Type GetCallbackType();
/// <devdoc>
/// <remarks>
/// Some changes made to dispatcher leads <paramref name="pvParam"/> only valid during <see cref="OnRunCallResult(IntPtr, bool, ulong)"/> invocation
/// </remarks>
/// </devdoc>
/// <param name="pvParam">Result struct buffer that valid while invocation,
/// must use <see cref="Marshal.PtrToStructure(IntPtr, Type)"/> to retrieve before return</param>
internal abstract void OnRunCallResult(IntPtr pvParam, bool bFailed, ulong hSteamAPICall);
internal abstract void SetUnregistered();
}
Expand Down Expand Up @@ -381,6 +569,7 @@ internal override Type GetCallbackType() {
return typeof(T);
}

/// <inheritdoc/>
internal override void OnRunCallResult(IntPtr pvParam, bool bFailed, ulong hSteamAPICall_) {
SteamAPICall_t hSteamAPICall = (SteamAPICall_t)hSteamAPICall_;
if (hSteamAPICall == m_hAPICall) {
Expand Down