From 81d707c271e814f5ec182906a96b84fa262fc011 Mon Sep 17 00:00:00 2001 From: roflmuffin Date: Thu, 18 Dec 2025 05:26:28 +0000 Subject: [PATCH] feat: move next frame and next world update into managed side for performance --- .../Generated/Natives/API.cs | 20 ----- managed/CounterStrikeSharp.API/Server.cs | 82 ++++++++++++++++--- .../FrameSchedulingTests.cs | 38 +++++++-- .../NativeTestsPlugin.cs | 2 +- .../TestUtils.cs | 2 +- src/core/managers/server_manager.cpp | 24 ------ src/core/managers/server_manager.h | 4 - src/mm_plugin.cpp | 27 +----- src/mm_plugin.h | 5 -- src/scripting/natives/natives_engine.cpp | 22 ----- src/scripting/natives/natives_engine.yaml | 2 - 11 files changed, 102 insertions(+), 126 deletions(-) diff --git a/managed/CounterStrikeSharp.API/Generated/Natives/API.cs b/managed/CounterStrikeSharp.API/Generated/Natives/API.cs index 7ad8032c7..6c241407c 100644 --- a/managed/CounterStrikeSharp.API/Generated/Natives/API.cs +++ b/managed/CounterStrikeSharp.API/Generated/Natives/API.cs @@ -807,16 +807,6 @@ public static double GetTickedTime(){ } } - public static void QueueTaskForNextFrame(InputArgument callback){ - lock (ScriptContext.GlobalScriptContext.Lock) { - ScriptContext.GlobalScriptContext.Reset(); - ScriptContext.GlobalScriptContext.Push((InputArgument)callback); - ScriptContext.GlobalScriptContext.SetIdentifier(0x9FE394D8); - ScriptContext.GlobalScriptContext.Invoke(); - ScriptContext.GlobalScriptContext.CheckErrors(); - } - } - public static void QueueTaskForFrame(int tick, InputArgument callback){ lock (ScriptContext.GlobalScriptContext.Lock) { ScriptContext.GlobalScriptContext.Reset(); @@ -828,16 +818,6 @@ public static void QueueTaskForFrame(int tick, InputArgument callback){ } } - public static void QueueTaskForNextWorldUpdate(InputArgument callback){ - lock (ScriptContext.GlobalScriptContext.Lock) { - ScriptContext.GlobalScriptContext.Reset(); - ScriptContext.GlobalScriptContext.Push((InputArgument)callback); - ScriptContext.GlobalScriptContext.SetIdentifier(0xAD51A0C9); - ScriptContext.GlobalScriptContext.Invoke(); - ScriptContext.GlobalScriptContext.CheckErrors(); - } - } - public static IntPtr GetValveInterface(int interfacetype, string interfacename){ lock (ScriptContext.GlobalScriptContext.Lock) { ScriptContext.GlobalScriptContext.Reset(); diff --git a/managed/CounterStrikeSharp.API/Server.cs b/managed/CounterStrikeSharp.API/Server.cs index 60a0b4c51..c6255bb80 100644 --- a/managed/CounterStrikeSharp.API/Server.cs +++ b/managed/CounterStrikeSharp.API/Server.cs @@ -14,21 +14,51 @@ * along with CounterStrikeSharp. If not, see . * */ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; +using System.Collections.Concurrent; using System.Threading.Tasks; -using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Core.Translations; using CounterStrikeSharp.API.Modules.Memory; using CounterStrikeSharp.API.Modules.Utils; +using Microsoft.Extensions.Logging; namespace CounterStrikeSharp.API { public class Server { + static Server() + { + NativeAPI.AddListener("OnTick", (Delegate)(() => OnTick())); + NativeAPI.AddListener("OnServerPreWorldUpdate", (Delegate)((bool simulating) => OnWorldUpdate())); + } + + private static readonly ConcurrentQueue _onTickTaskQueue = new(); + private static readonly ConcurrentQueue _onWorldUpdateTaskQueue = new(); + + internal static void OnTick() + { + ExecuteTickTasks(_onTickTaskQueue); + } + + internal static void OnWorldUpdate() + { + ExecuteTickTasks(_onWorldUpdateTaskQueue); + } + + private static void ExecuteTickTasks(ConcurrentQueue taskQueue) + { + while (taskQueue.TryDequeue(out var task)) + { + try + { + task(); + } + catch (Exception e) + { + Application.Instance.Logger.LogError(e, "Error invoking callback"); + } + } + } + + /// /// Duration of a single game tick in seconds, based on a 64 tick server (hard coded in CS2). /// @@ -101,9 +131,22 @@ public static void RunOnTick(int tick, Action task) /// public static Task NextFrameAsync(Action task) { - var functionReference = FunctionReference.Create(task, FunctionLifetime.SingleUse); - NativeAPI.QueueTaskForNextFrame(functionReference); - return functionReference.CompletionTask; + var tcs = new TaskCompletionSource(); + + _onTickTaskQueue.Enqueue(() => + { + try + { + task(); + tcs.SetResult(); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }); + + return tcs.Task; } /// @@ -121,9 +164,22 @@ public static void NextFrame(Action task) /// public static Task NextWorldUpdateAsync(Action task) { - var functionReference = FunctionReference.Create(task, FunctionLifetime.SingleUse); - NativeAPI.QueueTaskForNextWorldUpdate(functionReference); - return functionReference.CompletionTask; + var tcs = new TaskCompletionSource(); + + _onWorldUpdateTaskQueue.Enqueue(() => + { + try + { + task(); + tcs.SetResult(); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }); + + return tcs.Task; } /// diff --git a/managed/CounterStrikeSharp.Tests.Native/FrameSchedulingTests.cs b/managed/CounterStrikeSharp.Tests.Native/FrameSchedulingTests.cs index 3665bc492..ecca5c278 100644 --- a/managed/CounterStrikeSharp.Tests.Native/FrameSchedulingTests.cs +++ b/managed/CounterStrikeSharp.Tests.Native/FrameSchedulingTests.cs @@ -10,13 +10,24 @@ namespace NativeTestsPlugin; public class FrameSchedulingTests { + [Fact] + public async Task QueueTaskForNextFrame_RunsOnMainThread() + { + await Task.Run(async () => + { + await Task.Delay(10); + Assert.NotEqual(Thread.CurrentThread.ManagedThreadId, NativeTestsPlugin.gameThreadId); + + await Server.NextFrameAsync(() => { Assert.Equal(Thread.CurrentThread.ManagedThreadId, NativeTestsPlugin.gameThreadId); }); + }); + } + [Fact] public async Task QueueTaskForNextFrame_ExecutesCallback() { var mock = new Mock(); - var callback = FunctionReference.Create(mock.Object); - NativeAPI.QueueTaskForNextFrame(callback); + Server.NextFrame(mock.Object); await WaitOneFrame(); mock.Verify(s => s(), Times.Once); @@ -39,13 +50,28 @@ public async Task QueueTaskForFrame_ExecutesAtSpecifiedTick() mock.Verify(s => s(), Times.Once); } + [Fact] + public async Task QueueTaskForNextWorldUpdate_RunsOnMainThread() + { + await Task.Run(async () => + { + await Task.Delay(10); + Assert.NotEqual(Thread.CurrentThread.ManagedThreadId, NativeTestsPlugin.gameThreadId); + + await Server.NextWorldUpdateAsync(() => + { + Assert.Equal(Thread.CurrentThread.ManagedThreadId, NativeTestsPlugin.gameThreadId); + }); + }); + } + + [Fact] public async Task QueueTaskForNextWorldUpdate_ExecutesCallback() { var mock = new Mock(); - var callback = FunctionReference.Create(mock.Object); - NativeAPI.QueueTaskForNextWorldUpdate(callback); + Server.NextWorldUpdate(mock.Object); await WaitOneFrame(); mock.Verify(s => s(), Times.Once); @@ -116,8 +142,6 @@ public async Task NextFrameConcurrentQueueDrainsProperly() // All tasks should have been drained by latest NextFrameAsync await Server.NextFrameAsync(() => { }).ConfigureAwait(false); - // The task should be bucketed into multiple frames - Assert.All(callsByFrame.Values, count => Assert.Equal(1024, count)); Assert.Equal(4096, callCount); } @@ -139,8 +163,6 @@ public async Task NextWorldUpdateConcurrentQueueDrainsProperly() // All tasks should have been drained by latest NextFrameAsync await Server.NextWorldUpdateAsync(() => { }).ConfigureAwait(false); - // The task should be bucketed into multiple frames - Assert.All(callsByFrame.Values, count => Assert.Equal(1024, count)); Assert.Equal(4096, callCount); } } diff --git a/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs b/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs index 2e9a211dc..f0fc76944 100644 --- a/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs +++ b/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs @@ -38,7 +38,7 @@ public class NativeTestsPlugin : BasePlugin public override string ModuleDescription => "A an automated test plugin."; - private int gameThreadId; + public static int gameThreadId; public override void Load(bool hotReload) { diff --git a/managed/CounterStrikeSharp.Tests.Native/TestUtils.cs b/managed/CounterStrikeSharp.Tests.Native/TestUtils.cs index 7fb02fd17..101e1b944 100644 --- a/managed/CounterStrikeSharp.Tests.Native/TestUtils.cs +++ b/managed/CounterStrikeSharp.Tests.Native/TestUtils.cs @@ -5,7 +5,7 @@ public static class TestUtils { public static async Task WaitOneFrame() { - await Server.NextWorldUpdateAsync(() => { }).ConfigureAwait(false); + await Server.NextFrameAsync(() => { }).ConfigureAwait(false); } public static async Task WaitForSeconds(float seconds) diff --git a/src/core/managers/server_manager.cpp b/src/core/managers/server_manager.cpp index b399d8cf6..fe7637969 100644 --- a/src/core/managers/server_manager.cpp +++ b/src/core/managers/server_manager.cpp @@ -20,7 +20,6 @@ #include "scripting/callback_manager.h" #include "core/game_system.h" -#include SH_DECL_HOOK1_void(ISource2Server, ServerHibernationUpdate, SH_NOATTRIB, 0, bool); SH_DECL_HOOK0_void(ISource2Server, GameServerSteamAPIActivated, SH_NOATTRIB, 0); @@ -174,20 +173,6 @@ void ServerManager::UpdateWhenNotInGame(float flFrameTime) void ServerManager::PreWorldUpdate(bool bSimulating) { - std::vector> out_list(1024); - - auto size = m_nextWorldUpdateTasks.try_dequeue_bulk(out_list.begin(), 1024); - - if (size > 0) - { - CSSHARP_CORE_TRACE("Executing queued tasks of size: {0} at time {1}", size, globals::getGlobalVars()->curtime); - - for (size_t i = 0; i < size; i++) - { - out_list[i](); - } - } - auto callback = globals::serverManager.on_server_pre_world_update; if (callback && callback->GetFunctionCount()) @@ -198,15 +183,6 @@ void ServerManager::PreWorldUpdate(bool bSimulating) } } -void ServerManager::AddTaskForNextWorldUpdate(std::function&& task) -{ - auto success = m_nextWorldUpdateTasks.enqueue(std::forward(task)); - if (!success) - { - CSSHARP_CORE_ERROR("Failed to enqueue task for next world update!"); - } -} - void ServerManager::OnPrecacheResources(IEntityResourceManifest* pResourceManifest) { CSSHARP_CORE_TRACE("Precache resources"); diff --git a/src/core/managers/server_manager.h b/src/core/managers/server_manager.h index 11b956588..185fe279f 100644 --- a/src/core/managers/server_manager.h +++ b/src/core/managers/server_manager.h @@ -19,7 +19,6 @@ #include "core/globals.h" #include "core/global_listener.h" #include "scripting/script_engine.h" -#include #include "core/game_system.h" @@ -35,7 +34,6 @@ class ServerManager : public GlobalClass void OnShutdown() override; void* GetEconItemSystem(); bool IsPaused(); - void AddTaskForNextWorldUpdate(std::function&& task); void OnPrecacheResources(IEntityResourceManifest* pResourceManifest); ScriptCallback* on_server_pre_entity_think; @@ -59,8 +57,6 @@ class ServerManager : public GlobalClass ScriptCallback* on_server_pre_world_update; ScriptCallback* on_server_precache_resources; - - moodycamel::ConcurrentQueue> m_nextWorldUpdateTasks{ 4096 }; }; } // namespace counterstrikesharp diff --git a/src/mm_plugin.cpp b/src/mm_plugin.cpp index f30eee495..04940f8e4 100644 --- a/src/mm_plugin.cpp +++ b/src/mm_plugin.cpp @@ -54,9 +54,7 @@ DLL_EXPORT void InvokeNative(counterstrikesharp::fxNativeContext& context) { if (context.nativeIdentifier == 0) return; - if (context.nativeIdentifier != counterstrikesharp::hash_string_const("QUEUE_TASK_FOR_NEXT_FRAME") && - context.nativeIdentifier != counterstrikesharp::hash_string_const("QUEUE_TASK_FOR_NEXT_WORLD_UPDATE") && - context.nativeIdentifier != counterstrikesharp::hash_string_const("QUEUE_TASK_FOR_FRAME") && + if (context.nativeIdentifier != counterstrikesharp::hash_string_const("QUEUE_TASK_FOR_FRAME") && counterstrikesharp::globals::gameThreadId != std::this_thread::get_id()) { counterstrikesharp::ScriptContextRaw scriptContext(context); @@ -247,15 +245,6 @@ void CounterStrikeSharpMMPlugin::AllPluginsLoaded() on_metamod_all_plugins_loaded_callback->Execute(); } -void CounterStrikeSharpMMPlugin::AddTaskForNextFrame(std::function&& task) -{ - auto success = m_nextTasks.enqueue(std::forward(task)); - if (!success) - { - CSSHARP_CORE_ERROR("Failed to enqueue task for next frame!"); - } -} - void CounterStrikeSharpMMPlugin::Hook_GameFrame(bool simulating, bool bFirstTick, bool bLastTick) { /** @@ -267,20 +256,6 @@ void CounterStrikeSharpMMPlugin::Hook_GameFrame(bool simulating, bool bFirstTick // VPROF_BUDGET("CS#::Hook_GameFrame", "CS# On Frame"); globals::timerSystem.OnGameFrame(simulating); - std::vector> out_list(1024); - - auto size = m_nextTasks.try_dequeue_bulk(out_list.begin(), 1024); - - if (size > 0) - { - CSSHARP_CORE_TRACE("Executing queued tasks of size: {0} on tick number {1}", size, globals::getGlobalVars()->tickcount); - - for (size_t i = 0; i < size; i++) - { - out_list[i](); - } - } - auto callbacks = globals::tickScheduler.getCallbacks(globals::getGlobalVars()->tickcount); if (callbacks.size() > 0) { diff --git a/src/mm_plugin.h b/src/mm_plugin.h index 925884b67..129c352e1 100644 --- a/src/mm_plugin.h +++ b/src/mm_plugin.h @@ -25,7 +25,6 @@ #include #include #include "entitysystem.h" -#include "concurrentqueue.h" namespace counterstrikesharp { class ScriptCallback; @@ -49,7 +48,6 @@ class CounterStrikeSharpMMPlugin : public ISmmPlugin, public IMetamodListener void OnLevelShutdown() override; void Hook_GameFrame(bool simulating, bool bFirstTick, bool bLastTick); void Hook_StartupServer(const GameSessionConfiguration_t& config, ISource2WorldSession*, const char*); - void AddTaskForNextFrame(std::function&& task); void Hook_RegisterLoopMode(const char* pszLoopModeName, ILoopModeFactory* pLoopModeFactory, void** ppGlobalPointer); int Hook_LoadEventsFromFile(const char* filename, bool bSearchAll); @@ -64,9 +62,6 @@ class CounterStrikeSharpMMPlugin : public ISmmPlugin, public IMetamodListener const char* GetVersion() override; const char* GetDate() override; const char* GetLogTag() override; - - private: - moodycamel::ConcurrentQueue> m_nextTasks{ 4096 }; }; static ScriptCallback* on_activate_callback; diff --git a/src/scripting/natives/natives_engine.cpp b/src/scripting/natives/natives_engine.cpp index 2a30fc7be..f03616025 100644 --- a/src/scripting/natives/natives_engine.cpp +++ b/src/scripting/natives/natives_engine.cpp @@ -146,26 +146,6 @@ float GetSoundDuration(ScriptContext& script_context) double GetTickedTime(ScriptContext& script_context) { return globals::timerSystem.GetTickedTime(); } -void QueueTaskForNextFrame(ScriptContext& script_context) -{ - auto func = script_context.GetArgument(0); - - typedef void(voidfunc)(void); - globals::mmPlugin->AddTaskForNextFrame([func]() { - reinterpret_cast(func)(); - }); -} - -void QueueTaskForNextWorldUpdate(ScriptContext& script_context) -{ - auto func = script_context.GetArgument(0); - - typedef void(voidfunc)(void); - globals::serverManager.AddTaskForNextWorldUpdate([func]() { - reinterpret_cast(func)(); - }); -} - void QueueTaskForFrame(ScriptContext& script_context) { auto tick = script_context.GetArgument(0); @@ -289,8 +269,6 @@ REGISTER_NATIVES(engine, { // ScriptEngine::RegisterNativeHandler("EMIT_SOUND", EmitSound); ScriptEngine::RegisterNativeHandler("GET_TICKED_TIME", GetTickedTime); - ScriptEngine::RegisterNativeHandler("QUEUE_TASK_FOR_NEXT_FRAME", QueueTaskForNextFrame); - ScriptEngine::RegisterNativeHandler("QUEUE_TASK_FOR_NEXT_WORLD_UPDATE", QueueTaskForNextWorldUpdate); ScriptEngine::RegisterNativeHandler("QUEUE_TASK_FOR_FRAME", QueueTaskForFrame); ScriptEngine::RegisterNativeHandler("GET_VALVE_INTERFACE", GetValveInterface); ScriptEngine::RegisterNativeHandler("GET_COMMAND_PARAM_VALUE", GetCommandParamValue); diff --git a/src/scripting/natives/natives_engine.yaml b/src/scripting/natives/natives_engine.yaml index a283eddb6..b243f2907 100644 --- a/src/scripting/natives/natives_engine.yaml +++ b/src/scripting/natives/natives_engine.yaml @@ -22,9 +22,7 @@ TRACE_FILTER_PROXY_SET_TRACE_TYPE_CALLBACK: trace_filter:pointer, callback:point TRACE_FILTER_PROXY_SET_SHOULD_HIT_ENTITY_CALLBACK: trace_filter:pointer, callback:pointer -> void NEW_TRACE_RESULT: -> pointer GET_TICKED_TIME: -> double -QUEUE_TASK_FOR_NEXT_FRAME: callback:func -> void QUEUE_TASK_FOR_FRAME: tick:int, callback:func -> void -QUEUE_TASK_FOR_NEXT_WORLD_UPDATE: callback:func -> void GET_VALVE_INTERFACE: interfaceType:int, interfaceName:string -> pointer GET_COMMAND_PARAM_VALUE: param:string, dataType:DataType_t, defaultValue:any -> any PRINT_TO_SERVER_CONSOLE: msg:string -> void