diff --git a/1.3/Assemblies/1Fishery.dll b/1.3/Assemblies/1Fishery.dll deleted file mode 100644 index d5c8445..0000000 Binary files a/1.3/Assemblies/1Fishery.dll and /dev/null differ diff --git a/1.3/Assemblies/PerformanceFish.dll b/1.3/Assemblies/PerformanceFish.dll deleted file mode 100644 index 812d07a..0000000 Binary files a/1.3/Assemblies/PerformanceFish.dll and /dev/null differ diff --git a/1.3/Assemblies/System.Runtime.CompilerServices.Unsafe.dll b/1.3/Assemblies/System.Runtime.CompilerServices.Unsafe.dll deleted file mode 100644 index c5ba4e4..0000000 Binary files a/1.3/Assemblies/System.Runtime.CompilerServices.Unsafe.dll and /dev/null differ diff --git a/1.4/Assemblies/0PrepatcherAPI.dll b/1.4/Assemblies/0PrepatcherAPI.dll deleted file mode 100644 index d869ece..0000000 Binary files a/1.4/Assemblies/0PrepatcherAPI.dll and /dev/null differ diff --git a/1.4/Assemblies/1Fishery.dll b/1.4/Assemblies/1Fishery.dll deleted file mode 100644 index 25f63b3..0000000 Binary files a/1.4/Assemblies/1Fishery.dll and /dev/null differ diff --git a/1.4/Assemblies/1Fishery.pdb b/1.4/Assemblies/1Fishery.pdb deleted file mode 100644 index 0904abb..0000000 Binary files a/1.4/Assemblies/1Fishery.pdb and /dev/null differ diff --git a/1.4/Assemblies/PerformanceFish.dll b/1.4/Assemblies/PerformanceFish.dll index 52892d3..ddf6940 100644 Binary files a/1.4/Assemblies/PerformanceFish.dll and b/1.4/Assemblies/PerformanceFish.dll differ diff --git a/1.4/Assemblies/PerformanceFish.pdb b/1.4/Assemblies/PerformanceFish.pdb index a6e4688..76ee268 100644 Binary files a/1.4/Assemblies/PerformanceFish.pdb and b/1.4/Assemblies/PerformanceFish.pdb differ diff --git a/1.4/Assemblies/System.Runtime.CompilerServices.Unsafe.dll b/1.4/Assemblies/System.Runtime.CompilerServices.Unsafe.dll deleted file mode 100644 index c5ba4e4..0000000 Binary files a/1.4/Assemblies/System.Runtime.CompilerServices.Unsafe.dll and /dev/null differ diff --git a/1.4/Defs/GasDefs.xml b/1.4/Defs/GasDefs.xml new file mode 100644 index 0000000..6d2ec84 --- /dev/null +++ b/1.4/Defs/GasDefs.xml @@ -0,0 +1,42 @@ + + + + BlindSmoke + + 4 + false + (200,200,200) + + + + ToxGas + + 3 + true + (180,214,24) + + + + RotStink + + 4 + true + (214,90,24) + + + + FirstTestGas + + 2 + false + (229,52,235) + + + + SecondTestGas + + 1 + true + (52,113,235) + + \ No newline at end of file diff --git a/1.4/Defs/MainButtonDefs.xml b/1.4/Defs/MainButtonDefs.xml new file mode 100644 index 0000000..ab0b83a --- /dev/null +++ b/1.4/Defs/MainButtonDefs.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/About/About.xml b/About/About.xml index 3f7aa32..9e3e869 100644 --- a/About/About.xml +++ b/About/About.xml @@ -3,7 +3,6 @@ Performance Fish bs.performance -
  • 1.3
  • 1.4
  • bradson diff --git a/Source/PerformanceFish/AccessToolsCaching.cs b/Source/PerformanceFish/AccessToolsCaching.cs index 80912db..ed46b9f 100644 --- a/Source/PerformanceFish/AccessToolsCaching.cs +++ b/Source/PerformanceFish/AccessToolsCaching.cs @@ -7,9 +7,9 @@ using System.Linq; namespace PerformanceFish; -public class AccessToolsCaching : ClassWithFishPatches +public sealed class AccessToolsCaching : ClassWithFishPatches { - public class AllTypes : FishPatch + public sealed class AllTypes : FishPatch { public override string? Description { get; } = "Caches AccessTools.AllTypes to only recalculate when the " + "loaded assembly count changes. Improves load times."; diff --git a/Source/PerformanceFish/AllocationProfiling.cs b/Source/PerformanceFish/AllocationProfiling.cs new file mode 100644 index 0000000..7a70edd --- /dev/null +++ b/Source/PerformanceFish/AllocationProfiling.cs @@ -0,0 +1,207 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#if notWorking + +using System.Linq; +using System.Reflection.Emit; +using JetBrains.Annotations; + +namespace PerformanceFish; + +[UsedImplicitly] +public sealed class AllocationProfiling : ClassWithFishPatches +{ + public override bool RequiresLoadedGameForPatching => false; + + public static bool Active + { + get => _active; + set + { + return; + _active = value; + FishPatch.Get().Enabled = value; + } + } + + public static FishTable Data = new(); + private static bool _active; + + /// + /// The idea here was to patch object..ctor, fetch the type, count calls and get the allocation count for every + /// type by doing so, while ignoring structs which are expected to not construct as full object with type. + /// This however doesn't work. The IL method bodies of class constructors do contain calls to object..ctor, but + /// they appear to only go through for static constructor invocations and literal object construction. Everything + /// else somehow skips the call. + /// + public sealed class ObjectPatch : FishPatch + { + public override bool DefaultState => true; + + public override MethodBase? TargetMethodInfo { get; } + = MainTabWindow._objectConstructor; + // = AccessTools.Constructor(typeof(NotAClass)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void P0stf1x(object __instance) + { + CallCount++; + + ref var typeData = ref Data.GetOrAddReference(__instance.GetType()); + typeData.Count++; + + // if ((typeData.Count & 1023) != 0) + // return; + + typeData.StackTraces.GetOrAddReference(StackTraceUtility.ExtractStackTrace())++; + } + + public static CodeInstructions Transpiler(CodeInstructions instructions) + { + var codes = instructions.AsOrToList(); + Log.Message(string.Join("\n", codes)); + var retIndex = codes.FindIndex(static code => code.opcode == OpCodes.Ret); + if (retIndex == -1) + retIndex = codes.Count - 1; + + codes.Insert(retIndex, new(OpCodes.Ldarg_0)); + codes.Insert(retIndex + 1, new(OpCodes.Call, methodof(P0stf1x))); + + return codes; + } + + // ReSharper disable once VirtualMemberCallInConstructor + //private ObjectPatch() => TryPatch(); + + public static class NotAClass + { + + } + } + + public static ulong CallCount; + + public record struct TypeData + { + public int Count; + public FishTable StackTraces = new(); + + [UsedImplicitly] + public TypeData() + { + } + } + + [UsedImplicitly] + public sealed class MainTabWindow : RimWorld.MainTabWindow + { + public MainTabWindow() + { + draggable = true; + resizeable = true; + absorbInputAroundWindow = false; + closeOnClickedOutside = false; + } + + private GUIScope.ScrollViewStatus _scrollViewStatus = new(); + private Listing_Standard _listing = new(); + + public override void PreOpen() + { + Active = true; + base.PreOpen(); + } + + public override void PostClose() + { + base.PostClose(); + Active = false; + } + + internal static MethodBase _objectConstructor + = (MethodBase)MethodBodyReader.GetInstructions(null, AccessTools.DeclaredConstructor(typeof(PreceptComp))) + .First(static code => code.opcode == OpCodes.Call + && code.operand is MethodBase method + && method.Name.Contains("ctor")) + .operand; + // = typeof(object).GetMembers(AccessTools.all).OfType() + // .First(static member => member.Name.Contains("ctor")); + // = AccessTools.Constructor(typeof(ObjectPatch.NotAClass)); + + public override void DoWindowContents(Rect inRect) + { + absorbInputAroundWindow = false; + closeOnClickedOutside = false; + + if (GUIHelper.CanSkip) + return; + + Widgets.Label(inRect with { height = Text.LineHeight }, $"Data: {Data}, count: { + Data.Count.ToStringCached()}, callCount: {CallCount.ToString()}"); + inRect.yMin += Text.LineHeight; + + var objectPatches = Harmony.GetPatchInfo(_objectConstructor); + + var patchWidgetLabel = $"Patches on {_objectConstructor.FullDescription()}: pre { + objectPatches?.Prefixes?.Select(static patch => patch.PatchMethod.FullDescription()).ToStringSafeEnumerable()}, post: { + objectPatches?.Postfixes?.Select(static patch => patch.PatchMethod.FullDescription()).ToStringSafeEnumerable()}, trans: { + objectPatches?.Transpilers?.Select(static patch => patch.PatchMethod.FullDescription()).ToStringSafeEnumerable()}, final: { + objectPatches?.Finalizers?.Select(static patch => patch.PatchMethod.FullDescription()).ToStringSafeEnumerable()}, Active? { + Active.ToString()}, Enabled? {FishPatch.Get().Enabled.ToString()}"; + + var patchWidgetHeight = Text.CalcHeight(patchWidgetLabel, inRect.width); + Widgets.Label(inRect with { height = patchWidgetHeight }, patchWidgetLabel); + inRect.yMin += patchWidgetHeight; + + var objectMembers = typeof(object).GetMembers(AccessTools.all).OfType() + .Where(static member => member.Name.Contains("ctor")); + var objectMembersLabel = $"object members:\n{string.Join("\n --- ", objectMembers.Select(static member + => member.FullDescription()))}"; + var objectMembersHeight = Text.CalcHeight(objectMembersLabel, inRect.width); + Widgets.Label(inRect with { height = objectMembersHeight }, objectMembersLabel); + inRect.yMin += objectMembersHeight; + + using var scrollView = new GUIScope.ScrollableListingStandard(inRect, _scrollViewStatus, _listing); + var listing = _listing; + + using var keyValuePairsPooled = new PooledArray>(Data); + keyValuePairsPooled.Sort(_typeDataComparer); + + var keyValuePairs = keyValuePairsPooled.Array; + var typeCount = keyValuePairsPooled.Length; + + for (var i = 0; i < typeCount; i++) + { + if (scrollView.TryCull(Text.LineHeight)) + continue; + + var kvp = keyValuePairs[i]; + var labelRect = listing.GetRect(Text.LineHeight); + + Widgets.Label(labelRect, + string.Concat(kvp.Key, " ", kvp.Value.Count.ToStringCached())); + + if (Mouse.IsOver(labelRect)) + { + var traces = kvp.Value.StackTraces; + using var traceArray = new PooledArray>(traces); + traceArray.Sort(_traceComparer); + // listing.Label(traceArray.Array[0].Key); + listing.Label(traceArray.FirstOrDefault(static kvp => !kvp.Key.Contains("cctor")).Key ?? "NULL"); + // TooltipHandler.TipRegion(labelRect, + // string.Join("\n", traceArray.Select(static kvp => kvp.Key))); + } + } + } + + private static IComparer> _typeDataComparer + = Comparer>.Create(static (x, y) => x.Value.Count.CompareTo(y.Value.Count)); + + private static IComparer> _traceComparer + = Comparer>.Create(static (x, y) => x.Value.CompareTo(y.Value)); + } +} +#endif \ No newline at end of file diff --git a/Source/PerformanceFish/AnalyzerFixes.cs b/Source/PerformanceFish/AnalyzerFixes.cs index 8dfe3b0..02084af 100644 --- a/Source/PerformanceFish/AnalyzerFixes.cs +++ b/Source/PerformanceFish/AnalyzerFixes.cs @@ -15,17 +15,31 @@ public static class AnalyzerFixes { public static void Patch() { - var modBase = AccessTools.Constructor(Reflection.Type("PerformanceAnalyzer", "Analyzer.Modbase"), - new[] { typeof(ModContentPack) }); - if (modBase is null) - return /*false*/; + var entryPoint = AccessTools.DeclaredMethod(ModCompatibility.Types.PerformanceAnalyzer.Window_Analyzer, + "LoadEntries") as MethodBase; - var success = PerformanceFishMod.Harmony!.Patch(modBase, postfix: new(methodof(XmlParser.CollectXmlData))) - != null; + var prefixEntryPoint = false; + if (entryPoint != null) + { + prefixEntryPoint = true; + } + else + { + entryPoint = AccessTools.DeclaredConstructor(ModCompatibility.Types.PerformanceAnalyzer.ModBase, + [typeof(ModContentPack)]); + } + + if (entryPoint is null) + return /*false*/; + + var entryPatchMethod = new HarmonyMethod(methodof(XmlParser.CollectXmlData)); + + var success = PerformanceFishMod.Harmony!.Patch(entryPoint, prefix: prefixEntryPoint ? entryPatchMethod : null, + postfix: prefixEntryPoint ? null : entryPatchMethod) != null; var methodTransplanting = Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.MethodTransplanting", "UpdateMethod", - new[] { typeof(Type), typeof(MethodInfo) }); + [typeof(Type), typeof(MethodInfo)]); if (methodTransplanting is null || PerformanceFishMod.Harmony.Patch(methodTransplanting, transpiler: new(methodof(UpdateMethod_Transpiler))) @@ -145,7 +159,7 @@ public static class AnalyzerFixes public static class OverridingMethodUtility { - public static HashSet PatchedOverrides = new(); + public static HashSet PatchedOverrides = []; public static void ClearCaches() => PatchedOverrides.Clear(); @@ -247,7 +261,7 @@ public static class AnalyzerFixes public static CodeInstructions Parse_Transpiler(CodeInstructions CodeInstructions) { - var codes = CodeInstructions.ToList(); + var codes = CodeInstructions.AsOrToList(); var parseMethod = FishTranspiler.Call("PerformanceAnalyzer", "Analyzer.Profiling.XmlParser", "ParseMethod"); var errorMethod = FishTranspiler.Call("PerformanceAnalyzer", "Analyzer.Profiling.ThreadSafeLogger", "Error"); @@ -357,7 +371,7 @@ public static class AnalyzerFixes private static Action MakeGUIController_AddEntryMethod() { - var analyzer_Profiling_EntryType = Reflection.Type("PerformanceAnalyzer", "Analyzer.Profiling.Entry"); + var analyzer_Profiling_EntryType = ModCompatibility.Types.PerformanceAnalyzer.ProfilingEntry; if (analyzer_Profiling_EntryType == null) Log.Error("analyzer_Profiling_EntryType is null"); @@ -378,13 +392,13 @@ public static class AnalyzerFixes Log.Error("entry_Create is null");*/ var dictionary_Add = AccessTools.Method( - typeof(Dictionary<,>).MakeGenericType(new[] { analyzer_Profiling_EntryType, typeof(Type) }), - "Add", new[] { analyzer_Profiling_EntryType, typeof(Type) }); + typeof(Dictionary<,>).MakeGenericType([analyzer_Profiling_EntryType, typeof(Type)]), + "Add", [analyzer_Profiling_EntryType, typeof(Type)]); if (dictionary_Add == null) Log.Error("dictionary_Add is null"); var dynamicMethod = new DynamicMethod("GUIController_AddEntryMethod", typeof(void), - new[] { typeof(int), typeof(object), typeof(Type) }); + [typeof(int), typeof(object), typeof(Type)]); var il = dynamicMethod.GetILGenerator(); il.Emit(FishTranspiler.Argument(0)); //Category @@ -416,6 +430,7 @@ public static class AnalyzerFixes //Copied from Wiri's analyzer fork, with above fixes integrated. The steam version lacks this functionality public static class XmlParser { + [SuppressMessage("Security", "CA3075")] public static void CollectXmlData() { foreach (var dir in ModLister.AllActiveModDirs) @@ -484,7 +499,7 @@ public static class AnalyzerFixes var type = DynamicTypeBuilder_CreateType(node.Name, methods); var entry = Entry_Create(node.Name, category, type, false, true); - if (tip != string.Empty) + if (!string.IsNullOrEmpty(tip)) Entry_tip(entry) = tip; GUIController_AddSpecificEntry(category, entry, type); diff --git a/Source/PerformanceFish/Cache/BitCellGrid.cs b/Source/PerformanceFish/Cache/BitCellGrid.cs new file mode 100644 index 0000000..9e99ea6 --- /dev/null +++ b/Source/PerformanceFish/Cache/BitCellGrid.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace PerformanceFish.Cache; + +public sealed class BitCellGrid +{ + private long[] _innerArray; + private CellIndices _cellIndices; + + public BitCellGrid(Map map) + { + _innerArray = null!; + _cellIndices = null!; + map.InvokeWhenCellIndicesReady(Initialize); + } + + private void Initialize(Map map) + { + _cellIndices = map.cellIndices; + _innerArray = new long[(Math.Max(0, _cellIndices.NumGridCells - 1) >> 6) + 1]; + } + + public bool this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (_innerArray[index >> 6] & (1L << (index & 63))) != 0L; + set + { + ref var bucket = ref _innerArray[index >> 6]; + bucket ^= (-(long)value.AsInt() ^ bucket) & (1L << (index & 63)); + } + } + + public bool this[in IntVec3 c] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this[c.CellToIndex(_cellIndices)]; + set => this[c.CellToIndex(_cellIndices)] = value; + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Cache/ByIndex.cs b/Source/PerformanceFish/Cache/ByIndex.cs index b50b7e3..600525d 100644 --- a/Source/PerformanceFish/Cache/ByIndex.cs +++ b/Source/PerformanceFish/Cache/ByIndex.cs @@ -194,7 +194,7 @@ public class ByIndex : ByIndex where T : notnull where TResult : new() { - private static FishTable, TResult> _get - = Utility.AddNew, TResult>(); + public int Key; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe ByInt(T key) => Key = FunctionPointers.IndexGetter.Default(key); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ByInt(int key) => Key = key; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(ByInt other) => Key == other.Key; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => Key; + + private static object _valueInitializerLock = new(); + + private static FishTable, TResult> _get = InitializeNew(); [ThreadStatic] private static FishTable, TResult>? _getThreadStatic; + private static Func, TResult>? _valueInitializer; + + public static Func, TResult>? ValueInitializer + { + get + { + lock (_valueInitializerLock) + return _valueInitializer; + } + set + { + lock (_valueInitializerLock) + { + value ??= static _ => Reflection.New(); + _valueInitializer = value; + _get.ValueInitializer = value; + } + } + } + public static FishTable, TResult> Get { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _getThreadStatic ??= Utility.AddNew, TResult>(); + get => _getThreadStatic ??= InitializeNew(); } public static FishTable, TResult> GetDirectly @@ -40,6 +75,15 @@ public record struct ByInt where T : notnull where TResult : new() [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref TResult GetOrAddReference(ref ByInt key) => ref Get.GetOrAddReference(ref key); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TResult GetOrAdd(int key) => Get.GetOrAdd(Unsafe.As>(ref key)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TResult GetOrAdd(ByInt key) => Get.GetOrAdd(key); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TResult GetOrAdd(ref ByInt key) => Get.GetOrAdd(ref key); + [MethodImpl(MethodImplOptions.NoInlining)] public static ref TResult GetExistingReference(int key) => ref Get.GetReference(Unsafe.As>(ref key)); @@ -47,19 +91,8 @@ public record struct ByInt where T : notnull where TResult : new() [MethodImpl(MethodImplOptions.NoInlining)] public static ref TResult GetExistingReference(T key) => ref Get.GetReference(new(key)); - public int Key; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe ByInt(T key) => Key = FunctionPointers.IndexGetter.Default(key); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ByInt(int key) => Key = key; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Equals(ByInt other) => Key == other.Key; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode() => Key; + [MethodImpl(MethodImplOptions.NoInlining)] + private static FishTable, TResult> InitializeNew() => Utility.AddNew(ValueInitializer); } [PublicAPI] diff --git a/Source/PerformanceFish/Cache/ByMap.cs b/Source/PerformanceFish/Cache/ByMap.cs index ed800bc..7c02aeb 100644 --- a/Source/PerformanceFish/Cache/ByMap.cs +++ b/Source/PerformanceFish/Cache/ByMap.cs @@ -29,9 +29,9 @@ public static class ByMap where T : new() private static Func Initialize { get; } = typeof(MapComponent).IsAssignableFrom(typeof(T)) ? AccessTools.MethodDelegate>(AccessTools.Method(typeof(Map), nameof(Map.GetComponent), - Type.EmptyTypes, new[] { typeof(T) })) + Type.EmptyTypes, [typeof(T)])) // missing T constraints cause the compiler to complain when trying to just use map => map.GetComponent - : static _ => new(); + : static _ => Reflection.New(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T GetFor(Map map) @@ -39,7 +39,7 @@ public static class ByMap where T : new() var cache = GetCache; for (var i = 0; i < cache.Count; i++) { - ref var cacheEntry = ref cache.GetReferenceUnverifiable(i); + ref var cacheEntry = ref cache.GetReferenceUnsafe(i); if (cacheEntry.map == map) return cacheEntry.value; @@ -59,7 +59,7 @@ public static class ByMap where T : new() var cache = GetCache; for (var i = 0; i < cache.Count; i++) { - ref var cacheEntry = ref cache.GetReferenceUnverifiable(i); + ref var cacheEntry = ref cache.GetReferenceUnsafe(i); if (cacheEntry.map == map) return ref cacheEntry.value; @@ -74,7 +74,10 @@ public static class ByMap where T : new() } private static bool EntryValid(int i, Map map) - => Find.Maps.Count > i && Find.Maps.Contains(map); + { + var maps = Current.Game.Maps; + return maps.Count > i && maps.Contains(map); + } private static T AddEntry(Map map, List<(Map map, T value)> cache) { @@ -85,6 +88,6 @@ public static class ByMap where T : new() private static ref T AddEntryByRef(Map map, List<(Map map, T value)> cache) { cache.Add((map, Initialize(map))); - return ref cache.GetReferenceUnverifiable(cache.Count - 1).value; + return ref cache.GetReferenceUnsafe(cache.Count - 1).value; } } \ No newline at end of file diff --git a/Source/PerformanceFish/Cache/ByReference.cs b/Source/PerformanceFish/Cache/ByReference.cs index 15e3138..3a19181 100644 --- a/Source/PerformanceFish/Cache/ByReference.cs +++ b/Source/PerformanceFish/Cache/ByReference.cs @@ -134,7 +134,7 @@ public static class ByReferenceUnclearable Log.Error($"{e}"); } - return result ?? throw new(); + return result ?? ThrowHelper.ThrowInvalidOperationException(); } [SecuritySafeCritical] @@ -153,6 +153,7 @@ public static class ByReferenceUnclearable cacheCopy.Task = null; } // ReSharper disable once SuspiciousTypeConversion.Global + // ReSharper disable once PatternNeverMatches else if (centralCache.Task is Countdown taskCountdown) { Interlocked.Decrement(ref taskCountdown.Value); @@ -216,7 +217,7 @@ public static class ByReferenceUnclearable Log.Error($"{e}"); } - return result ?? throw new(); + return result ?? ThrowHelper.ThrowInvalidOperationException(); } public static unsafe void Initialize() @@ -239,17 +240,39 @@ public static class ByReferenceUnclearable public static class ByReference where T : notnull where TResult : new() { + private static object _valueInitializerLock = new(); + public static object SyncLock = new(); - private static FishTable _get = Utility.AddNew(); + private static FishTable _get = InitializeNew(); [ThreadStatic] private static FishTable? _getThreadStatic; + private static Func? _valueInitializer; + + public static Func? ValueInitializer + { + get + { + lock (_valueInitializerLock) + return _valueInitializer; + } + set + { + lock (_valueInitializerLock) + { + value ??= static _ => Reflection.New(); + _valueInitializer = value; + _get.ValueInitializer = value; + } + } + } + public static FishTable Get { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _getThreadStatic ??= Utility.AddNew(); + get => _getThreadStatic ??= InitializeNew(); } public static FishTable GetDirectly @@ -258,6 +281,12 @@ public static class ByReference get => _get; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TResult GetOrAdd(T key) => Get.GetOrAdd(key); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TResult GetOrAdd(ref T key) => Get.GetOrAdd(ref key); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref TResult GetOrAddReference(T key) => ref Get.GetOrAddReference(key); @@ -266,6 +295,12 @@ public static class ByReference [MethodImpl(MethodImplOptions.NoInlining)] public static ref TResult GetExistingReference(T key) => ref Get.GetReference(key); + + public static void Remove(T key) + { + Get.Remove(key); + GetDirectly.Remove(key); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Update(T key) where VResult : TResult, ICacheable @@ -355,7 +390,7 @@ public static class ByReference Log.Error($"{e}"); } - return result ?? throw new(); + return result ?? ThrowHelper.ThrowInvalidOperationException(); } public static Task RequestFromCacheAsync(T key, TArgument2 second) @@ -406,7 +441,7 @@ public static class ByReference Log.Error($"{e}"); } - return result ?? throw new(); + return result ?? ThrowHelper.ThrowInvalidOperationException(); } public static unsafe void Initialize() @@ -423,6 +458,9 @@ public static class ByReference typeof(ByReference).FullName}\n{ex}"); } } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static FishTable InitializeNew() => Utility.AddNew(ValueInitializer); } [PublicAPI] @@ -528,6 +566,7 @@ public record struct ByReference : IInitializable } [MethodImpl(MethodImplOptions.AggressiveInlining)] + // ReSharper disable once ConvertToPrimaryConstructor public ByReference(T1 first, T2 second) { First = first; diff --git a/Source/PerformanceFish/Cache/CellGrid.cs b/Source/PerformanceFish/Cache/CellGrid.cs new file mode 100644 index 0000000..d83ad3f --- /dev/null +++ b/Source/PerformanceFish/Cache/CellGrid.cs @@ -0,0 +1,46 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Diagnostics.CodeAnalysis; + +namespace PerformanceFish.Cache; + +public sealed class CellGrid +{ + private T[] _innerArray; + private CellIndices _cellIndices; + + public CellGrid(Map map) + { + _innerArray = null!; + _cellIndices = null!; + map.InvokeWhenCellIndicesReady(Initialize); + } + + [MemberNotNull(nameof(_innerArray), nameof(_cellIndices))] + private void Initialize(Map map) + { + _cellIndices = map.cellIndices; + _innerArray = new T[_cellIndices.NumGridCells]; + } + + public ref T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref _innerArray[index]; + } + + public ref T this[in IntVec3 c] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref _innerArray[c.CellToIndex(_cellIndices)]; + } + + public ref T this[CellIndex cellIndex] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref _innerArray[cellIndex.Value]; + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Cache/ICacheable.cs b/Source/PerformanceFish/Cache/ICacheable.cs index c2ac64d..03d35b0 100644 --- a/Source/PerformanceFish/Cache/ICacheable.cs +++ b/Source/PerformanceFish/Cache/ICacheable.cs @@ -134,7 +134,7 @@ public interface IAsyncCacheable : IAsyncCacheable MakeResultAsync(TKey key, T2 second); } -internal record Countdown(int Value) +internal sealed record Countdown(int Value) { public volatile int Value = Value; } \ No newline at end of file diff --git a/Source/PerformanceFish/Cache/Utility.cs b/Source/PerformanceFish/Cache/Utility.cs index d9f2d67..4a9d420 100644 --- a/Source/PerformanceFish/Cache/Utility.cs +++ b/Source/PerformanceFish/Cache/Utility.cs @@ -5,16 +5,16 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using System.Reflection.Emit; using JetBrains.Annotations; +using PerformanceFish.Events; namespace PerformanceFish.Cache; public static class Utility { - public static event Action? ThingDestroyed; // TODO - public static event Action? Cleared; private static volatile int _destroyedThingCounter, _lastGCTick, _gcRate = 1000; @@ -24,11 +24,21 @@ public static class Utility private static ICollection[]? _cachesToProcessForGC; private static object _gcLock = new(); + + public static ConcurrentBag All { get; } = []; - public static void NotifyThingDestroyed(Thing thing) + public static void Clear() { - ThingDestroyed?.Invoke(thing); + foreach (var cache in All) + cache.Clear(); + + Cleared?.Invoke(); + } + internal static void Initialize() => ThingEvents.Destroyed += TryContinueGC; + + public static void TryContinueGC(Thing thing) + { if (Interlocked.Increment(ref _destroyedThingCounter) < 100 || Current.programStateInt != ProgramState.Playing || TickHelper.TicksGame - _lastGCTick < _gcRate) @@ -77,48 +87,59 @@ public static class Utility var count = 0; for (var i = collection.Length; i-- > 0;) { - if (collection.Length > 0) + if (collection[i].Count > 0) count++; } return count; } - public static ConcurrentBag All { get; } = new(); - - public static void Clear() - { - foreach (var cache in All) - cache.Clear(); - - Cleared?.Invoke(); - } - public static void LogCurrentCacheUtilization() { - var allCaches = All.ToArray(); - Log.Message($"Total utilized caches: {allCaches.CountNonEmpty()}\n\n" - + $"Individual counts:\n{string.Join("\n", - allCaches.Select(static cache => cache.GetType()).Distinct().Select(type - => $"{type.FullDescription()} :: {allCaches.Where(cache => cache.GetType() == type) - .Select(static cache => cache.Count).Sum()}{TryCalculateSize( - type, allCaches)}"))}\n============================================================="); + var allCaches = All.Where(static c => c.Count > 0).ToArray(); + var allCacheTypes = allCaches.Select(static cache => cache.GetType()).Distinct(); + var cacheData = allCacheTypes.Select(type + => + { + var cachesOfType = allCaches.Where(cache => cache.GetType() == type).ToList(); + return (type, cachesOfType.Select(static cache => cache.Count).Sum(), + TryCalculateSize(type, cachesOfType)); + }).ToArray(); + + Array.Sort(cacheData, static (x, y) + => y.Item3 is { } size ? size.CompareTo(x.Item3 ?? 0L) + : y.Item2 != x.Item2 ? y.Item2.CompareTo(x.Item2) + : string.Compare(x.Item1.ToString(), y.Item1.ToString(), StringComparison.Ordinal)); + + Log.Message($"Total utilized caches: {allCaches.Length} ({ + ToByteString(cacheData.Select(static tuple => tuple.Item3 ?? 0L).Sum())})\n\n" + + $"Individual counts:\n{string.Join("\n", cacheData.Select(static tuple + => $"{tuple.Item1.FullDescription()} :: {tuple.Item2.ToStringCached()}{ + (tuple.Item3 is { } size ? $" ({ToByteString(size)})" + : "")}"))}\n============================================================="); } - private static string TryCalculateSize(Type type, ICollection[] allCaches) - => (type.GetProperty(nameof(FishTable.SizeEstimate))?.GetMethod + private static long? TryCalculateSize(Type type, IEnumerable cachesOfType) + { + var sizeGetter = type.GetProperty(nameof(FishTable.SizeEstimate))?.GetMethod ?? (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>) ? AccessTools.Method(typeof(FisheryLib.CollectionExtensions), nameof(FisheryLib.CollectionExtensions.GetSizeEstimate), generics: type.GetGenericArguments()) - : null)) is { } sizeGetter - ? $" ({(allCaches.Where(cache => cache.GetType() == type) - .Select(cache => (long)(uint)sizeGetter.Invoke(sizeGetter.IsStatic ? null : cache, - sizeGetter.IsStatic ? new object[] { cache } : null)).Sum() - is var sizeInBytes - && sizeInBytes > 1024L * 1024L ? $"{Math.Round(sizeInBytes / (1024d * 1024d), 2)} MiB" - : sizeInBytes > 1024L ? $"{Math.Round(sizeInBytes / 1024d, 2)} KiB" - : $"{sizeInBytes} B")})" - : ""; + : null); + + return sizeGetter is null + ? null + : cachesOfType.Select(cache + => (long)(uint)sizeGetter.Invoke(sizeGetter.IsStatic ? null : cache, + sizeGetter.IsStatic ? [cache] : null)).Sum(); + } + + private static string ToByteString(long sizeInBytes) + => sizeInBytes > 1024L * 1024L + ? $"{Math.Round(sizeInBytes / (1024d * 1024d), 2).ToString(CultureInfo.CurrentCulture)} MiB" + : sizeInBytes > 1024L + ? $"{Math.Round(sizeInBytes / 1024d, 2).ToString(CultureInfo.CurrentCulture)} KiB" + : $"{((int)sizeInBytes).ToStringCached()} B"; private static void Clear(this ICollection cache) { @@ -167,14 +188,29 @@ public static class Utility } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public static FishTable AddNew(Action>? onEntryAdded = null, + public static FishTable AddNew(Func? valueInitializer = null, + Action>? onEntryAdded = null, Action>? onEntryRemoved = null) { var newCollection = new FishTable(); + + if (valueInitializer != null) + newCollection.ValueInitializer = valueInitializer; if (onEntryAdded != null) newCollection.EntryAdded += onEntryAdded; + if (typeof(TKey).IsAssignableTo(typeof(Thing))) + { + newCollection.EntryAdded + += entry => + { + var data = new OnDestroyActionData(entry.Key, newCollection); + data.Action = data.Invoke; + data.Thing.Events().Destroyed += data.Action; + }; + } + if (onEntryRemoved != null) newCollection.EntryRemoved += onEntryRemoved; @@ -182,6 +218,26 @@ public static class Utility return newCollection; } + private sealed record OnDestroyActionData(TKey Key, FishTable Collection) + { + public Action? Action; + public Thing Thing => Unsafe.As(Key); + + public void Invoke(Thing thing) + { + Collection.Remove(Key); + Thing.Events().Destroyed -= Action; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static FishTable AddNew(Func> collectionInitializer) + { + var newCollection = collectionInitializer(); + All.Add(newCollection); + return newCollection; + } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static T AddNewUnclearable() where T : class, ICollection, new() { @@ -229,48 +285,13 @@ public interface IClearable public void Clear(); } -public interface IFishBool -{ - public bool Value - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get; - } -} - -public struct FishBool : IFishBool -{ - private byte _value; - - public bool Value - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _value == 1; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => _value = value ? (byte)1 : (byte)0; - } - - public struct True : IFishBool - { - public bool Value => true; - } - - public struct False : IFishBool - { - public bool Value => false; - } -} - [PublicAPI] -public class CollectionRef : IClearable, ICollection where T : ICollection +public sealed class CollectionRef(AccessTools.FieldRef fieldRef) : IClearable, ICollection + where T : ICollection { - private readonly AccessTools.FieldRef _fieldRef; private static readonly ClearInvoker? _clearInvoker = CreateClearDelegate(); - public CollectionRef(AccessTools.FieldRef fieldRef) => _fieldRef = fieldRef; - - public ref T Get => ref _fieldRef(); + public ref T Get => ref fieldRef(); public void Create() => Get = Reflection.New(); @@ -304,7 +325,7 @@ public class CollectionRef : IClearable, ICollection where T : ICollection return null; var dm = new DynamicMethod($"ClearInvoker_{typeof(T).FullName}", null, - new[] { typeof(T).MakeByRefType() }, typeof(CollectionRef), true); + [typeof(T).MakeByRefType()], typeof(CollectionRef), true); var il = dm.GetILGenerator(); il.Emit(FishTranspiler.Argument(0)); diff --git a/Source/PerformanceFish/CellIndex.cs b/Source/PerformanceFish/CellIndex.cs new file mode 100644 index 0000000..dfbe248 --- /dev/null +++ b/Source/PerformanceFish/CellIndex.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace PerformanceFish; + +public record struct CellIndex(int Value) +{ + public int Value = Value; + + public CellIndex(IntVec3 cell, Map map) : this(cell, map.cellIndices.mapSizeX) {} + + public CellIndex(IntVec3 cell, int mapSizeX) : this(cell.CellToIndex(mapSizeX)) {} + + public IntVec3 ToCell(Map map) => ToCell(map.cellIndices.mapSizeX); + + public IntVec3 ToCell(int mapSizeX) => CellIndicesUtility.IndexToCell(Value, mapSizeX); + + public List GetThingList(Map map) => map.thingGrid.ThingsListAtFast(Value); +} \ No newline at end of file diff --git a/Source/PerformanceFish/ContentFinderCaching.cs b/Source/PerformanceFish/ContentFinderCaching.cs index 42d9c40..09d36fb 100644 --- a/Source/PerformanceFish/ContentFinderCaching.cs +++ b/Source/PerformanceFish/ContentFinderCaching.cs @@ -7,9 +7,9 @@ using PerformanceFish.Prepatching; namespace PerformanceFish; -public class ContentFinderCaching : ClassWithFishPrepatches +public sealed class ContentFinderCaching : ClassWithFishPrepatches { - public class Get_Patch : FishPrepatch + public sealed class Get_Patch : FishPrepatch { public override string? Description { get; } = "Caches texture lookups on the ContentFinder. This has a relatively large performance impact when used " diff --git a/Source/PerformanceFish/DDSLoader.cs b/Source/PerformanceFish/DDSLoader.cs index e7667a1..40dfc66 100644 --- a/Source/PerformanceFish/DDSLoader.cs +++ b/Source/PerformanceFish/DDSLoader.cs @@ -104,7 +104,7 @@ public static class DDSLoader if ((dds_pxlf_dwFlags & DDPF_FOURCC) != 0) { - // Texture dos not contain RGB data, check FourCC for format + // Texture does not contain RGB data, check FourCC for format isCompressed = true; if (FourCcEquals(dds_pxlf_dwFourCC, "DXT1")) diff --git a/Source/PerformanceFish/DebugActions.cs b/Source/PerformanceFish/DebugActions.cs new file mode 100644 index 0000000..8276624 --- /dev/null +++ b/Source/PerformanceFish/DebugActions.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#if RAINBOW_ERRORS +using JetBrains.Annotations; + +namespace PerformanceFish; + +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +public static class DebugActions +{ + [DebugOutput("Performance Fish", true, name = "Toggle Rainbow Errors")] + public static void SpamErrors() + { + lock (_debugLock) + { + if (_errorSpamEnabled) + { + _errorSpamEnabled = false; + Application.onBeforeRender -= _errorAction; + return; + } + + if (_random is null) + { + _random = new(); + _random.NextBytes(_colorBytes = new byte[3000]); + } + + _errorSpamEnabled = true; + Application.onBeforeRender += _errorAction; + } + } + + private static readonly object _debugLock = new(); + private static global::System.Random? _random; + private static bool _errorSpamEnabled; + private static byte[]? _colorBytes; + private static volatile int _logErrorCount = -1; + private static UnityEngine.Events.UnityAction _errorAction = () => + { + var i = Interlocked.Increment(ref _logErrorCount); + if (i >= 1000) + { + _random!.NextBytes(_colorBytes!); + _logErrorCount = 0; + i = 0; + } + + Log.Error("ERROR".Colorize(new ColorInt(_colorBytes![i], _colorBytes[i + 1000], _colorBytes[i + 2000]) + .ToColor)); + }; +} +#endif \ No newline at end of file diff --git a/Source/PerformanceFish/DeepResourceGridOptimization.cs b/Source/PerformanceFish/DeepResourceGridOptimization.cs new file mode 100644 index 0000000..5f6cc96 --- /dev/null +++ b/Source/PerformanceFish/DeepResourceGridOptimization.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; + +namespace PerformanceFish; + +public sealed class DeepResourceGridOptimization : ClassWithFishPrepatches +{ + public sealed class AnyActiveDeepScannersOnMapPatch : FishPrepatch + { + public override string? Description { get; } + = "Optimizes lookups for active deep scanners to directly return the result of cached data instead of " + + "iterating over all buildings on the map trying to find any matches"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(DeepResourceGrid), nameof(DeepResourceGrid.AnyActiveDeepScannersOnMap)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static bool ReplacementBody(DeepResourceGrid __instance) + { + foreach (var item in __instance.map.listerBuildings.Cache().ColonistDeepScanners) + { + var compDeepScanner = item.TryGetComp(); + if (compDeepScanner != null && compDeepScanner.ShouldShowDeepResourceOverlay()) + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/DefDatabasePatches.cs b/Source/PerformanceFish/Defs/DefDatabasePatches.cs similarity index 88% rename from Source/PerformanceFish/DefDatabasePatches.cs rename to Source/PerformanceFish/Defs/DefDatabasePatches.cs index 6b9367d..5a97455 100644 --- a/Source/PerformanceFish/DefDatabasePatches.cs +++ b/Source/PerformanceFish/Defs/DefDatabasePatches.cs @@ -8,11 +8,11 @@ using Mono.Cecil.Cil; using PerformanceFish.Prepatching; -namespace PerformanceFish; +namespace PerformanceFish.Defs; -public class DefDatabasePatches : ClassWithFishPrepatches +public sealed class DefDatabasePatches : ClassWithFishPrepatches { - public class GetNamedPatch : FishPrepatch + public sealed class GetNamedPatch : FishPrepatch { public override MethodBase TargetMethodBase { get; } = methodof(DefDatabase.GetNamed); @@ -36,7 +36,7 @@ public class DefDatabasePatches : ClassWithFishPrepatches DefDatabase.defsList.Count} defs of this type loaded."); } - public class GetByShortHashPatch : FishPrepatch + public sealed class GetByShortHashPatch : FishPrepatch { public override MethodBase TargetMethodBase { get; } = methodof(DefDatabase.GetByShortHash); diff --git a/Source/PerformanceFish/Defs/DefThingPair.cs b/Source/PerformanceFish/Defs/DefThingPair.cs new file mode 100644 index 0000000..5f80970 --- /dev/null +++ b/Source/PerformanceFish/Defs/DefThingPair.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace PerformanceFish.Defs; + +public record struct DefThingPair(int DefShortHash, int ThingIDNumber) +{ + public int + DefShortHash = DefShortHash, + ThingIDNumber = ThingIDNumber; + + public bool Equals(DefThingPair other) + => Unsafe.As(ref this) == Unsafe.As(ref other); + + public override int GetHashCode() => HashCode.Combine(DefShortHash, ThingIDNumber); +} \ No newline at end of file diff --git a/Source/PerformanceFish/FishDefOf.cs b/Source/PerformanceFish/Defs/FishDefOf.cs similarity index 96% rename from Source/PerformanceFish/FishDefOf.cs rename to Source/PerformanceFish/Defs/FishDefOf.cs index 86de5e5..0b74ea9 100644 --- a/Source/PerformanceFish/FishDefOf.cs +++ b/Source/PerformanceFish/Defs/FishDefOf.cs @@ -5,7 +5,8 @@ /*using System.Linq; -namespace PerformanceFish; +namespace PerformanceFish.Defs; + [StaticConstructorOnStartup] public static class FishDefOfOrNull { diff --git a/Source/PerformanceFish/RecipeDefCaching.cs b/Source/PerformanceFish/Defs/RecipeDefCaching.cs similarity index 90% rename from Source/PerformanceFish/RecipeDefCaching.cs rename to Source/PerformanceFish/Defs/RecipeDefCaching.cs index fd0e184..0692266 100644 --- a/Source/PerformanceFish/RecipeDefCaching.cs +++ b/Source/PerformanceFish/Defs/RecipeDefCaching.cs @@ -5,19 +5,19 @@ using RecipeUsersCache = PerformanceFish.Cache.ByInt; + PerformanceFish.Defs.RecipeDefCaching.AllRecipeUsers_Patch.RecipeUsersCacheValue>; -namespace PerformanceFish; +namespace PerformanceFish.Defs; -public class RecipeDefCaching : ClassWithFishPatches +public sealed class RecipeDefCaching : ClassWithFishPatches { public override bool RequiresLoadedGameForPatching => true; - public class AllRecipeUsers_Patch : FirstPriorityFishPatch + public sealed class AllRecipeUsers_Patch : FirstPriorityFishPatch { public override string Description { get; } = "Caches results of the AllRecipeUsers method. Becomes impactful with lots of unfinished things around, " - + "as those check everything for potentially accepting them"; + + "as those check everything for potentially accepting them"; public override MethodBase TargetMethodInfo { get; } = AccessTools.PropertyGetter(typeof(RecipeDef), nameof(RecipeDef.AllRecipeUsers)); @@ -68,7 +68,7 @@ public class RecipeDefCaching : ClassWithFishPatches } else { - RecipeUsers = new(result); + RecipeUsers = [..result]; } _def = instance; @@ -86,7 +86,7 @@ public class RecipeDefCaching : ClassWithFishPatches } } - /*public class PotentiallyMissingIngredients_Patch : FirstPriorityFishPatch + /*public sealed class PotentiallyMissingIngredients_Patch : FirstPriorityFishPatch { public override Expression TargetMethod { get; } = () => new RecipeDef().PotentiallyMissingIngredients(null, null); diff --git a/Source/PerformanceFish/Defs/ThingDefPatches.cs b/Source/PerformanceFish/Defs/ThingDefPatches.cs new file mode 100644 index 0000000..2ff4c6d --- /dev/null +++ b/Source/PerformanceFish/Defs/ThingDefPatches.cs @@ -0,0 +1,112 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; +using PerformanceFish.Prepatching; +using FieldAttributes = Mono.Cecil.FieldAttributes; +using MethodImplAttributes = Mono.Cecil.MethodImplAttributes; + +namespace PerformanceFish.Defs; + +public sealed class ThingDefPatches : ClassWithFishPrepatches +{ + public const string FISH_HASH_FIELD_NAME = "FishHash"; + + public sealed class GetHashCodePatch : FishClassPrepatch + { + public override string? Description { get; } + = "Def.GetHashCode is overridden to return defName.GetHashCode, which is rather slow as that's a string. " + + "This patch caches the hashcode in a field added through prepatcher and simply returns that instead."; + + public override Type Type { get; } = typeof(BuildableDef); + + public override void FreePatch(TypeDefinition typeDefinition) + { + var module = typeDefinition.Module; + + var fishHashField = new FieldDefinition(FISH_HASH_FIELD_NAME, FieldAttributes.Private, + module.TypeSystem.Int32); + + typeDefinition.Fields.Insert(0, fishHashField); + + var constructorBody = typeDefinition.GetConstructors().First().Body; + + constructorBody.GetILProcessor().InsertRange(constructorBody.Instructions.Count - 2, + OpCodes.Ldarg_0, + (OpCodes.Ldc_I4, -42), + (OpCodes.Stfld, fishHashField)); + + constructorBody.OptimizeMacros(); + + var getHashCodeBody = typeDefinition.GetMethod(nameof(BuildableDef.GetHashCode))!.Body; + + var variables = getHashCodeBody.Variables; + variables.Clear(); + + var hashVariable = new VariableDefinition(module.TypeSystem.Int32); + variables.Add(hashVariable); + + var instructions = getHashCodeBody.Instructions; + instructions.Clear(); + + var ilProcessor = getHashCodeBody.GetILProcessor(); + ilProcessor.InsertRange(0, + OpCodes.Ldarg_0, + (OpCodes.Ldfld, fishHashField), + (OpCodes.Stloc, hashVariable), + (OpCodes.Ldloc, hashVariable), + (OpCodes.Ldc_I4, -42), + // 5 bne_un_s ^2 + OpCodes.Ldarg_0, + (OpCodes.Call, methodof(CreateLongHash)), + OpCodes.Ret, + (OpCodes.Ldloc, hashVariable), + OpCodes.Ret); + + ilProcessor.InsertAt(5, OpCodes.Bne_Un_S, instructions[^2]); + + getHashCodeBody.MaxStackSize = 2; + getHashCodeBody.Method.ImplAttributes |= MethodImplAttributes.AggressiveInlining; + getHashCodeBody.OptimizeMacros(); + } + + // [MethodImpl(MethodImplOptions.AggressiveInlining)] + // public static int ReplacementBody(BuildableDef instance) + // { + // var hash = instance.FishHash; + // return hash != -42 ? hash : CreateLongHash(instance); + // } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CreateLongHash(BuildableDef instance) + { + var defName = instance.defName; + if (defName is null) + return 0; + + var defHash = defName.GetHashCode(); + if (defName is "" or Def.DefaultDefName) + return defHash; + + ref var cacheHash = ref Lazy.FishHashField(instance); + cacheHash = defHash != -42 ? defHash : defHash + 1; + return cacheHash; + } + + private static class Lazy + { + internal static readonly AccessTools.FieldRef FishHashField + = AccessTools.FieldRefAccess(FISH_HASH_FIELD_NAME); + + static Lazy() + { + // prevent beforefieldinit + } + } + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/DesignationManagerCaching.cs b/Source/PerformanceFish/DesignationManagerCaching.cs index f90de42..05f730a 100644 --- a/Source/PerformanceFish/DesignationManagerCaching.cs +++ b/Source/PerformanceFish/DesignationManagerCaching.cs @@ -16,9 +16,9 @@ using DesignationsOfDefCache = namespace PerformanceFish; -public class DesignationManagerCaching : ClassWithFishPatches +public sealed class DesignationManagerCaching : ClassWithFishPatches { - public class SpawnedDesignationsOfDef_Patch : FishPatch + public sealed class SpawnedDesignationsOfDef_Patch : FishPatch { public override string Description => "Designation caching"; @@ -62,7 +62,7 @@ public class DesignationManagerCaching : ClassWithFishPatches dict[new(instance, key)] = cache; } - public class AnySpawnedDesignationOfDef_Patch : FishPatch + public sealed class AnySpawnedDesignationOfDef_Patch : FishPatch { public override string Description => "Designation caching"; @@ -76,7 +76,7 @@ public class DesignationManagerCaching : ClassWithFishPatches => Reflection.GetCodeInstructions(AnySpawnedDesignationOfDef); } - public class AllDesignationsOn_Patch : FishPatch + public sealed class AllDesignationsOn_Patch : FishPatch { public override string Description => "Designation caching"; public override Expression TargetMethod => () => default(DesignationManager)!.AllDesignationsOn(null); @@ -99,7 +99,7 @@ public class DesignationManagerCaching : ClassWithFishPatches } } - public class DesignationOn_Patch : FishPatch + public sealed class DesignationOn_Patch : FishPatch { public override string Description => "Designation caching"; public override Expression TargetMethod => () => default(DesignationManager)!.DesignationOn(null); @@ -111,7 +111,7 @@ public class DesignationManagerCaching : ClassWithFishPatches => Reflection.GetCodeInstructions(DesignationOn); } - public class DesignationOn_ByDef_Patch : FishPatch + public sealed class DesignationOn_ByDef_Patch : FishPatch { public override string Description => "Designation caching"; public override Expression TargetMethod => () => default(DesignationManager)!.DesignationOn(null, null); @@ -149,7 +149,7 @@ public class DesignationManagerCaching : ClassWithFishPatches => Reflection.GetCodeInstructions(DesignationOn); } - public class HasMapDesignationOn_Patch : FishPatch + public sealed class HasMapDesignationOn_Patch : FishPatch { public override string Description => "Designation caching"; public override Expression TargetMethod => () => default(DesignationManager)!.HasMapDesignationOn(null); diff --git a/Source/PerformanceFish/Events/MapEvents.cs b/Source/PerformanceFish/Events/MapEvents.cs new file mode 100644 index 0000000..faf6990 --- /dev/null +++ b/Source/PerformanceFish/Events/MapEvents.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using PerformanceFish.Prepatching; + +namespace PerformanceFish.Events; + +public sealed class MapEvents : ClassWithFishPrepatches +{ + public sealed class Instanced + { + public event Action? + ComponentsConstructed, + ComponentsLoaded; + + internal void OnComponentsConstructed(Map map) => ComponentsConstructed?.Invoke(map); + + internal void OnComponentsLoaded(Map map) => ComponentsLoaded?.Invoke(map); + } + + public sealed class MapConstructComponentsPatch : FishPrepatch + { + public override string? Description { get; } + = "Hook to raise an event when map components finish constructing. Does nothing by itself, but certain " + + "functions require this."; + + public override bool Enabled => true; + + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(Map), nameof(Map.ConstructComponents)); + + public static void Postfix(Map __instance) => __instance.Events().OnComponentsConstructed(__instance); + } + + public sealed class MapExposeComponentsPatch : FishPrepatch + { + public override string? Description { get; } + = "Maps apparently construct all components twice when loading a save. Once in ConstructComponents and " + + "afterwards another time here. This is the companion patch to MapConstructCompanions, raising the " + + "ComponentsExposed event."; + + public override bool Enabled => true; + + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(Map), nameof(Map.ExposeComponents)); + + public static void Postfix(Map __instance) + { + if (Scribe.mode == LoadSaveMode.LoadingVars) + __instance.Events().OnComponentsLoaded(__instance); + } + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Events/StaticEvents.cs b/Source/PerformanceFish/Events/StaticEvents.cs new file mode 100644 index 0000000..3ea8dc1 --- /dev/null +++ b/Source/PerformanceFish/Events/StaticEvents.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace PerformanceFish.Events; + +public static class StaticEvents +{ + public static event Action CacheCleared + { + add => Cache.Utility.Cleared += value; + remove => Cache.Utility.Cleared -= value; + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Events/ThingEvents.cs b/Source/PerformanceFish/Events/ThingEvents.cs new file mode 100644 index 0000000..3f9c301 --- /dev/null +++ b/Source/PerformanceFish/Events/ThingEvents.cs @@ -0,0 +1,167 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using PerformanceFish.Prepatching; + +namespace PerformanceFish.Events; + +public sealed class ThingEvents : ClassWithFishPrepatches +{ + public static event Action? + Initialized, + Destroyed; + + public sealed class Instanced + { + public event ThingGridEvents.EventHandler? + RegisteredAtThingGrid, + DeregisteredAtThingGrid; + + public event Action? Spawned; + + public event Action? + DeSpawning, + DeSpawned, + Destroying, + Destroyed; + + internal void OnSpawned(Thing thing, Map map) => Spawned?.Invoke(thing, map); + + internal void OnDeSpawning(Thing thing) => DeSpawning?.Invoke(thing); + + internal void OnDeSpawned(Thing thing) => DeSpawned?.Invoke(thing); + + internal void OnDestroying(Thing thing) => Destroying?.Invoke(thing); + + internal void OnDestroyed(Thing thing) => Destroyed?.Invoke(thing); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void OnRegisteredAtThingGrid(Thing thing, Map map, in IntVec3 cell) + => RegisteredAtThingGrid?.Invoke(thing, map, cell); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void OnDeregisteredAtThingGrid(Thing thing, Map map, in IntVec3 cell) + => DeregisteredAtThingGrid?.Invoke(thing, map, cell); + } + + public sealed class SpawnSetupPatch : FishPrepatch + { + public override string? Description { get; } + = "Hook to invoke an event whenever things spawn. Does nothing by itself, but certain functions require " + + "this."; + + public override bool Enabled => true; + + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(Thing), nameof(Thing.SpawnSetup)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Postfix(Thing __instance, Map map) => __instance.Events().OnSpawned(__instance, map); + } + + public sealed class DeSpawnPatch : FishPrepatch + { + public override string? Description { get; } + = "Hook to invoke an event whenever things despawn. Does nothing by itself, but certain functions require " + + "this."; + + public override bool Enabled => true; + + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(Thing), nameof(Thing.DeSpawn)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Prefix(Thing __instance) => __instance.Events().OnDeSpawning(__instance); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Postfix(Thing __instance) => __instance.Events().OnDeSpawned(__instance); + } + + public sealed class DestroyPatch : FishPrepatch + { + public override string? Description { get; } + = "Hook to invoke an event whenever things get destroyed. Does nothing by itself, but certain functions " + + "require this."; + + public override bool Enabled => true; + + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(Thing), nameof(Thing.Destroy)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Prefix(Thing __instance) => __instance.Events().OnDestroying(__instance); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Postfix(Thing __instance) + { + __instance.Events().OnDestroyed(__instance); + Destroyed?.Invoke(__instance); + } + } + + public sealed class ExposeDataInitializePatch : FishPrepatch + { + public override string? Description { get; } + = "Hook to invoke an event when things finish loading from a save file. Does nothing by itself, but " + + "certain functions require this."; + + public override bool Enabled => true; + + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(Thing), nameof(Thing.ExposeData)); + + public static void Postfix(Thing __instance) + { + if (Scribe.mode == LoadSaveMode.LoadingVars && __instance is not ThingWithComps) + OnThingInitialized(__instance); + } + } + + public sealed class PostMakePatch : FishPrepatch + { + public override string? Description { get; } + = "Hook to invoke an event when things get created. Does nothing by itself, but certain functions require " + + "this."; + + public override bool Enabled => true; + + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(Thing), nameof(Thing.PostMake)); + + public static void Postfix(Thing __instance) + { + if (__instance is not ThingWithComps) + OnThingInitialized(__instance); + } + } + + public sealed class InitializeCompsPatch : FishPrepatch + { + public override string? Description { get; } + = "Hook to invoke an event when things with comps get initialized. Does nothing by itself, but certain " + + "functions require this."; + + public override bool Enabled => true; + + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ThingWithComps), nameof(ThingWithComps.InitializeComps)); + + public static void Postfix(ThingWithComps __instance) => OnThingInitialized(__instance); + } + + private static void OnThingInitialized(Thing thing) => Initialized?.Invoke(thing); +} \ No newline at end of file diff --git a/Source/PerformanceFish/Events/ThingGridEvents.cs b/Source/PerformanceFish/Events/ThingGridEvents.cs new file mode 100644 index 0000000..1427387 --- /dev/null +++ b/Source/PerformanceFish/Events/ThingGridEvents.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; + +namespace PerformanceFish.Events; + +public sealed class ThingGridEvents : ClassWithFishPrepatches +{ + /// + /// Invoked for every thing.Position change, including for motes and pawns + /// + public sealed class RegisterInCellPatch : FishPrepatch + { + public override string? Description { get; } + = "Hook to invoke an event whenever things register in a cell. Does nothing by itself, but certain " + + "functions require this."; + + public override bool Enabled => true; + + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ThingGrid), nameof(ThingGrid.RegisterInCell)); + + public static void OnRegistered(ThingGrid __instance, Thing t, in IntVec3 c) + => t.Events().OnRegisteredAtThingGrid(t, __instance.map, c); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.InsertRange(ilProcessor.instructions.FirstIndexOf(static code + => code.OpCode == OpCodes.Callvirt + && code.Operand is MethodReference { Name: "Add" } method + && method.DeclaringType.Name.Contains("List")) + + 1, + OpCodes.Ldarg_0, + OpCodes.Ldarg_1, + (OpCodes.Ldarga_S, ilProcessor.Body.GetParameter(2)), + (OpCodes.Call, methodof(OnRegistered))); + } + + public sealed class DeregisterInCellPatch : FishPrepatch + { + public override string? Description { get; } + = "Hook to invoke an event whenever things deregister from a cell. Does nothing by itself, but certain " + + "functions require this."; + + public override bool Enabled => true; + + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ThingGrid), nameof(ThingGrid.DeregisterInCell)); + + public static void OnDeregistered(ThingGrid __instance, Thing t, in IntVec3 c) + => t.Events().OnDeregisteredAtThingGrid(t, __instance.map, c); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.InsertRange(ilProcessor.instructions.FirstIndexOf(static code + => code.OpCode == OpCodes.Callvirt + && code.Operand is MethodReference { Name: "Remove" } method + && method.DeclaringType.Name.Contains("List")) + + 1, + OpCodes.Ldarg_0, + OpCodes.Ldarg_1, + (OpCodes.Ldarga, ilProcessor.Body.GetParameter(2)), + (OpCodes.Call, methodof(OnDeregistered))); + } + + public delegate void EventHandler(Thing thing, Map map, in IntVec3 cell); +} \ No newline at end of file diff --git a/Source/PerformanceFish/FishSettings.cs b/Source/PerformanceFish/FishSettings.cs index 7547b74..1d07f23 100644 --- a/Source/PerformanceFish/FishSettings.cs +++ b/Source/PerformanceFish/FishSettings.cs @@ -13,16 +13,22 @@ using PerformanceFish.Prepatching; namespace PerformanceFish; [PublicAPI] -public class FishSettings : ModSettings +public sealed class FishSettings : ModSettings { public static bool SettingsLoaded { get; private set; } - + + private const string VERSION_SCRIBE_LABEL = "Version"; + public override void ExposeData() { base.ExposeData(); + if (!TryScribeVersion()) + goto DoneLoading; + Scribe_Values.Look(ref _threadingEnabled, "ThreadingEnabled"); Scribe_Values.Look(ref MothballEverything, "MothballEverything"); + Scribe_Values.Look(ref ImproveHaulingAccuracy, "ImproveHaulingAccuracy", true); if (PerformanceFishMod.AllPatchClasses is { } allPatchClasses) { @@ -36,9 +42,33 @@ public class FishSettings : ModSettings patchClass.Patches.Scribe(); } + DoneLoading: SettingsLoaded = true; } + private static bool TryScribeVersion() + { + var currentVersion = PerformanceFishMod.LoadedVersion; + var lastSavedVersion = new Version().ToString(); + + var scribeMode = Scribe.mode; + if (scribeMode == LoadSaveMode.Saving) + { + lastSavedVersion = currentVersion.ToString(); + Scribe_Values.Look(ref lastSavedVersion, VERSION_SCRIBE_LABEL); + } + else if (scribeMode == LoadSaveMode.LoadingVars) + { + var defaultVersion = lastSavedVersion; + Scribe_Values.Look(ref lastSavedVersion, VERSION_SCRIBE_LABEL); + + if (new Version(lastSavedVersion ?? defaultVersion) < currentVersion) + return false; + } + + return true; + } + public static void DoSettingsWindowContents(Rect inRect) { var windowRect = Find.WindowStack.currentlyDrawnWindow.windowRect; @@ -49,7 +79,7 @@ public class FishSettings : ModSettings { ResetAll(); } - + Widgets.BeginScrollView(inRect, ref _scrollPosition, _scrollRect); var ls = new Listing_Standard(); ls.Begin(_scrollRect); @@ -62,7 +92,7 @@ public class FishSettings : ModSettings ModEnabled = checkModEnabled; ToggleAllPatches(checkModEnabled); } - + ls.Gap(); var curFontStyle = Text.CurFontStyle; var curFontSize = curFontStyle.fontSize; @@ -76,6 +106,9 @@ public class FishSettings : ModSettings { try { + if (!patchClass.ShowSettings) + continue; + ls.Label(patchClass.GetType().Name, tooltip: patchClass is IHasDescription classWithDescription ? classWithDescription.Description @@ -84,6 +117,9 @@ public class FishSettings : ModSettings ls.GapLine(2f); foreach (var patch in patchClass.Patches) { + if (!patch.ShowSettings) + continue; + if (ShouldSkipForScrollView(inRect.height, Text.LineHeight, ls.curY, _scrollPosition.y)) { ls.curY += Text.LineHeight; @@ -91,14 +127,14 @@ public class FishSettings : ModSettings } var check = patch.Enabled; - if (patch.Description == null) + if (patch.DescriptionWithNotes == null) Widgets.DrawHighlightIfMouseover(_scrollRect with { y = ls.curY, height = Text.LineHeight }); var label = patch.Name ?? patch.GetType().Name; if (label.EndsWith("_Patch")) label = label.Remove(label.Length - 6); else if (label.EndsWith("Patch")) label = label.Remove(label.Length - 5); - ls.CheckboxLabeled(label, ref check, patch.Description); + ls.CheckboxLabeled(label, ref check, patch.DescriptionWithNotes); if (check != patch.Enabled) patch.Enabled = check; } @@ -110,18 +146,21 @@ public class FishSettings : ModSettings Log.Error($"{ex}"); } } - + ls.Gap(); curFontStyle.fontSize = 20; ls.Label("Prepatches"); curFontStyle.fontSize = curFontSize; ls.GapLine(); ls.Gap(); - + foreach (var patchClass in PerformanceFishMod.AllPrepatchClasses!) { try { + if (!patchClass.ShowSettings) + continue; + ls.Label(patchClass.GetType().Name, // ReSharper disable once SuspiciousTypeConversion.Global tooltip: patchClass is IHasDescription classWithDescription @@ -131,6 +170,9 @@ public class FishSettings : ModSettings ls.GapLine(2f); foreach (var patch in patchClass.Patches) { + if (!patch.ShowSettings) + continue; + if (ShouldSkipForScrollView(inRect.height, Text.LineHeight, ls.curY, _scrollPosition.y)) { ls.curY += Text.LineHeight; @@ -138,14 +180,14 @@ public class FishSettings : ModSettings } var check = patch.Enabled; - if (patch.Description == null) + if (patch.DescriptionWithNotes == null) Widgets.DrawHighlightIfMouseover(_scrollRect with { y = ls.curY, height = Text.LineHeight }); var label = patch.Name ?? patch.GetType().Name; if (label.EndsWith("_Patch")) label = label.Remove(label.Length - 6); else if (label.EndsWith("Patch")) label = label.Remove(label.Length - 5); - ls.CheckboxLabeled(label, ref check, patch.Description); + ls.CheckboxLabeled(label, ref check, patch.DescriptionWithNotes); if (check != patch.Enabled) patch.Enabled = check; } @@ -157,10 +199,12 @@ public class FishSettings : ModSettings Log.Error($"{ex}"); } } - + + ls.Gap(); + ls.CheckboxLabeled("Improve hauling accuracy", ref ImproveHaulingAccuracy, ImproveHaulingAccuracyDescription); ls.Gap(); ls.CheckboxLabeled("Mothball everything", ref MothballEverything, MothballEverythingDescription); - + ls.Gap(); if (ls.ButtonText("Log hediffs affected by mothball optimization")) @@ -168,12 +212,12 @@ public class FishSettings : ModSettings Log.Message($"Allowed defs for mothballing: {MothballOptimization.AllowedDefs.ToStringSafeEnumerable()}"); Log.Message($"Blocking defs for mothballing: {DefDatabase.AllDefsListForReading.Where(static def => !MothballOptimization.AllowedDefs.Contains(def)).ToStringSafeEnumerable()}"); - + Verse.Log.TryOpenLogWindow(); } ls.Gap(); - + if (ls.ButtonText("Log patch count")) { PerformanceFishMod.LogPatchCount(); @@ -181,7 +225,7 @@ public class FishSettings : ModSettings } ls.Gap(); - + if (ls.ButtonText("Log cache utilization")) { Cache.Utility.LogCurrentCacheUtilization(); @@ -201,10 +245,16 @@ public class FishSettings : ModSettings }; } - public static string MothballEverythingDescription - = MothballOptimization.BASIC_DESCRIPTION + " This setting removes all conditions, enabling every world " - + "pawn for mothballing. Note, health conditions do not progress on mothballed pawns. Requires the " - + "WorldPawnsDefPreventingMothball patch to apply"; + public static string + MothballEverythingDescription + = MothballOptimization.BASIC_DESCRIPTION + + " This setting removes all conditions, enabling every world " + + "pawn for mothballing. Note, health conditions do not progress on mothballed pawns. Requires the " + + "WorldPawnsDefPreventingMothball patch to apply", + ImproveHaulingAccuracyDescription + = "Requires the StoreUtilityPrepatches:TryFindBestBetterStoreCellForWorker patch to function. Having this " + + "enabled makes hauling lookups more thorough, raising the odds of getting closer haul destinations at " + + "the cost of a usually minor performance hit"; private static void ToggleAllPatches(bool newState) => ToggleAllPatches(_ => newState, _ => newState); @@ -214,15 +264,21 @@ public class FishSettings : ModSettings foreach (var patchClass in PerformanceFishMod.AllPatchClasses!) { foreach (var patch in patchClass.Patches) - patch.Enabled = fishPatchFunc(patch); + { + if (patch.ShowSettings) + patch.Enabled = fishPatchFunc(patch); + } } foreach (var patchClass in PerformanceFishMod.AllPrepatchClasses!) { foreach (var patch in patchClass.Patches) - patch.Enabled = fishPrepatchFunc(patch); + { + if (patch.ShowSettings) + patch.Enabled = fishPrepatchFunc(patch); + } } - + Cache.Utility.Clear(); } @@ -259,7 +315,9 @@ public class FishSettings : ModSettings private static bool _modEnabled = true; - public static bool MothballEverything; + public static bool + MothballEverything, + ImproveHaulingAccuracy = true; public static bool ThreadingEnabled { @@ -275,7 +333,7 @@ public class FishSettings : ModSettings } private static bool _threadingEnabled; - public static List ThreadingPatches { get; } = new(); + public static List ThreadingPatches { get; } = []; private static Rect _scrollRect = new(0f, 0f, 500f, 9001f); private static Vector2 _scrollPosition; diff --git a/Source/PerformanceFish/FunctionPointers/IndexGetter.cs b/Source/PerformanceFish/FunctionPointers/IndexGetter.cs index 8873ccd..499daff 100644 --- a/Source/PerformanceFish/FunctionPointers/IndexGetter.cs +++ b/Source/PerformanceFish/FunctionPointers/IndexGetter.cs @@ -68,14 +68,15 @@ public static class IndexGetter Delegate func; while (!_indexFuncs.TryGetValue(type, out func!)) { - type = type.BaseType - ?? throw new($"Performance Fish failed to find a fallback method to handle type { - typeof(T).FullName} from mod {typeof(T).Assembly.GetName().Name}"); + type = type?.BaseType + ?? ThrowHelper.ThrowInvalidOperationException( + $"Performance Fish failed to find a fallback method to handle type { + typeof(T).FullName} from mod {typeof(T).Assembly.GetName().Name}"); } if (!isThing) { - Log.Message($"Found fallback of type {type.FullName}.{ + Log.Message($"Found fallback of type {type?.FullName ?? "NULL"}.{ (!isDef ? " This likely means we can safely continue from here." : "")}"); } diff --git a/Source/PerformanceFish/GasDef.cs b/Source/PerformanceFish/GasDef.cs new file mode 100644 index 0000000..f5422eb --- /dev/null +++ b/Source/PerformanceFish/GasDef.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using JetBrains.Annotations; + +namespace PerformanceFish; + +[PublicAPI] +public class GasDef : Def +{ + public int dissipationRate; + public bool diffuses; + public Color color; + + public static GasDef? OfGasType(GasType gasType) + => gasType switch + { + GasType.BlindSmoke => GasDefOf.BlindSmoke, + GasType.RotStink => GasDefOf.RotStink, + GasType.ToxGas => GasDefOf.ToxGas, + _ => null + }; +} \ No newline at end of file diff --git a/Source/PerformanceFish/GasDefOf.cs b/Source/PerformanceFish/GasDefOf.cs new file mode 100644 index 0000000..5cddf50 --- /dev/null +++ b/Source/PerformanceFish/GasDefOf.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using JetBrains.Annotations; + +namespace PerformanceFish; + +[DefOf] +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +#pragma warning disable CS8618 +public static class GasDefOf +{ + public static GasDef + BlindSmoke, + ToxGas, + RotStink; + + static GasDefOf() => DefOfHelper.EnsureInitializedInCtor(typeof(GasDefOf)); +} +#pragma warning restore CS8618 \ No newline at end of file diff --git a/Source/PerformanceFish/GasGridExtensions.cs b/Source/PerformanceFish/GasGridExtensions.cs new file mode 100644 index 0000000..a96060f --- /dev/null +++ b/Source/PerformanceFish/GasGridExtensions.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using JetBrains.Annotations; + +namespace PerformanceFish; + +[PublicAPI] +public static class GasGridExtensions +{ + public static GasGridOptimization.ParallelGasGrid ForDef(this GasGridOptimization.ParallelGasGrid[] grids, + GasDef def) + => grids[def.index]; + + public static GasGridOptimization.ParallelGasGrid ForDef(this GasGrid grid, GasDef def) + => grid.ParallelGasGrids().ForDef(def); + + public static void AddGas(this GasGrid grid, in IntVec3 cell, GasDef gas, int amount, bool canOverflow = true) + => grid.ForDef(gas).AddGas(cell, amount, canOverflow); + + public static void AddGas(this GasGrid grid, in IntVec3 cell, GasDef gas, float radius) + => grid.AddGas(cell, gas, 255 * GenRadial.NumCellsInRadius(radius)); + + public static void SetDirect(this GasGrid grid, in IntVec3 cell, GasDef gas, byte density) + => grid.ForDef(gas).SetDirect(cell, density); + + public static byte DensityAt(this GasGrid grid, in IntVec3 cell, GasDef gas) => grid.ForDef(gas).DensityAt(cell); + + public static float DensityPercentAt(this GasGrid grid, in IntVec3 cell, GasDef gas) + => grid.DensityAt(cell, gas) / 255f; + + /// + /// For modders. Performance Fish doesn't automatically scribe added custom defs. + /// + public static void ScribeForDef(this GasGrid grid, GasDef gas, string label) + { + if (gas == GasDefOf.BlindSmoke || gas == GasDefOf.ToxGas || gas == GasDefOf.RotStink) + { + Log.Error($"Tried scribing vanilla gas '{gas}'. This should never be manually scribed by a mod."); + return; + } + + grid.ForDef(gas).Scribe(label); + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/GasGridOptimization.cs b/Source/PerformanceFish/GasGridOptimization.cs new file mode 100644 index 0000000..4306a13 --- /dev/null +++ b/Source/PerformanceFish/GasGridOptimization.cs @@ -0,0 +1,995 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// #define GAS_DEBUG_L1 // erroneous gas changes +// #define GAS_DEBUG_L2 // total gas grid changes +// #define GAS_DEBUG_L3 // individual cell changes + +using System.Diagnostics; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using MonoMod.Utils; +using PerformanceFish.Prepatching; + +namespace PerformanceFish; + +public sealed class GasGridOptimization : ClassWithFishPrepatches +{ + // public static readonly List ParallelGasGrids = new(); + // public static GasGrid[] RegisteredGasGrids = Array.Empty(); + + public sealed class SetDirectPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override string? Description { get; } + = "Required by the gas grid optimization"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.SetDirect)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(GasGrid __instance, int index, byte smoke, byte toxic, byte rotStink) + { + if (!ModsConfig.BiotechActive) + toxic = 0; + + var gasGrids = __instance.ParallelGasGrids(); + + gasGrids[0].SetDirect(index, smoke); + gasGrids[1].SetDirect(index, toxic); + gasGrids[2].SetDirect(index, rotStink); + } + } + + public sealed class TickPatch : FishPrepatch + { + public override List LinkedPatches { get; } = + [ + typeof(SetDirectPatch), typeof(ColorAtPatch), typeof(AddGasPatch), typeof(AnyGasAtPatch), + typeof(DensityAtPatch), typeof(Debug_ClearAllPatch), typeof(Debug_FillAllPatch), + typeof(Notify_ThingSpawnedPatch), typeof(ExposeDataPatch), typeof(MouseOverReadOutOnGUI), + typeof(DebugToolsGeneralPushGas) + ]; + + // public static EventWaitHandle? WaitHandle { get; private set; } + + public static object? MonitorObject { get; private set; } + + public override string? Description { get; } + = "Optimizes the gas grid to utilize highly performant bit manipulation for much faster processing of " + + "cells, usually in buckets of 64 simultaneously checked cells, and threading to process the different " + + "gas types in parallel. Additionally exposes gas to xml through the PerformanceFish.GasDef type. " + + "Performance impact scales with map size, but is generally large. Currently only utilizing single core " + + "parallelism, threading is planned"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.Tick)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(GasGrid __instance) + { + // Parallel.For(0, ParallelGasGrids.Length, static i => ParallelGasGrids[i].Tick()); + // (WaitHandle ?? Setup()).Set(); + + // if (!RegisteredGasGrids.Contains(__instance)) + // Setup(__instance); + + // ParallelNoAlloc.PulseRegisteredBackgroundWorkers(__instance.MonitorObject()); + + var gasGrids = __instance.ParallelGasGrids(); + + for (var i = 1; i < gasGrids.Length; i++) + gasGrids[i].Tick(); + + if (gasGrids.Length < 1) + return; + + var firstGasGrid = gasGrids[0]; + firstGasGrid.Tick(); + + __instance.cycleIndexDiffusion = firstGasGrid.CycleIndexDiffusion; + __instance.cycleIndexDissipation = firstGasGrid.CycleIndexDissipation; + } + + // [MemberNotNull(nameof(WaitHandle))] + // public static EventWaitHandle Setup() + // { + // WaitHandle = new ManualResetEvent(false); + // for (var i = ParallelGasGrids.Length; i-- > 1;) + // { + // var gasGrid = ParallelGasGrids[i]; + // ThreadPool.RegisterWaitForSingleObject(WaitHandle, (_, _) => gasGrid.Tick(), null, -1, false); + // } + // + // return WaitHandle; + // } + +// public static ParallelGasGrid[] Setup(GasGrid instance) +// { +// // lock (ParallelGasGrids) +// // { +// // if (RegisteredGasGrids.Contains(instance)) +// // { +// // // only possible if 2 threads try registering the same gas grid instance. Rimthreaded? Maybe throw? +// // SpinWait.SpinUntil(static () => MonitorObject != null); +// // #pragma warning disable CS8774 +// // return; +// // #pragma warning restore CS8774 +// // } +// // +// // RegisteredGasGrids = RegisteredGasGrids.Add(instance); +// // } +// +// return new ParallelGasGrid[DefDatabase.DefCount]; +// +// var gasDefsList = DefDatabase.AllDefsListForReading; +// +// var newGasGrids = new ParallelGasGrid[gasDefsList.Count]; +// try +// { +// for (var i = 0; i < newGasGrids.Length; i++) +// newGasGrids[i] = new(instance.map, gasDefsList[i]); +// +// // ParallelGasGrids.AddRangeFast(newGasGrids); +// +// instance.MonitorObject() = ParallelNoAlloc.RegisterBackgroundWaitingWorkers(MakeTickActions(newGasGrids, +// Math.Max(newGasGrids.Length - 1, 0), 1)); // 1st grid ticked on main thread +// } +// catch (Exception e) +// { +// Log.Error($"Caught exception in map with ID {instance.map.uniqueID} and hashCode {instance.map.GetHashCode()}: {e}"); +// return Array.Empty(); +// } +// +// return newGasGrids; +// } + + private static Action[] MakeTickActions(T gasGrids, int count, int startIndex) + where T : IList + { + var actions = new Action[count]; + count += startIndex; + for (var i = startIndex; i < count; i++) + { + var gasGrid = gasGrids[i]; + actions[i - startIndex] = () => gasGrid.Tick(); + } + + return actions; + } + + // for save-game compatibility. The two cycle indices get scribed with ExposeData + public static void AdvanceCycleIndex(ref int cycleIndex, int area, int increment) + { + cycleIndex += increment; + if (cycleIndex >= area) + cycleIndex = 0; + } + } + + public sealed class ColorAtPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.ColorAt)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static Color ReplacementBody(GasGrid __instance, IntVec3 cell) + { + var cellIndex = cell.CellToIndex(__instance.map); + + var gasGrids = __instance.ParallelGasGrids(); + var gasGridCount = gasGrids.Length; + + Span densities = stackalloc float[gasGridCount]; + var sum = 0f; + + for (var i = 0; i < gasGridCount; i++) + sum += densities[i] = gasGrids[i].DensityAt(cellIndex); + + var resultColor = default(Color); + + for (var i = 0; i < gasGridCount; i++) + resultColor += gasGrids[i].Color * (densities[i] / sum); + + resultColor.a = Mathf.Lerp(GasGrid.AlphaRange.min, GasGrid.AlphaRange.max, + sum / (/*gasGridCount*/ 3 * byte.MaxValue)); // maintain vanilla transparency + + return resultColor; + } + } + + public sealed class AddGasPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.AddGas)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(GasGrid __instance, IntVec3 cell, GasType gasType, int amount, + bool canOverflow = true) + => __instance.ParallelGasGrids()[(int)gasType >> 3].AddGas(cell, amount, canOverflow); + } + + public sealed class AnyGasAtPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.AnyGasAt), [typeof(int)]); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static bool ReplacementBody(GasGrid __instance, int idx) + { + var gasGrids = __instance.ParallelGasGrids(); + for (var i = 0; i < gasGrids.Length; i++) + { + if (gasGrids[i].AnyGasAt(idx)) + return true; + } + + return false; + } + } + + public sealed class DensityAtPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.DensityAt), + [typeof(int), typeof(GasType)]); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static byte ReplacementBody(GasGrid __instance, int index, GasType gasType) + => __instance.ParallelGasGrids()[(int)gasType >> 3].DensityAt(index); + } + + public sealed class Debug_ClearAllPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.Debug_ClearAll)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(GasGrid __instance) + { + var gasGrids = __instance.ParallelGasGrids(); + + for (var i = 0; i < gasGrids.Length; i++) + gasGrids[i].Clear(); + + __instance.map.mapDrawer.WholeMapChanged(MapMeshFlag.Gas); + } + } + + public sealed class Debug_FillAllPatch : FishPrepatch + { + public override string? Description { get; } = "Adds custom gases to the debug fill all action"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.Debug_FillAll)); + + public static void Postfix(GasGrid __instance) + { + var gasGrids = __instance.ParallelGasGrids(); + if (gasGrids.Length < 3) + return; + + var cellCount = __instance.gasDensity.Length; + var cellIndices = __instance.map.cellIndices; + + for (var i = 0; i < cellCount; i++) + { + if (!__instance.GasCanMoveTo(cellIndices.IndexToCell(i))) + continue; + + for (var j = 3; j < gasGrids.Length; j++) + gasGrids[j].SetDirect(i, 255); + } + } + } + + public sealed class Notify_ThingSpawnedPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.Notify_ThingSpawned)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(GasGrid __instance, Thing thing) + { + if (!thing.IsSpawned() || thing.def.Fillage != FillCategory.Full) + return; + + var grids = __instance.ParallelGasGrids(); + var gridCount = grids.Length; + var map = __instance.map; + var mapDrawer = map.mapDrawer; + var mapSizeX = GetMapSizeX(map); + + foreach (var occupiedCell in thing.OccupiedRect()) + { + var anyGridDirty = false; + + for (var i = gridCount; i-- > 0;) + { + var grid = grids[i]; + var cellIndex = occupiedCell.CellToIndex(mapSizeX); + + if (!grid.AnyGasAt(cellIndex)) + continue; + + grid.SetDirect(cellIndex, 0); + anyGridDirty = true; + } + + if (anyGridDirty) + mapDrawer.MapMeshDirty(occupiedCell, MapMeshFlag.Gas); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetMapSizeX(Map map) => map.cellIndices.mapSizeX; + } + + public sealed class ExposeDataPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.ExposeData)); + + public static void Prefix(GasGrid __instance) + { + if (Scribe.mode != LoadSaveMode.Saving) + return; + + var grids = __instance.ParallelGasGrids(); + var smokeGrid = grids[0]; + var toxicGrid = grids[1]; + var rotStinkGrid = grids[2]; + + for (var i = smokeGrid.CellCount; i-- > 0;) + { + __instance.gasDensity[i] = (rotStinkGrid.DensityAt(i) << 16) + | (toxicGrid.DensityAt(i) << 8) + | smokeGrid.DensityAt(i); + } + } + + public static void Postfix(GasGrid __instance) + { + if (Scribe.mode != LoadSaveMode.LoadingVars) + return; + + var grids = __instance.ParallelGasGrids(); + var smokeGrid = grids[0]; + var toxicGrid = grids[1]; + var rotStinkGrid = grids[2]; + + for (var i = smokeGrid.CellCount; i-- > 0;) + { + var gasDensityAtCell = __instance.gasDensity[i]; + + smokeGrid.SetDirect(i, (byte)gasDensityAtCell); + toxicGrid.SetDirect(i, (byte)((uint)gasDensityAtCell >> 8)); + rotStinkGrid.SetDirect(i, (byte)((uint)gasDensityAtCell >> 16)); + } + } + } + + public sealed class DebugToolsGeneralPushGas : FishPrepatch + { + public override string? Description { get; } = "Adds a debug action for adding custom gas defs"; + + public override MethodBase TargetMethodBase { get; } = methodof(DebugToolsGeneral.PushGas); + + public static List Postfix(List __result) + { + var gasDefs = DefDatabase.AllDefsListForReading; + + for (var i = 3; i < gasDefs.Count; i++) + { + var gasDef = gasDefs[i]; + + __result.Add(new(gasDef.LabelCap, DebugActionType.ToolMap, + () => Find.CurrentMap.gasGrid.AddGas(UI.MouseCell(), gasDef, 5f))); + } + + return __result; + } + } + + public sealed class MouseOverReadOutOnGUI : FishPrepatch + { + public override string? Description { get; } + = "Adds custom gas defs to the mouse over readout on the bottom left"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(MouseoverReadout), nameof(MouseoverReadout.MouseoverReadoutOnGUI)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + { + var drawGasMethod = AccessTools.DeclaredMethod(typeof(MouseoverReadout), nameof(MouseoverReadout.DrawGas)); + + var instructions = ilProcessor.instructions; + + var lastDrawGasIndex = instructions.Count - instructions.Reverse().FirstIndexOf(instruction + => instruction.Operand is MethodReference method && method.Is(drawGasMethod)) - 1; + + var heightVariable = instructions[lastDrawGasIndex - 1]; + if (heightVariable.OpCode != OpCodes.Ldloca_S && heightVariable.OpCode != OpCodes.Ldloca) + { + ThrowHelper.ThrowInvalidOperationException( + $"Failed to find height variable for MouseOverReadOutOnGUI patch. Expected Ldloca_S or " + + $"Ldloca, got '{heightVariable}' instead."); + } + + var cellVariable = instructions[lastDrawGasIndex - 4]; + if (!cellVariable.OpCode.Name.Contains("ldloc", StringComparison.OrdinalIgnoreCase)) + { + ThrowHelper.ThrowInvalidOperationException( + $"Failed to cell variable for MouseOverReadOutOnGUI patch. Expected Ldloc, got { + cellVariable} instead."); + } + + ilProcessor.InsertRange(lastDrawGasIndex + 1, + cellVariable, + heightVariable, + (OpCodes.Call, methodof(DrawCustomGases))); + } + + public static void DrawCustomGases(IntVec3 cell, ref float curYOffset) + { + var map = Find.CurrentMap; + var gasGrids = map.gasGrid.ParallelGasGrids(); + var cellIndex = new CellIndex(cell, map); + + for (var i = 3; i < gasGrids.Length; i++) + { + var gasGrid = gasGrids[i]; + if (!gasGrid.AnyGasAt(cellIndex)) + continue; + + Widgets.Label( + new(MouseoverReadout.BotLeft.x, UI.screenHeight - MouseoverReadout.BotLeft.y - curYOffset, 999f, + 999f), + string.Concat(gasGrid.GasDef.LabelCap, " ", + (gasGrid.DensityAt(cellIndex) / 255f).ToStringPercent("F0"))); + + curYOffset += 19f; + } + } + } + + public class ParallelGasGrid + { + private ulong[] _gasCoverageForIndicesInRandomOrder; + private byte[] _gasDensity; + + public event Action? Ticked; + + public GasDef GasDef { get; private set; } + + public int DissipationRate => GasDef.dissipationRate; + + public bool Diffuses => GasDef.diffuses; + + public Color Color => GasDef.color; + + public bool CalculateGasEffects => anyGasEverAdded; + + public Map Map; + + public int + CycleIndexDiffusion, + CycleIndexDissipation; + + public IntVec3[] CardinalDirections; + + public List CellsInRandomOrder = null!; + + public int[] + CellIndicesInRandomOrder = null!, + SourceIndicesOfRandomCells = null!; + + public bool anyGasEverAdded; + + private DissipationTicker _dissipationTicker; + private DiffusionTicker _diffusionTicker; + + public int CellCount { get; private set; } + +#pragma warning disable CS8618 + public ParallelGasGrid(Map map, GasDef gasDef) +#pragma warning restore CS8618 + => map.InvokeWhenCellIndicesReady(m => Initialize(m, gasDef)); + + private void Initialize(Map map, GasDef gasDef) + { + GasDef = gasDef; + Map = map; + CellCount = map.cellIndices.NumGridCells; + _gasDensity = new byte[CellCount]; + _gasCoverageForIndicesInRandomOrder = new ulong[((_gasDensity.Length - 1) >> 6) + 1]; + CellIndicesInRandomOrder = new int[CellCount]; + SourceIndicesOfRandomCells = new int[CellCount]; + CardinalDirections = new IntVec3[GenAdj.CardinalDirections.Length]; + Array.Copy(GenAdj.CardinalDirections, CardinalDirections, GenAdj.CardinalDirections.Length); + CycleIndexDiffusion = Rand.Range(0, map.Area >> 1); + + _dissipationTicker = new() { GasGrid = this }; + _diffusionTicker = new() { GasGrid = this }; + + Ticked += _onFirstTickAction; + } + + private static Action _onFirstTickAction = TickInitialize; + + private static void TickInitialize(ParallelGasGrid grid) + { + // map.info is somehow null during ExposeData, which itself runs after CreateComponents and initializes a + // 2nd copy of every component for each map instance, of which apparently three get created per map, two + // of those thrown away, so cellsInRandomOrder.GetAll fails and throws when accessed from either + // CreateComponents or ExposeData while loading a game. A total of 6 instances of each component get + // created per map while loading it's stupid + // Also cellsInRandomOrder itself initializes after gasGrid within CreateComponents, but that's easy to + // solve + grid.CellsInRandomOrder = grid.Map.cellsInRandomOrder.GetAll(); + grid.UpdateCellIndicesInRandomOrder(); + grid.Ticked -= _onFirstTickAction; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int CellToIndex(in IntVec3 cell) => cell.CellToIndex(Map); + + public void UpdateCellIndicesInRandomOrder() + { + if (Map.cellIndices.NumGridCells != CellCount) + { + Log.Error($"Map size changed from {CellCount} to { + Map.cellIndices.NumGridCells}. This is not supported."); + return; + } + + var mapSizeX = Map.Size.x; + for (var i = CellIndicesInRandomOrder.Length; i-- > 0;) + { + SourceIndicesOfRandomCells[ + CellIndicesInRandomOrder[i] = CellIndicesUtility.CellToIndex(CellsInRandomOrder[i], mapSizeX)] = i; + } + + _gasCoverageForIndicesInRandomOrder.Clear(); + + for (var i = _gasDensity.Length; i-- > 0;) + { + if (_gasDensity[i] > 0) + SetDirect(i, _gasDensity[i]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool AnyGasAt(int idx) => _gasDensity[idx] > 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool AnyGasAt(in IntVec3 cell) => AnyGasAt(CellToIndex(cell)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool AnyGasAt(CellIndex cellIndex) => AnyGasAt(cellIndex.Value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte DensityAt(int index) => _gasDensity[index]; + + public byte DensityAt(in IntVec3 cell) => DensityAt(CellToIndex(cell)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte DensityAt(CellIndex cellIndex) => DensityAt(cellIndex.Value); + + public void SetDirect(in IntVec3 cell, byte density) => SetDirect(CellToIndex(cell), density); + + public void SetDirect(CellIndex cellIndex, byte density) => SetDirect(cellIndex.Value, density); + + public void SetDirect(int index, byte density) + { + _gasDensity[index] = density; + + index = SourceIndicesOfRandomCells[index]; + + ref var gasCoverageBucket = ref _gasCoverageForIndicesInRandomOrder[index >> 6]; + var hasGas = density > 0; + gasCoverageBucket ^= ((ulong)-(long)hasGas.AsInt() ^ gasCoverageBucket) & (1UL << (index & 63)); + anyGasEverAdded |= hasGas; + } + + public void AddGas(CellIndex cellIndex, int amount, bool canOverflow = true) + => AddGas(IndexToCell(cellIndex.Value), amount, canOverflow); + + public void AddGas(in IntVec3 cell, int amount, bool canOverflow = true) + { + if (amount <= 0 || !GasCanMoveTo(cell)) + return; + + anyGasEverAdded = true; + var index = CellToIndex(cell); + + SetDirect(index, AdjustedDensity(_gasDensity[index] + amount, out var overflow)); + + Map.mapDrawer.MapMeshDirty(cell, MapMeshFlag.Gas); + + if (canOverflow && overflow > 0) + Overflow(cell, overflow); + } + + private static byte AdjustedDensity(int newDensity, out int overflow) + { + if (newDensity > 255) + { + overflow = newDensity - 255; + return byte.MaxValue; + } + + overflow = 0; + if (newDensity < 0) + return 0; + + return (byte)newDensity; + } + + private void Overflow(in IntVec3 cell, int amount) + { + if (amount <= 0) + return; + + var remainingAmount = amount; + + Map.floodFiller.FloodFill(cell, GasCanMoveTo, c => + { + var num = Mathf.Min(remainingAmount, 255 - DensityAt(c)); + if (num > 0) + { + AddGas(c, num, false); + remainingAmount -= num; + } + + return remainingAmount <= 0; + }, GenRadial.NumCellsInRadius(40f), true); + } + + public void Clear() + { + _gasCoverageForIndicesInRandomOrder.Clear(); + _gasDensity.Clear(); + anyGasEverAdded = false; + } + + private abstract class BitwiseGasTicker + { + public required ParallelGasGrid GasGrid; + public abstract ref int CellCycleIndex { get; } + + public void TickCells(int area, int cellCountToTick) + { + var cellCycleIndex = CellCycleIndex; + + if ((cellCycleIndex & 63) != 0) + TickOddCells(area, ref cellCycleIndex); + + var gasCoverageArray = GasGrid._gasCoverageForIndicesInRandomOrder; + + cellCountToTick >>= 6; + cellCountToTick += 1; + for (var i = 0; i < cellCountToTick; i++) + { + if (area - cellCycleIndex < 64) + TickOddCells(area, ref cellCycleIndex); + + var gasCoverage = gasCoverageArray[cellCycleIndex >> 6]; + if (gasCoverage != default) + TickBucket(gasCoverage, cellCycleIndex); + + cellCycleIndex += 64; + } + + CellCycleIndex = cellCycleIndex; + } + + private void TickBucket(ulong gasCoverage, int cellCycleIndex) + { + for (var i = 0; i < 64;) + { + var gasCoverageSlice = (gasCoverage >> i) & 0xFF; + if (gasCoverageSlice == default) + { + i += 8; + continue; + } + + for (var j = 0; j < 8; j++) + { + if (((gasCoverageSlice >> j) & 1UL) != default) + TickingAction(cellCycleIndex + i); + + i++; + } + } + } + + public abstract void TickingAction(int cellCycleIndex); + + [MethodImpl(MethodImplOptions.NoInlining)] + private void TickOddCells(int area, ref int cellCycleIndex) + { + if (cellCycleIndex < area) + { + var gasCoverage = GasGrid._gasCoverageForIndicesInRandomOrder[cellCycleIndex >> 6]; + + do + { + if ((cellCycleIndex & 63) == 0) + return; + + if (((gasCoverage >> cellCycleIndex) & 1UL) != default) + TickingAction(cellCycleIndex); + + cellCycleIndex++; + } + while (cellCycleIndex < area); + } + + cellCycleIndex = 0; + } + } + + private sealed class DissipationTicker : BitwiseGasTicker + { + public override ref int CellCycleIndex => ref GasGrid.CycleIndexDissipation; + + public override void TickingAction(int cellCycleIndex) + => GasGrid.DissipateGasAt(GasGrid.CellIndicesInRandomOrder[cellCycleIndex]); + } + + private sealed class DiffusionTicker : BitwiseGasTicker + { + public override ref int CellCycleIndex => ref GasGrid.CycleIndexDiffusion; + + public override void TickingAction(int cellCycleIndex) + => GasGrid.DiffuseGasAt(GasGrid.CellsInRandomOrder[cellCycleIndex]); + } + + public void Tick() + { + if (!CalculateGasEffects) + { +#if GAS_DEBUG_L1 + if (CheckCurrentlyContainsGas()) + { + anyGasEverAdded = true; + Debug.LogIncorrectAnyGasEverAdded(this); + } +#endif + + return; + } + + Ticked?.Invoke(this); + + var area = Map.Area; + // CellsInRandomOrder = Map.cellsInRandomOrder.GetAll(); + // UpdateCellIndicesInRandomOrder(); + // Assigned in PostInitialize instead. Ludeon was likely doing this due to component construction order in + // Map.ConstructComponents + +#if GAS_DEBUG_L2 + using var previousDensity = new PooledArray(_gasDensity.Length); + Array.Copy(_gasDensity, 0, previousDensity.BackingArray, 0, _gasDensity.Length); +#endif + + _dissipationTicker.TickCells(area, (area + 63) >> 6); // Mathf.CeilToInt(area * (1f / 64f)) + + if (Diffuses) + _diffusionTicker.TickCells(area, (area + 31) >> 5); // Mathf.CeilToInt(area * (1f / 32f)) + +#if GAS_DEBUG_L2 + Debug.LogTotalGasChange(this, previousDensity); +#endif + + if (Map.IsHashIntervalTick(600)) + RecalculateEverHadGas(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public void RecalculateEverHadGas() => anyGasEverAdded = CheckCurrentlyContainsGas(); + + private bool CheckCurrentlyContainsGas() + { + for (var i = _gasCoverageForIndicesInRandomOrder.Length; i-- > 0;) + { + if (_gasCoverageForIndicesInRandomOrder[i] != default) + return true; + } + + return false; + } + + private void DissipateGasAt(int index) + { +#if GAS_DEBUG_L1 + var previousDensity = _gasDensity[index]; + if (previousDensity == 0) + Debug.LogInvalidDissipationAttempt(this, index); +#endif + + var densityAfterDissipation = Math.Max(_gasDensity[index] - DissipationRate, 0); + + Debug.LogDissipationRate(this, index, densityAfterDissipation); + SetDirect(index, (byte)densityAfterDissipation); + +#if GAS_DEBUG_L1 + if (_gasDensity[index] >= previousDensity) + Debug.LogFailedDissipation(this, index, previousDensity); +#endif + + if (densityAfterDissipation == 0) + { + Debug.LogMapMeshDirty(this, index); + Map.mapDrawer.MapMeshDirty(IndexToCell(index), MapMeshFlag.Gas); + } + } + + private void DiffuseGasAt(IntVec3 cell) + { + var originCellIndex = CellToIndex(cell); + int densityAtOriginCell = _gasDensity[originCellIndex]; + if (densityAtOriginCell < 17) + return; + + var diffusedToAnyCell = false; + CardinalDirections.Shuffle(); + + for (var i = 0; i < CardinalDirections.Length; i++) + { + var otherCell = cell + CardinalDirections[i]; + if (!GasCanMoveTo(otherCell)) + continue; + + var otherCellIndex = CellToIndex(otherCell); + int densityOtOtherCell = _gasDensity[otherCellIndex]; + + if (!TryDiffuseIndividualGas(ref densityAtOriginCell, ref densityOtOtherCell)) + continue; + + SetDirect(otherCellIndex, (byte)densityOtOtherCell); + Map.mapDrawer.MapMeshDirty(otherCell, MapMeshFlag.Gas); + diffusedToAnyCell = true; + if (densityAtOriginCell < 17) + break; + } + + if (!diffusedToAnyCell) + return; + + SetDirect(originCellIndex, (byte)densityAtOriginCell); + Map.mapDrawer.MapMeshDirty(cell, MapMeshFlag.Gas); + } + + private static bool TryDiffuseIndividualGas(ref int gasA, ref int gasB) + { + if (gasA < 17) + return false; + + var num = Mathf.Abs(gasA - gasB) >> 1; + + if (gasA <= gasB || num < 17) + return false; + + Debug.LogDiffusionRate(gasA, gasB, num); + + gasA -= num; + gasB += num; + +#if GAS_DEBUG_L1 + if (gasA < 0 || gasB < 0) + Log.Error("Diffused below 0!"); +#endif + + return true; + } + + public bool GasCanMoveTo(IntVec3 cell) + { + if (!cell.InBounds(Map)) + return false; + if (cell.Filled(Map)) + return cell.GetDoor(Map)?.Open ?? false; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private IntVec3 IndexToCell(int index) + => CellIndicesUtility.IndexToCell(index, Map.cellIndices.mapSizeX); + + /// + /// For modders. Performance Fish doesn't automatically scribe added custom defs. + /// + public void Scribe(string label) + => MapExposeUtility.ExposeUshort(Map, + cell => DensityAt(cell), + (cell, value) => SetDirect(cell, (byte)value), + label); + + // ReSharper disable UnusedMember.Local + private static class Debug + { + [MethodImpl(MethodImplOptions.NoInlining)] + [Conditional("GAS_DEBUG_L1")] + internal static void LogIncorrectAnyGasEverAdded(ParallelGasGrid grid) + { + var gasDensities = grid._gasDensity; + var firstCellIndexWithGas = Array.FindIndex(gasDensities, static g => g > 0); + + Log.Error($"GasGrid of type '{grid.GasDef.defName}' contains gas at cell '{ + grid.IndexToCell(firstCellIndexWithGas)}' with index { + firstCellIndexWithGas}, but anyGasEverAdded=false. This index had a source index of { + grid.SourceIndicesOfRandomCells[firstCellIndexWithGas]} before randomization"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + [Conditional("GAS_DEBUG_L1")] + internal static void LogInvalidDissipationAttempt(ParallelGasGrid grid, int index) + => Log.Error($"Tried to dissipate gas at cell '{ + grid.IndexToCell(index)}' with density of 0"); + + [MethodImpl(MethodImplOptions.NoInlining)] + [Conditional("GAS_DEBUG_L1")] + internal static void LogFailedDissipation(ParallelGasGrid grid, int index, byte previousDensity) + => Log.Error($"GasDensity went from '{previousDensity}' to '{grid._gasDensity[index]}' at cell '{ + grid.IndexToCell(index)}' while trying to dissipate. It should've decreased"); + + [MethodImpl(MethodImplOptions.NoInlining)] + [Conditional("GAS_DEBUG_L2")] + internal static void LogTotalGasChange(ParallelGasGrid grid, PooledArray previousDensity) + => Log.Message($"Total density for {grid.GasDef.defName} changed from { + previousDensity.Sum(static b => b)} to {grid._gasDensity.Sum(static b => b)}"); + + [Conditional("GAS_DEBUG_L3")] + internal static void LogMapMeshDirty(ParallelGasGrid grid, int index) + => Log.Message($"Dirtied MapMesh for '{grid.GasDef.defName}' at '{index}'"); + + [Conditional("GAS_DEBUG_L3")] + internal static void LogDissipationRate(ParallelGasGrid grid, int index, int desiredResult) + => Log.Message($"Dissipating {grid._gasDensity[index] - desiredResult} gas of type '{ + grid.GasDef.defName}' at cell '{grid.IndexToCell(index)}' with target rate { + grid.DissipationRate}. Was {grid._gasDensity[index]}, becomes {desiredResult}"); + + [Conditional("GAS_DEBUG_L3")] + internal static void LogDiffusionRate(int gasA, int gasB, int num) + => Log.Message($"Diffusing {num} gas from cell with density {gasA} to cell with density {gasB}"); + } + // ReSharper restore UnusedMember.Local + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/GenLocalDateCaching.cs b/Source/PerformanceFish/GenLocalDateCaching.cs index 12676a5..0fcfb04 100644 --- a/Source/PerformanceFish/GenLocalDateCaching.cs +++ b/Source/PerformanceFish/GenLocalDateCaching.cs @@ -10,9 +10,9 @@ using System.Reflection.Emit; namespace PerformanceFish; -public class GenLocalDateCaching : ClassWithFishPatches +public sealed class GenLocalDateCaching : ClassWithFishPatches { - public class DayTickByThing_Patch : FishPatch + public sealed class DayTickByThing_Patch : FishPatch { public override string Description { get; } = "Caches results of GenLocalDate.DayTick for the first map. This is similar to Rim73's mind state " @@ -60,7 +60,7 @@ public class GenLocalDateCaching : ClassWithFishPatches //} } - public class DayTickByMap_Patch : FishPatch + public sealed class DayTickByMap_Patch : FishPatch { public override string Description { get; } = "Caches results of GenLocalDate.DayTick for the first map. This is similar to Rim73's mind state " diff --git a/Source/PerformanceFish/GenTypesPatches.cs b/Source/PerformanceFish/GenTypesPatches.cs index 8a120c5..8e3fab4 100644 --- a/Source/PerformanceFish/GenTypesPatches.cs +++ b/Source/PerformanceFish/GenTypesPatches.cs @@ -10,9 +10,9 @@ using PerformanceFish.Prepatching; namespace PerformanceFish; -public class GenTypesPatches : ClassWithFishPrepatches +public sealed class GenTypesPatches : ClassWithFishPrepatches { - public class AllTypes : FishPrepatch + public sealed class AllTypes : FishPrepatch { public override string? Description { get; } = "Fix for thread safety. The vanilla method was rarely breaking and preventing the game from loading"; @@ -29,7 +29,7 @@ public class GenTypesPatches : ClassWithFishPrepatches public static void Postfix() => Monitor.Exit(Lock); // TODO: replace with finalizer } - public class AllTypesWithAttribute : FishPrepatch + public sealed class AllTypesWithAttribute : FishPrepatch { public override string? Description { get; } = "Fix for thread safety. The vanilla method was rarely breaking and preventing the game from loading"; @@ -62,7 +62,7 @@ public class GenTypesPatches : ClassWithFishPrepatches => static type => type.HasAttribute(); } - public class AllSubclasses : FishPrepatch + public sealed class AllSubclasses : FishPrepatch { public override string? Description { get; } = "Fix for thread safety. The vanilla method was rarely breaking and preventing the game from loading"; @@ -94,14 +94,14 @@ public class GenTypesPatches : ClassWithFishPrepatches public static Func Predicate = new PredicateClass().Invoke; - public class PredicateClass + public sealed class PredicateClass { public Type? BaseType; public bool Invoke(Type type) => type.IsSubclassOf(BaseType!); } } - public class AllSubclassesNonAbstract : FishPrepatch + public sealed class AllSubclassesNonAbstract : FishPrepatch { public override string? Description { get; } = "Fix for thread safety, paired with a small optimization for faster loading. The vanilla method was " diff --git a/Source/PerformanceFish/GeneTrackerOptimization.cs b/Source/PerformanceFish/GeneTrackerOptimization.cs index 722eb07..e423e57 100644 --- a/Source/PerformanceFish/GeneTrackerOptimization.cs +++ b/Source/PerformanceFish/GeneTrackerOptimization.cs @@ -10,9 +10,9 @@ using PerformanceFish.Prepatching; namespace PerformanceFish; -public class GeneTrackerOptimization : ClassWithFishPrepatches +public sealed class GeneTrackerOptimization : ClassWithFishPrepatches { - public class GeneTrackerTickPatch : FishPrepatch + public sealed class GeneTrackerTickPatch : FishPrepatch { public override string? Description { get; } = "Every pawn has a gene tracker, which is responsible for ticking each of their genes. Normally it ticks " @@ -41,7 +41,7 @@ public class GeneTrackerOptimization : ClassWithFishPrepatches genesToTick[i].Tick(); } - if (instance.pawn.Spawned + if (instance.pawn.IsSpawned() && instance.Xenotype != XenotypeDefOf.Baseliner && instance.pawn.IsHashIntervalTick(300)) { diff --git a/Source/PerformanceFish/GetCompCaching.cs b/Source/PerformanceFish/GetCompCaching.cs index 0a01b50..5bbe788 100644 --- a/Source/PerformanceFish/GetCompCaching.cs +++ b/Source/PerformanceFish/GetCompCaching.cs @@ -10,9 +10,9 @@ using RimWorld.Planet; namespace PerformanceFish; -public class GetCompCaching : ClassWithFishPrepatches +public sealed class GetCompCaching : ClassWithFishPrepatches { - public class ThingCompPatch : FishPrepatch + public sealed class ThingCompPatch : FishPrepatch { public override string? Description { get; } = "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x " @@ -73,7 +73,132 @@ public class GetCompCaching : ClassWithFishPrepatches } } - public class HediffCompPatch : FishPrepatch + public sealed class ThingCompPropertiesPatch : FishPrepatch + { + public override string? Description { get; } + = "Optimizes GetCompProperties lookups with a fast custom dictionary implementation. This is often more " + + "than 10x faster than the vanilla method. Replaces Performance Optimizer's error prone and slower " + + "optimization."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.Method(typeof(ThingDef), nameof(ThingDef.GetCompProperties)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(GetCompProperties); + + public static T? GetCompProperties(ThingDef thingDef) where T : CompProperties + { + if (thingDef.comps != null) + { + ref var cache = ref Cache.ByInt>.GetOrAddReference(thingDef.shortHash); + return !cache.IsDirty(thingDef.comps) ? cache.Comp : UpdateCache(thingDef, ref cache); + } + return null; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static T? UpdateCache(ThingDef thingDef, ref CacheValue cache) + where T : CompProperties + { + for (var i = 0; i < thingDef.comps.Count; i++) + { + if (thingDef.comps[i] is not T comp) + continue; + + cache.Update(thingDef, comp); + return comp; + } + + cache.Update(thingDef, null); + return null; + } + + public record struct CacheValue where T : CompProperties + { + private int _listVersion = -1; + public T? Comp; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDirty(List comps) => comps._version != _listVersion; + + [MethodImpl(MethodImplOptions.NoInlining)] + public void Update(ThingDef thingDef, T? comp) + { + if (thingDef.shortHash == default) + return; + + _listVersion = thingDef.comps._version; + Comp = comp; + } + + public CacheValue() {} + } + } + + public sealed class ThingDefHasCompPatch : FishPrepatch + { + public override string? Description { get; } + = "Optimizes HasComp lookups with a fast custom dictionary implementation. This is often more than 10x " + + "faster than the vanilla method. Replaces Performance Optimizer's error prone and slower optimization."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.Method(typeof(ThingDef), nameof(ThingDef.HasComp)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(HasComp); + + public static bool HasComp(ThingDef thingDef, Type compType) + { + if (thingDef.comps != null) + { + ref var cache + = ref Cache.ByReference.GetOrAddReference(thingDef.shortHash, + compType.TypeHandle); + + return !cache.IsDirty(thingDef.comps) ? cache.HasComp : UpdateCache(thingDef, compType, ref cache); + } + return false; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool UpdateCache(ThingDef thingDef, Type compType, ref CacheValue cache) + { + for (var i = 0; i < thingDef.comps.Count; i++) + { + if (thingDef.comps[i].compClass != compType) + continue; + + cache.Update(thingDef, true); + return true; + } + + cache.Update(thingDef, false); + return false; + } + + public record struct CacheValue + { + private int _listVersion = -1; + public bool HasComp; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDirty(List comps) => comps._version != _listVersion; + + [MethodImpl(MethodImplOptions.NoInlining)] + public void Update(ThingDef thingDef, bool hasComp) + { + if (thingDef.shortHash == default) + return; + + _listVersion = thingDef.comps._version; + HasComp = hasComp; + } + + public CacheValue() {} + } + } + + public sealed class HediffCompPatch : FishPrepatch { public override string? Description { get; } = "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x " @@ -134,7 +259,7 @@ public class GetCompCaching : ClassWithFishPrepatches } } - public class HediffCompPropsPatch : FishPrepatch + public sealed class HediffCompPropsPatch : FishPrepatch { public override string? Description { get; } = "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x " @@ -195,7 +320,7 @@ public class GetCompCaching : ClassWithFishPrepatches } } - public class AbilityCompPatch : FishPrepatch + public sealed class AbilityCompPatch : FishPrepatch { public override string? Description { get; } = "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x " @@ -256,7 +381,7 @@ public class GetCompCaching : ClassWithFishPrepatches } } - public class WorldObjectCompPatch : FishPrepatch + public sealed class WorldObjectCompPatch : FishPrepatch { public override string? Description { get; } = "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x " @@ -312,7 +437,7 @@ public class GetCompCaching : ClassWithFishPrepatches } } - public class MapComponentPatch : FishPrepatch + public sealed class MapComponentPatch : FishPrepatch { public override string? Description { get; } = "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x " @@ -368,7 +493,7 @@ public class GetCompCaching : ClassWithFishPrepatches } } - public class WorldComponentPatch : FishPrepatch + public sealed class WorldComponentPatch : FishPrepatch { public override string? Description { get; } = "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x " @@ -419,7 +544,7 @@ public class GetCompCaching : ClassWithFishPrepatches } } - public class GameComponentPatch : FishPrepatch + public sealed class GameComponentPatch : FishPrepatch { public override string? Description { get; } = "Optimizes GetComp lookups with a fast custom dictionary implementation. This is often more than 10x " diff --git a/Source/PerformanceFish/GroupThingPair.cs b/Source/PerformanceFish/GroupThingPair.cs new file mode 100644 index 0000000..7984172 --- /dev/null +++ b/Source/PerformanceFish/GroupThingPair.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using JetBrains.Annotations; + +namespace PerformanceFish; + +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +public record struct GroupThingPair +{ + private uint _data; + + public ThingRequestGroup ThingRequestGroup + { + get => (ThingRequestGroup)(byte)(_data >> 24); // fewer hash collisions this way than in rightmost bit + set => _data = (_data & 0x00_FF_FF_FF) | ((uint)value << 24); + } + + public int ThingIDNumber + { + get => (int)(_data & 0x00_FF_FF_FF); // 24 bit, 16.8m MaxValue + set => _data = (_data & 0xFF_00_00_00) | (uint)(value & 0x00_FF_FF_FF); + } + + public GroupThingPair(ThingRequestGroup thingRequestGroup, int thingIDNumber) + => _data = ((uint)thingRequestGroup << 24) | (0x00_FF_FF_FF & (uint)thingIDNumber); + + public bool Equals(GroupThingPair other) => _data == other._data; + + public override int GetHashCode() => unchecked((int)_data); +} + +// [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +// public record struct GroupThingPair +// { +// private uint _data; +// +// public ThingRequestGroup ThingRequestGroup +// { +// get => (ThingRequestGroup)(byte)_data; +// set => _data = (_data & 0xFF_FF_FF_00) | (byte)value; +// } +// +// public int ThingIDNumber +// { +// get => (int)(_data >> 8); // 24 bit, 16.8m MaxValue +// set => _data = (_data & 0xFF) | (uint)(value << 8); +// } +// +// public GroupThingPair(ThingRequestGroup thingRequestGroup, int thingIDNumber) +// => _data = (uint)thingRequestGroup | ((uint)thingIDNumber << 8); +// +// public bool Equals(GroupThingPair other) => _data == other._data; +// +// public override int GetHashCode() => Unsafe.As(ref _data); +// } \ No newline at end of file diff --git a/Source/PerformanceFish/Hauling/GridsUtilityPatches.cs b/Source/PerformanceFish/Hauling/GridsUtilityPatches.cs new file mode 100644 index 0000000..a8e6dc1 --- /dev/null +++ b/Source/PerformanceFish/Hauling/GridsUtilityPatches.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Events; +using PerformanceFish.Prepatching; + +namespace PerformanceFish.Hauling; + +public sealed class GridsUtilityPatches : ClassWithFishPrepatches +{ + public sealed class GetItemCountPatch : FishPrepatch + { + public override int Priority => HarmonyLib.Priority.First; + + public override string? Description { get; } + = "Caches item counts in a grid, to not have to repeat counting on every request."; + + public override MethodBase TargetMethodBase { get; } = methodof(GridsUtility.GetItemCount); + + protected internal override void BeforeHarmonyPatching() => ThingEvents.Initialized += TryRegister; + + public static void TryRegister(Thing thing) + { + if (!thing.IsItem()) + return; + + var thingEvents = thing.Events(); + thingEvents.RegisteredAtThingGrid += Increment; + thingEvents.DeregisteredAtThingGrid += Decrement; + } + + public static void Increment(Thing thing, Map map, in IntVec3 cell) + => map.ItemCountGrid()[cell]++; + + public static void Decrement(Thing thing, Map map, in IntVec3 cell) + { + ref var itemCount = ref map.ItemCountGrid()[cell]; + itemCount--; + + if (itemCount < 0) + { + itemCount = 0; + ErrorForInvalidCount(thing); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ErrorForInvalidCount(Thing thing) + => Log.Error($"Tried decrementing item count at cell {thing.Position} for thing '{ + thing}', but it was already 0. This should never happen."); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReplacementBody(IntVec3 c, Map map) => map.ItemCountGrid()[c]; + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Hauling/HaulDestinationManagerCache.cs b/Source/PerformanceFish/Hauling/HaulDestinationManagerCache.cs new file mode 100644 index 0000000..1c67060 --- /dev/null +++ b/Source/PerformanceFish/Hauling/HaulDestinationManagerCache.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace PerformanceFish.Hauling; + +public record struct HaulDestinationManagerCache +{ + public static readonly int StoragePriorityCount = Enum.GetValues(typeof(StoragePriority)).Length; + + public event Action? PriorityChanged; + + public int[] GroupCountByPriority = new int[StoragePriorityCount]; + + [MethodImpl(MethodImplOptions.NoInlining)] + public void OnPriorityChanged(HaulDestinationManager manager) + { + var slotGroupsByPriority = manager.AllGroupsListInPriorityOrder; + + Array.Clear(GroupCountByPriority, 0, GroupCountByPriority.Length); + for (var i = slotGroupsByPriority.Count; i-- > 0;) + GroupCountByPriority[(int)slotGroupsByPriority[i].Settings.Priority]++; + + PriorityChanged?.Invoke(manager); + } + + public HaulDestinationManagerCache() + { + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Hauling/HaulDestinationManagerPatches.cs b/Source/PerformanceFish/Hauling/HaulDestinationManagerPatches.cs new file mode 100644 index 0000000..a1cf443 --- /dev/null +++ b/Source/PerformanceFish/Hauling/HaulDestinationManagerPatches.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using PerformanceFish.Prepatching; + +namespace PerformanceFish.Hauling; + +public sealed class HaulDestinationManagerPatches : ClassWithFishPrepatches +{ + public sealed class Notify_HaulDestinationChangedPriorityPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(HaulDestinationManager), + nameof(HaulDestinationManager.Notify_HaulDestinationChangedPriority)); + + public static void Postfix(HaulDestinationManager __instance) + => __instance.Cache().OnPriorityChanged(__instance); + } + + public sealed class AddHaulDestinationPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(HaulDestinationManager), + nameof(HaulDestinationManager.AddHaulDestination)); + + public static void Postfix(HaulDestinationManager __instance, IHaulDestination haulDestination) + { + if (haulDestination is ISlotGroupParent) + __instance.Cache().OnPriorityChanged(__instance); + } + } + + public sealed class RemoveHaulDestinationPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(HaulDestinationManager), + nameof(HaulDestinationManager.RemoveHaulDestination)); + + public static void Postfix(HaulDestinationManager __instance, IHaulDestination haulDestination) + { + if (haulDestination is ISlotGroupParent) + __instance.Cache().OnPriorityChanged(__instance); + } + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Hauling/LwmCompCacheValue.cs b/Source/PerformanceFish/Hauling/LwmCompCacheValue.cs new file mode 100644 index 0000000..a0ef995 --- /dev/null +++ b/Source/PerformanceFish/Hauling/LwmCompCacheValue.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +global using LwmCompCache = PerformanceFish.Cache.ByReference; + +namespace PerformanceFish.Hauling; + +public record struct LwmCompCacheValue +{ + public CompProperties? CompProperties; + + public static CompProperties? TryGetLwmProps(Thing slotGroupParent) + => ModCompatibility.Types.LWM.CompProperties != null + ? slotGroupParent.def.comps?.Find(static props + => props.GetType() == ModCompatibility.Types.LWM.CompProperties) + : null; + + static LwmCompCacheValue() => EnsureInitialized(); + + public static void EnsureInitialized() + { + if (Interlocked.Exchange(ref _initialized, 1) == 0) + LwmCompCache.ValueInitializer = static key => new() { CompProperties = TryGetLwmProps(key) }; + } + + private static volatile int _initialized; + + public static AccessTools.FieldRef? + LwmMaxNumberStacks = TryGetLwmField("maxNumberStacks"), + LwmMinNumberStacks = TryGetLwmField("minNumberStacks"); + + private static AccessTools.FieldRef? TryGetLwmField(string name) + => ModCompatibility.Types.LWM.CompProperties is { } propsType + ? AccessTools.FieldRefAccess(AccessTools.Field(propsType, name)) + : null; +} \ No newline at end of file diff --git a/Source/PerformanceFish/Hauling/SlotGroupExtensions.cs b/Source/PerformanceFish/Hauling/SlotGroupExtensions.cs new file mode 100644 index 0000000..7f4da02 --- /dev/null +++ b/Source/PerformanceFish/Hauling/SlotGroupExtensions.cs @@ -0,0 +1,147 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Diagnostics.CodeAnalysis; +using FisheryLib.Pools; + +namespace PerformanceFish.Hauling; + +public static class SlotGroupExtensions +{ + public const int + MINIMUM_DISTRICT_COUNT = 3, + MINIMUM_DISTRICT_SLOTS = 9, + MINIMUM_DISTRICT_CELLS = 3; + + public static bool ShouldHaveDistricts([NotNullWhen(true)] this SlotGroup? slotGroup, out int cellCountIfTrue, + out int totalSlotsIfTrue) + { + if (slotGroup is null || slotGroup.parent is Building { Spawned: false }) + { + Unsafe.SkipInit(out cellCountIfTrue); + goto False; + } + + cellCountIfTrue = slotGroup.CellsList.Count; + if (cellCountIfTrue >= MINIMUM_DISTRICT_COUNT * MINIMUM_DISTRICT_CELLS) + { + return (totalSlotsIfTrue = slotGroup.GetTotalSlots(cellCountIfTrue)) + >= MINIMUM_DISTRICT_COUNT * MINIMUM_DISTRICT_SLOTS; + } + + False: + Unsafe.SkipInit(out totalSlotsIfTrue); + return false; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ResetAndRemakeDistricts(this SlotGroup slotGroup) + { + slotGroup.ResetDistricts(); + slotGroup.NotifyCellCountChanged(); + } + + public static void ResetDistricts(this SlotGroup slotGroup) + { + ref var districts = ref slotGroup.Districts(); + + for (var i = districts.Length; i-- > 0;) + districts[i].Dispose(); + + districts = StorageDistrict.GetDefaultArray(); + } + + public static void NotifyCellCountChanged(this SlotGroup slotGroup) + { + if (slotGroup.ShouldHaveDistricts(out var cellCount, out var totalSlots)) + slotGroup.AdjustDistricts(totalSlots, cellCount); + else + slotGroup.ResetDistricts(); + } + + public static void AdjustDistricts(this SlotGroup slotGroup, int totalSlots, int cellCount, + Span cellsToAdd = default, Span cellsToRemove = default) + { + var districtCount = Math.Min(totalSlots / MINIMUM_DISTRICT_SLOTS, cellCount / MINIMUM_DISTRICT_COUNT); + ref var districts = ref slotGroup.Districts(); + + using var cellBufferPooled = new PooledIList>(); + var cellBuffer = cellBufferPooled.List; + var hasDistricts = districts.Length > 1; + + if (cellsToRemove.Length > 0) + { + if (hasDistricts) + { + for (var i = cellsToRemove.Length; i-- > 0;) + { + for (var j = districts.Length; j-- > 0;) + { + if (districts[j].TryRemoveCell(cellsToRemove[i])) + break; + } + } + } + else + { + Log.Error($"Tried removing cells from districts in slotGroup of { + slotGroup.parent}, but none exist."); + } + } + + if (cellsToAdd.Length > 0 && hasDistricts) + { + for (var i = cellsToAdd.Length; i-- > 0;) + cellBuffer.Add(cellsToAdd[i]); + } + + if (hasDistricts) + { + if (districts.Length > districtCount) + { + for (var i = districtCount - 1; i < districts.Length; i++) + cellBuffer.AddRange(districts[i].Cells); + } + } + else + { + cellBuffer.AddRange(slotGroup.CellsList); + } + + if (districts.Length != districtCount) + { + if (hasDistricts) + Array.Resize(ref districts, districtCount); + else + districts = new StorageDistrict[districtCount]; + + for (var i = 0; i < districts.Length; i++) + ((StorageDistrict?[])districts)[i] ??= new(slotGroup); + } + + var districtSize = cellCount / districtCount; + var finalDistrictSize = cellCount - (districtSize * (districtCount - 1)); + + for (var i = 0; i < districtCount; i++) + { + var district = districts[i]; + var desiredDistrictSize = i != districtCount - 1 ? districtSize : finalDistrictSize; + + while (district.Cells.Count > desiredDistrictSize) + cellBuffer.Add(district.PopCell()); + } + + for (var i = 0; i < districtCount; i++) + { + var district = districts[i]; + + while (district.Cells.Count < districtSize) + district.AddCell(cellBuffer.Pop()); + } + + while (cellBuffer.Count > 0) + districts[^1].AddCell(cellBuffer.Pop()); + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Hauling/SlotGroupPrepatches.cs b/Source/PerformanceFish/Hauling/SlotGroupPrepatches.cs new file mode 100644 index 0000000..a4449bb --- /dev/null +++ b/Source/PerformanceFish/Hauling/SlotGroupPrepatches.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using PerformanceFish.Prepatching; + +namespace PerformanceFish.Hauling; + +public sealed class SlotGroupPrepatches : ClassWithFishPrepatches +{ + public sealed class Notify_AddedCellPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(SlotGroup), nameof(SlotGroup.Notify_AddedCell)); + + public static void Postfix(SlotGroup __instance, IntVec3 c) + { + if (__instance.ShouldHaveDistricts(out var cellCount, out var totalSlots)) + __instance.AdjustDistricts(totalSlots, cellCount, stackalloc IntVec3[] { c }); + } + } + + public sealed class Notify_LostCellPatch : FishPrepatch + { + public override bool ShowSettings => false; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(SlotGroup), nameof(SlotGroup.Notify_LostCell)); + + public static void Postfix(SlotGroup __instance, IntVec3 c) + { + if (__instance.Districts().Length <= 1) + return; + + if (__instance.ShouldHaveDistricts(out var cellCount, out var totalSlots)) + __instance.AdjustDistricts(totalSlots, cellCount, cellsToRemove: stackalloc IntVec3[] { c }); + else + __instance.ResetDistricts(); + } + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Hauling/StorageDistrict.cs b/Source/PerformanceFish/Hauling/StorageDistrict.cs new file mode 100644 index 0000000..b05ba87 --- /dev/null +++ b/Source/PerformanceFish/Hauling/StorageDistrict.cs @@ -0,0 +1,125 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace PerformanceFish.Hauling; + +public class StorageDistrict +{ + public int FreeSlots = 0xFFFFFFF; + + private int _storedThingCount; + + public int TotalSlots => Parent.GetTotalSlots(Cells.Count); + + public List Cells { get; } = []; + + private FishTable> _storedThingsByDef = new(); + + public SlotGroup Parent { get; } + + public Map Map { get; } = null!; + + public StorageDistrict(SlotGroup? parent) + { + Parent = parent!; + + if (parent != null) + Map = parent.Map; + } + + public void AddCell(in IntVec3 cell) + { + Cells.Add(cell); + var cellIndex = new CellIndex(cell, Map); + Map.StorageDistrictGrid()[cellIndex] = this; + + FreeSlots = TotalSlots - _storedThingCount; + + var thingsAtCell = cellIndex.GetThingList(Map); + for (var i = 0; i < thingsAtCell.Count; i++) + { + var thing = thingsAtCell[i]; + if (!thing.IsItem()) + continue; + + AddThing(thing); + } + } + + public IntVec3 PopCell() + { + var cell = Cells.Pop(); + DeregisterCell(cell); + return cell; + } + + public bool TryRemoveCell(in IntVec3 cell) + { + if (!Cells.Remove(cell)) + return false; + + DeregisterCell(cell); + return true; + } + + private void DeregisterCell(in IntVec3 cell) + { + var cellIndex = new CellIndex(cell, Map); + + ref var districtGridSlot = ref Map.StorageDistrictGrid()[cellIndex]; + if (districtGridSlot == this) + districtGridSlot = null; + + var thingsAtCell = cellIndex.GetThingList(Map); + for (var i = 0; i < thingsAtCell.Count; i++) + { + var thing = thingsAtCell[i]; + if (!thing.IsItem()) + continue; + + RemoveThing(thing); + } + } + + public void AddThing(Thing thing) + { + StoredThingsOfDef(thing.def).Add(thing); + + FreeSlots = TotalSlots - ++_storedThingCount; + } + + public void RemoveThing(Thing thing) + { + if (!StoredThingsOfDef(thing.def).Remove(thing)) + ThrowHelper.ThrowInvalidOperationException(); + + FreeSlots = TotalSlots - --_storedThingCount; + } + + public List StoredThingsOfDef(ThingDef def) => _storedThingsByDef.GetOrAdd(def.shortHash); + + public bool CapacityAllows(Thing t) => FreeSlots > 0 || StoredThingsOfDef(t.def).ContainsThingStackableWith(t); + + public static StorageDistrict[] GetDefaultArray() => _defaultDistrictArray; + + public void Dispose() + { + if (this == _defaultDistrict) + return; + + var districtGrid = Map.StorageDistrictGrid(); + var cells = Cells; + + for (var i = cells.Count; i-- > 0;) + { + ref var district = ref districtGrid[cells[i]]; + if (district == this) + district = null; + } + } + + private static StorageDistrict _defaultDistrict = new(null); + private static StorageDistrict[] _defaultDistrictArray = [_defaultDistrict]; +} \ No newline at end of file diff --git a/Source/PerformanceFish/Hauling/StorageExtensions.cs b/Source/PerformanceFish/Hauling/StorageExtensions.cs new file mode 100644 index 0000000..ea16f52 --- /dev/null +++ b/Source/PerformanceFish/Hauling/StorageExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace PerformanceFish.Hauling; + +public static class StorageExtensions +{ + public static int TrueItemsPerCell(this Thing slotGroupParent) + => slotGroupParent.TrueItemsPerCell(slotGroupParent.TryGetLwmCompProperties()); + + public static int TrueItemsPerCell(this Thing slotGroupParent, CompProperties? lwmProps) + => lwmProps != null + ? Math.Max(LwmCompCacheValue.LwmMinNumberStacks!(lwmProps), LwmCompCacheValue.LwmMaxNumberStacks!(lwmProps)) + : slotGroupParent is Building building + ? building.MaxItemsInCell + : 1; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CompProperties? TryGetLwmCompProperties(this Thing thing) + => LwmCompCache.GetOrAdd(thing).CompProperties; + + public static int GetTotalSlots(this SlotGroup slotGroup) => slotGroup.GetTotalSlots(slotGroup.CellsList.Count); + + public static int GetTotalSlots(this SlotGroup slotGroup, int cellCount) + => slotGroup.parent is Building storage + ? storage.TrueItemsPerCell() * cellCount + : cellCount; + + static StorageExtensions() => LwmCompCacheValue.EnsureInitialized(); +} \ No newline at end of file diff --git a/Source/PerformanceFish/Hauling/StorageSettingsPatches.cs b/Source/PerformanceFish/Hauling/StorageSettingsPatches.cs new file mode 100644 index 0000000..3ff1298 --- /dev/null +++ b/Source/PerformanceFish/Hauling/StorageSettingsPatches.cs @@ -0,0 +1,232 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// #define StorageSettings_DEBUG + +using System.Diagnostics; +using System.Linq; +using FisheryLib.Pools; +using PerformanceFish.Events; + +namespace PerformanceFish.Hauling; + +public sealed class StorageSettingsPatches : ClassWithFishPatches +{ + public sealed class AllowedToAcceptPatch : FirstPriorityFishPatch + { + public override string Description { get; } + = "StorageSettings and capacity caching. High impact in colonies with multiple full storage buildings."; + + public override Expression TargetMethod { get; } + = static () => default(StorageSettings)!.AllowedToAccept(default(Thing)); + + protected internal override void OnPatchingCompleted() => ThingEvents.Initialized += TryRegister; + + public static void TryRegister(Thing thing) + { + var thingEvents = thing.Events(); + + if (thing is ISlotGroupParent) + thingEvents.Spawned += InitializeSlotGroupParent; + + if (!thing.IsItem()) + return; + + thingEvents.RegisteredAtThingGrid += TryNotifyReceivedThing; + thingEvents.DeregisteredAtThingGrid += TryNotifyLostThing; + } + + public static void TryNotifyReceivedThing(Thing thing, Map map, in IntVec3 cell) + { + if (cell.GetSlotGroupParent(map) is { } parent and Thing) + parent.GetStoreSettings()?.Cache().Notify_ReceivedThing(thing); + } + + public static void TryNotifyLostThing(Thing thing, Map map, in IntVec3 cell) + { + if (cell.GetSlotGroupParent(map) is { } parent and Thing) + parent.GetStoreSettings()?.Cache().Notify_LostThing(thing); + } + + public static void InitializeSlotGroupParent(Thing thing, Map map) + { + var slotGroupParent = (ISlotGroupParent)thing; + + if (slotGroupParent.GetStoreSettings() is not { } storeSettings) + return; + + ref var cache = ref storeSettings.Cache(); + cache.Initialize(thing); + + var slotCellsList = slotGroupParent.AllSlotCellsList(); + for (var i = slotCellsList.Count; i-- > 0;) + { + var thingsAtCell = slotCellsList[i].GetThingListUnchecked(map); + for (var j = thingsAtCell.Count; j-- > 0;) + { + var storedThing = thingsAtCell[j]; + if (!storedThing.IsItem() || cache.Contains(storedThing)) + continue; + + cache.Notify_ReceivedThing(storedThing); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Prefix(StorageSettings __instance, Thing t, ref bool __result, out bool __state) + { + var cachedAllowedToAcceptValue = __instance.Cache().AllowedToAccept.GetOrAdd(t.thingIDNumber); + + if (StorageSettingsCache.IsDirty(cachedAllowedToAcceptValue)) + return __state = true; + + __result = cachedAllowedToAcceptValue.AsBool() && CapacityAllows(__instance, t); + + return __state = false; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool CapacityAllows(StorageSettings __instance, Thing t) + { + if (__instance.owner is not Thing ownerThing) + return true; + + ref var cache = ref __instance.Cache(); + Debug.VerifyItemCount(ownerThing, ref cache); + + return cache.FreeSlots > 0 + || (t.TryGetMap() is { } map && t.Position.GetSlotGroupParent(map) == ownerThing) + || AcceptsForStacking(ref cache, t); + } + + public static bool AcceptsForStacking(ref StorageSettingsCache cache, Thing t) + => cache.StoredThingsOfDef(t.def).ContainsThingStackableWith(t); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Postfix(StorageSettings __instance, Thing t, bool __result, bool __state) + { + if (!__state) + return; + + UpdateCache(__instance, t, __result); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void UpdateCache(StorageSettings __instance, Thing t, bool __result) + => __instance.Cache().Update(t.thingIDNumber, __result && t is { def: not null, thingIDNumber: not -1 }); + } + + public sealed class TryNotifyChanged_Patch : FishPatch + { + public static event Action? Changed; + + public override string Description { get; } = "StorageSettings cache resetting"; + + public override Expression TargetMethod { get; } + = static () => default(StorageSettings)!.TryNotifyChanged(); + + public static void Postfix(StorageSettings __instance) + { + __instance.Cache().OnSettingsChanged(__instance); + Changed?.Invoke(__instance); + } + } + + public record struct StorageSettingsCache + { + public FishTable AllowedToAccept = new(); + + private Thing? _parent; + + public int FreeSlots = 0xFFFFFFF; + + private int _storedThingCount; + + private FishTable> _storedThingsByDef = new(); + + private CompProperties? _lwmProps; + + public event Action? SettingsChanged; + + public int TotalSlots => _parent!.TrueItemsPerCell(_lwmProps) * _parent!.SlotGroupCellCount(); + + public List StoredThingsOfDef(ThingDef def) => _storedThingsByDef.GetOrAdd(def.shortHash); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDirty(int allowedToAcceptValue) => allowedToAcceptValue == int.MaxValue; + + public void Update(int thingIDNumber, bool allowedToAccept) + => AllowedToAccept[thingIDNumber] = allowedToAccept.AsInt(); + + public void Initialize(Thing slotGroupParent) + { + _parent = slotGroupParent; + _storedThingsByDef.Clear(); + AllowedToAccept.Clear(); + _lwmProps = slotGroupParent.TryGetLwmCompProperties(); + FreeSlots = TotalSlots; + } + + public bool Contains(Thing thing) => StoredThingsOfDef(thing.def).Contains(thing); + + public void Notify_ReceivedThing(Thing thing) + { + if (_parent is null) + return; + + StoredThingsOfDef(thing.def).Add(thing); + FreeSlots = TotalSlots - ++_storedThingCount; + Debug.VerifyItemCount(_parent, ref this); + } + + public void Notify_LostThing(Thing thing) + { + if (_parent is null) + return; + + StoredThingsOfDef(thing.def).Remove(thing); + FreeSlots = TotalSlots - --_storedThingCount; + + Debug.VerifyItemCount(_parent, ref this); + } + + public void OnSettingsChanged(StorageSettings storageSettings) + { + AllowedToAccept.Clear(); + + if (_parent?.IsSpawned() ?? false) + FreeSlots = TotalSlots - _storedThingCount; + + SettingsChanged?.Invoke(storageSettings); + } + + public StorageSettingsCache() => AllowedToAccept.ValueInitializer = static _ => int.MaxValue; + } + + public static class Debug + { + [Conditional("StorageSettings_DEBUG")] + public static void LogMessage(PooledStringHandler stringHandler) => Log.Message(stringHandler); + + [Conditional("StorageSettings_DEBUG")] + public static void VerifyItemCount(Thing storageThing, ref StorageSettingsCache cache) + { + if (storageThing.TryGetMap() is not { } ownerMap || storageThing is not ISlotGroupParent slotGroup) + return; + + var realTotalSlots = cache.TotalSlots; + var realTotalItemCount = slotGroup.AllSlotCellsList().Sum(cell => cell.GetItemCount(ownerMap)); + if (cache.FreeSlots == realTotalSlots - realTotalItemCount) + return; + + Log.Error($"StorageSettings cache for '{storageThing}' has incorrect free slots count of '{ + cache.FreeSlots}'. Real total item count: '{realTotalItemCount}', real total slots: '{ + realTotalSlots}', real free slots: '{realTotalSlots - realTotalItemCount}'"); + + Current.Game.tickManager.Pause(); + } + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Hauling/StoreUtilityPrepatches.cs b/Source/PerformanceFish/Hauling/StoreUtilityPrepatches.cs new file mode 100644 index 0000000..267472b --- /dev/null +++ b/Source/PerformanceFish/Hauling/StoreUtilityPrepatches.cs @@ -0,0 +1,296 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Events; +using PerformanceFish.Listers; +using PerformanceFish.Prepatching; + +namespace PerformanceFish.Hauling; + +public sealed class StoreUtilityPrepatches : ClassWithFishPrepatches +{ + public sealed class NoStorageBlockersInPatch : FishPrepatch + { + public override string? Description { get; } + = "Optimizes this frequently accessed storage check by caching fully blocked cells in a bit array and " + + "reordering instructions to make better use of caches."; + + public override MethodBase TargetMethodBase { get; } = methodof(StoreUtility.NoStorageBlockersIn); + + protected internal override void OnPatchingCompleted() => ThingEvents.Initialized += TryRegister; + + public static void TryRegister(Thing thing) + { + if (!IsStorageBlocker(thing.def)) + return; + + var thingEvents = thing.Events(); + thingEvents.RegisteredAtThingGrid += SetStorageBlocker; + thingEvents.DeregisteredAtThingGrid += TryUnsetStorageBlocker; + } + + public static bool IsStorageBlocker(ThingDef thingDef) + => (thingDef.entityDefToBuild != null && thingDef.entityDefToBuild.passability != 0) + || (thingDef.passability != 0 + && thingDef.surfaceType == SurfaceType.None + && thingDef.category != ThingCategory.Item); + + public static void SetStorageBlocker(Thing thing, Map map, in IntVec3 cell) + => map.StorageBlockerGrid()[cell] = true; + + public static void TryUnsetStorageBlocker(Thing thing, Map map, in IntVec3 cell) + { + var thingsList = map.thingGrid.ThingsListAt(cell); + + for (var i = thingsList.Count; i-- > 0;) + { + var otherThing = thingsList[i]; + if (otherThing != thing && IsStorageBlocker(otherThing.def)) + return; + } + + map.StorageBlockerGrid()[cell] = false; + } + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static bool ReplacementBody(IntVec3 c, Map map, Thing thing) + { + if (map.StorageBlockerGrid()[c]) + return false; + + if (c.GetItemCount(map) < c.GetMaxItemsAllowedInCell(map)) + return true; + + var thingsAtCell = map.thingGrid.ThingsListAt(c); + var thingCount = thingsAtCell.Count; + for (var i = 0; i < thingCount; i++) + { + var thingAtCell = thingsAtCell[i]; + + if (thingAtCell.stackCount < thingAtCell.def.stackLimit + && thingAtCell.CanStackWith(thing) + && thingAtCell.def.EverStorable(false)) + { + return true; + } + } + + return false; + } + } + + public sealed class TryFindBestBetterStoreCellForWorkerPatch : FishPrepatch + { + public override List LinkedPatches { get; } = + [ + typeof(SlotGroupPrepatches.Notify_AddedCellPatch), typeof(SlotGroupPrepatches.Notify_LostCellPatch) + ]; + + public override string? Description { get; } + = "Optimizes hauling by splitting storage areas into smaller regions with cached capacity information. " + + "Additionally makes lookups more thorough when having the 'Improve hauling accuracy' setting enabled, " + + "raising the odds of getting closer cells as haul destinations"; + + public override MethodBase TargetMethodBase { get; } + = methodof(StoreUtility.TryFindBestBetterStoreCellForWorker); + + protected internal override void OnPatchingCompleted() => ThingEvents.Initialized += TryRegister; + + public static void TryRegister(Thing thing) + { + var thingEvents = thing.Events(); + + if (thing is ISlotGroupParent) + { + thingEvents.Spawned += NotifySlotGroupCellCountChanged; + thingEvents.DeSpawned += NotifySlotGroupCellCountChanged; + } + + if (!thing.IsItem()) + return; + + thingEvents.RegisteredAtThingGrid += TryNotifyReceivedThing; + thingEvents.DeregisteredAtThingGrid += TryNotifyLostThing; + } + + public static void NotifySlotGroupCellCountChanged(Thing slotGroupParent, Map map) + => NotifySlotGroupCellCountChanged(slotGroupParent); + + public static void NotifySlotGroupCellCountChanged(Thing slotGroupParent) + => slotGroupParent.GetSlotGroup()?.NotifyCellCountChanged(); + + public static void TryNotifyReceivedThing(Thing thing, Map map, in IntVec3 cell) + => map.StorageDistrictGrid()[cell]?.AddThing(thing); + + public static void TryNotifyLostThing(Thing thing, Map map, in IntVec3 cell) + => map.StorageDistrictGrid()[cell]?.RemoveThing(thing); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(Thing t, Pawn carrier, Map map, Faction faction, SlotGroup? slotGroup, + bool needAccurateResult, ref IntVec3 closestSlot, ref float closestDistSquared, + ref StoragePriority foundPriority) + { + if (slotGroup == null || !slotGroup.parent.Accepts(t)) + return; + + var thingPosition = t.SpawnedOrAnyParentSpawned ? t.PositionHeld : carrier.PositionHeld; + var slotGroupCells = slotGroup.CellsList; + + var maxValidCellsToCheck + = !FishSettings.ImproveHaulingAccuracy && needAccurateResult + ? Mathf.FloorToInt(slotGroupCells.Count * Rand.Range(0.005f, 0.018f)) + : 0; + + StartOfDistrictLoop: + var districts = slotGroup.Districts(); + + for (var districtIndex = 0; districtIndex < districts.Length; districtIndex++) + { + var district = districts[districtIndex]; + + if (!district.CapacityAllows(t)) + continue; + + var districtCells = districts.Length > 1 ? district.Cells : slotGroupCells; + + for (var i = 0; i < districtCells.Count; i++) + { + var slotGroupCell = districtCells[i]; + var cellDistance = (float)(thingPosition - slotGroupCell).LengthHorizontalSquared; + + if (cellDistance > closestDistSquared + || !StoreUtility.IsGoodStoreCell(slotGroupCell, map, t, carrier, faction)) + { + continue; + } + + if (districts.Length > 1 && slotGroupCell.GetSlotGroup(map) != district.Parent) + { + district.Parent.ResetAndRemakeDistricts(); // TODO: figure out why this happens + goto StartOfDistrictLoop; + } + + closestSlot = slotGroupCell; + closestDistSquared = cellDistance; + foundPriority = slotGroup.Settings.Priority; + if (!FishSettings.ImproveHaulingAccuracy && (districtIndex * i) + i >= maxValidCellsToCheck) + return; + } + } + } + } + + public sealed class TryFindBestBetterStoreCellForPatch : FishPrepatch + { + public override string? Description { get; } + = "Optimizes a loop in the method to make use of cached storage priorities for fewer comparisons against " + + "those. Also removes haulables from ListerHaulables if the found best store cell turns out to be the " + + "current cell"; + + public override MethodBase TargetMethodBase { get; } = methodof(StoreUtility.TryFindBestBetterStoreCellFor); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static bool ReplacementBody(Thing t, Pawn carrier, Map map, + StoragePriority currentPriority, Faction faction, out IntVec3 foundCell, bool needAccurateResult = true) + { + var haulDestinationManager = map.haulDestinationManager; + List listInPriorityOrder = haulDestinationManager.AllGroupsListInPriorityOrder; + if (listInPriorityOrder.Count == 0) + { + foundCell = IntVec3.Invalid; + return false; + } + + var foundPriority = currentPriority; + var closestDistSquared = (float)int.MaxValue; + var closestSlot = IntVec3.Invalid; + + var groupCountByPriority = GetCachedGroupCountByPriority(haulDestinationManager); + var i = 0; + for (var priorityGroupIndex = groupCountByPriority.Length; priorityGroupIndex-- > 0;) + { + var groupsOfPriorityCount = groupCountByPriority[priorityGroupIndex]; + + if ((StoragePriority)priorityGroupIndex < foundPriority) // succeeded finding a higher priority storage + break; + + if ((StoragePriority)priorityGroupIndex <= currentPriority) // current storage is highest valid priority + { + TryRemoveFromListerHaulables(t, currentPriority); + break; + } + + while (groupsOfPriorityCount-- > 0) + { + StoreUtility.TryFindBestBetterStoreCellForWorker(t, carrier, map, faction, listInPriorityOrder[i++], + needAccurateResult, ref closestSlot, ref closestDistSquared, ref foundPriority); + } + } + + if (!closestSlot.IsValid) + { + foundCell = IntVec3.Invalid; + return false; + } + + foundCell = closestSlot; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int[] GetCachedGroupCountByPriority(HaulDestinationManager haulDestinationManager) + => haulDestinationManager.Cache().GroupCountByPriority; + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void TryRemoveFromListerHaulables(Thing t, StoragePriority currentPriority) + { + if (t.IsInAnyStorage() /*t.IsInValidStorage()*/ && t.TryGetMapHeld() is { } map) + map.listerHaulables.TryRemoveDirectly(t); + + // remove for any storage instead of only valid storage to prevent further haul attempts until the next + // automatic ListerHaulablesTick cycle + } + } + + public sealed class GetSlotGroupPatch : FishPrepatch + { + public override string? Description { get; } + = "Optimization for this method by reducing its amount of instructions without changing results"; + + public override MethodBase TargetMethodBase { get; } + = methodof((Func)StoreUtility.GetSlotGroup); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static SlotGroup? ReplacementBody(Thing thing) + => thing.TryGetMap() is { } map ? thing.Position.GetSlotGroup(map) : null; + } + + public sealed class CurrentHaulDestinationOfPatch : FishPrepatch + { + public override string? Description { get; } + = "Optimization for this method by reducing its amount of instructions without changing results"; + + public override MethodBase TargetMethodBase { get; } + = methodof(StoreUtility.CurrentHaulDestinationOf); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static IHaulDestination? ReplacementBody(Thing t) + => t.TryGetMap() is { } map + ? map.haulDestinationManager.SlotGroupParentAt(t.Position) + : t.ParentHolder as IHaulDestination; + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Hediffs/HealthTrackerOptimization.cs b/Source/PerformanceFish/Hediffs/HealthTrackerOptimization.cs index e01f046..7ef0f27 100644 --- a/Source/PerformanceFish/Hediffs/HealthTrackerOptimization.cs +++ b/Source/PerformanceFish/Hediffs/HealthTrackerOptimization.cs @@ -8,7 +8,7 @@ using PerformanceFish.Prepatching; namespace PerformanceFish.Hediffs; -public class HealthTrackerOptimization : ClassWithFishPrepatches +public sealed class HealthTrackerOptimization : ClassWithFishPrepatches { public static void Initialize() { @@ -24,7 +24,7 @@ public class HealthTrackerOptimization : ClassWithFishPrepatches } } - public class HealthTickPatch : FishPrepatch + public sealed class HealthTickPatch : FishPrepatch { public override MethodBase TargetMethodBase { get; } = AccessTools.Method(typeof(Pawn_HealthTracker), nameof(Pawn_HealthTracker.HealthTick)); diff --git a/Source/PerformanceFish/Hediffs/HediffComp_PsychicHarmonizer_Bugfix.cs b/Source/PerformanceFish/Hediffs/HediffComp_PsychicHarmonizer_Bugfix.cs index 5c7d2d9..952e935 100644 --- a/Source/PerformanceFish/Hediffs/HediffComp_PsychicHarmonizer_Bugfix.cs +++ b/Source/PerformanceFish/Hediffs/HediffComp_PsychicHarmonizer_Bugfix.cs @@ -3,18 +3,17 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -using System.Linq; using static System.Reflection.Emit.OpCodes; namespace PerformanceFish.Hediffs; -public class HediffComp_PsychicHarmonizer_Bugfix : ClassWithFishPatches +public sealed class HediffComp_PsychicHarmonizer_Bugfix : ClassWithFishPatches { - public class CompPostTick_Patch : FishPatch + public sealed class CompPostTick_Patch : FishPatch { public override string Description { get; } = "Fixes a bug in the psychic harmonizer tick method that was causing it to recalculate results on 149 " - + "out of 150 ticks instead of once every 150 ticks. Large impact."; + + "out of 150 ticks instead of once every 150 ticks. Large impact."; public override MethodBase TargetMethodInfo { get; } = AccessTools.Method(typeof(HediffComp_PsychicHarmonizer), @@ -22,29 +21,19 @@ public class HediffComp_PsychicHarmonizer_Bugfix : ClassWithFishPatches public static CodeInstructions Transpiler(CodeInstructions codeInstructions) { - var codes = codeInstructions.ToList(); + var codes = codeInstructions.AsOrToList(); var pawn_int_IsHashIntervalTick = FishTranspiler.Call>(Gen.IsHashIntervalTick); var success = false; + for (var i = 0; i < codes.Count; i++) { - if (i + 2 < codes.Count - && codes[i].opcode == Ldc_I4 - && codes[i + 1] == pawn_int_IsHashIntervalTick - && codes[i + 2].opcode == Brfalse_S) + if (i - 2 >= 0 + && codes[i - 2].opcode == Ldc_I4 + && codes[i - 1] == pawn_int_IsHashIntervalTick + && codes[i].opcode == Brfalse_S) { - //codes[i].operand = 128; - yield return codes[i]; - yield return codes[i + 1]; - - codes[i + 2].opcode = Brtrue_S; - yield return codes[i + 2]; - + codes[i].opcode = Brtrue_S; success = true; - i += 2; - } - else - { - yield return codes[i]; } } @@ -55,6 +44,8 @@ public class HediffComp_PsychicHarmonizer_Bugfix : ClassWithFishPatches + "an actual failure to patch if settings were not toggled however, which would mean some other " + "patch or a vanilla change either messes with this or implements the same thing"); } + + return codes; } } } \ No newline at end of file diff --git a/Source/PerformanceFish/Hediffs/HediffSetCaching.cs b/Source/PerformanceFish/Hediffs/HediffSetCaching.cs index 873ba2f..ce06620 100644 --- a/Source/PerformanceFish/Hediffs/HediffSetCaching.cs +++ b/Source/PerformanceFish/Hediffs/HediffSetCaching.cs @@ -18,9 +18,9 @@ using NotMissingPartsCache namespace PerformanceFish.Hediffs; -public class HediffSetPrecaching : ClassWithFishPrepatches +public sealed class HediffSetPrecaching : ClassWithFishPrepatches { - public class GetFirstHediffOfDef : FishPrepatch + public sealed class GetFirstHediffOfDef : FishPrepatch { public override string Description { get; } = "Caches results of the HediffSet.GetFirstHediffOfDef method. Impact scales with hediff count."; @@ -94,7 +94,7 @@ public class HediffSetPrecaching : ClassWithFishPrepatches } } - public class HasHediff : FishPrepatch + public sealed class HasHediff : FishPrepatch { public override string Description { get; } = "Caches results of the HediffSet.HasHediff method. Impact scales with hediff count."; @@ -111,7 +111,7 @@ public class HediffSetPrecaching : ClassWithFishPrepatches => __instance.GetFirstHediffOfDef(def, mustBeVisible) != null; } - public class GetNotMissingParts : FishPrepatch + public sealed class GetNotMissingParts : FishPrepatch { public override string? Description { get; } = "Caches results of the HediffSet.GetNotMissingParts method. Impact scales with hediff count."; @@ -156,9 +156,9 @@ public class HediffSetPrecaching : ClassWithFishPrepatches } } -public class HediffSetCaching : ClassWithFishPatches +public sealed class HediffSetCaching : ClassWithFishPatches { - public class DirtyCache : FishPatch + public sealed class DirtyCache : FishPatch { public override string Description { get; } = "Patched to trigger psylink cache clearing for the psychic entropy optimization."; @@ -176,9 +176,9 @@ public class HediffSetCaching : ClassWithFishPatches public record struct NotMissingPartsCacheValue { private int _version = -1; - private List _hediffsInSet = new(); + private List _hediffsInSet = []; private int _nextRefreshTick; - public List Parts = new(); + public List Parts = []; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(HediffSet set, ref IEnumerable result) @@ -209,7 +209,7 @@ public class HediffSetCaching : ClassWithFishPatches public record struct HediffCacheValue { private int _version = -1; - private List _hediffsInSet = new(); + private List _hediffsInSet = []; private int _nextRefreshTick; public Hediff? Hediff; diff --git a/Source/PerformanceFish/Hediffs/HediffWithCompsOptimization.cs b/Source/PerformanceFish/Hediffs/HediffWithCompsOptimization.cs new file mode 100644 index 0000000..815c3fd --- /dev/null +++ b/Source/PerformanceFish/Hediffs/HediffWithCompsOptimization.cs @@ -0,0 +1,99 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#if useless + +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; + +namespace PerformanceFish.Hediffs; + +public sealed class HediffWithCompsOptimization : ClassWithFishPrepatches +{ + public sealed class ShouldRemovePatch : FishPrepatch + { + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredPropertyGetter(typeof(HediffWithComps), nameof(HediffWithComps.ShouldRemove)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static unsafe bool ReplacementBody(HediffWithComps instance) + { + if (instance.comps is { } comps) + { + for (var i = 0; i < comps.Count; i++) + { + if (comps[i].CompShouldRemove) + return true; + } + } + + return BaseShouldRemove(instance); + } + + public static unsafe delegate* BaseShouldRemove + = (delegate*)AccessTools + .DeclaredPropertyGetter(typeof(Hediff), nameof(Hediff.ShouldRemove)).GetFunctionPointer(); + } + + public sealed class VisiblePatch : FishPrepatch + { + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredPropertyGetter(typeof(HediffWithComps), nameof(HediffWithComps.Visible)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static unsafe bool ReplacementBody(HediffWithComps instance) + { + if (instance.comps is { } comps) + { + for (var i = 0; i < comps.Count; i++) + { + if (comps[i].CompDisallowVisible()) + return false; + } + } + + return BaseVisible(instance); + } + + public static unsafe delegate* BaseVisible + = (delegate*)AccessTools + .DeclaredPropertyGetter(typeof(Hediff), nameof(Hediff.Visible)).GetFunctionPointer(); + } + + public sealed class PostTickPatch : FishPrepatch + { + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(HediffWithComps), nameof(HediffWithComps.PostTick)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static unsafe void ReplacementBody(HediffWithComps instance) + { + BasePostTick(instance); + + if (instance.comps is not { } comps) + return; + + var severityAdjustment = 0f; + for (var i = 0; i < comps.Count; i++) + comps[i].CompPostTick(ref severityAdjustment); + + if (severityAdjustment != 0f) + instance.Severity += severityAdjustment; + } + + public static unsafe delegate* BasePostTick + = (delegate*)AccessTools.DeclaredMethod(typeof(Hediff), nameof(Hediff.PostTick)) + .GetFunctionPointer(); + } +} + +#endif \ No newline at end of file diff --git a/Source/PerformanceFish/Hediffs/ImmunityHandlerCaching.cs b/Source/PerformanceFish/Hediffs/ImmunityHandlerCaching.cs index dc24188..2cb4a84 100644 --- a/Source/PerformanceFish/Hediffs/ImmunityHandlerCaching.cs +++ b/Source/PerformanceFish/Hediffs/ImmunityHandlerCaching.cs @@ -12,9 +12,9 @@ using ImmunityRecordExistsCache namespace PerformanceFish.Hediffs; -public class ImmunityHandlerCaching : ClassWithFishPatches +public sealed class ImmunityHandlerCaching : ClassWithFishPatches { - public class NeededImmunitiesNow : FirstPriorityFishPatch + public sealed class NeededImmunitiesNow : FirstPriorityFishPatch { public override string Description { get; } = "Caches results of the ImmunityHandler.NeededImmunitiesNow method. Impact scales with hediff quantity."; @@ -54,7 +54,7 @@ public class ImmunityHandlerCaching : ClassWithFishPatches .Update(__instance.pawn.health.hediffSet, __result); } - public class ImmunityRecordExists : FishPatch + public sealed class ImmunityRecordExists : FishPatch { public override string? Description { get; } = "Required by the TryAddImmunityRecord optimization."; @@ -82,7 +82,7 @@ public class ImmunityHandlerCaching : ClassWithFishPatches } } - public class TryAddImmunityRecord : FishPatch + public sealed class TryAddImmunityRecord : FishPatch { public override string? Description { get; } = "Optimization for this specific method to not run more checks than necessary."; @@ -113,9 +113,9 @@ public class ImmunityHandlerCaching : ClassWithFishPatches public record struct ImmunityInfoCacheValue { private int _version = -1; - private List _hediffsInSet = new(); + private List _hediffsInSet = []; private int _nextRefreshTick; - public List Infos = new(); + public List Infos = []; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(HediffSet set, List infos) diff --git a/Source/PerformanceFish/Hediffs/Pawn_PsychicEntropyTrackerOptimization.cs b/Source/PerformanceFish/Hediffs/Pawn_PsychicEntropyTrackerOptimization.cs index dfe1d6f..7fa52ea 100644 --- a/Source/PerformanceFish/Hediffs/Pawn_PsychicEntropyTrackerOptimization.cs +++ b/Source/PerformanceFish/Hediffs/Pawn_PsychicEntropyTrackerOptimization.cs @@ -5,9 +5,9 @@ namespace PerformanceFish.Hediffs; -public class Pawn_PsychicEntropyTrackerOptimization : ClassWithFishPatches +public sealed class Pawn_PsychicEntropyTrackerOptimization : ClassWithFishPatches { - public class Psylink_Patch : FirstPriorityFishPatch + public sealed class Psylink_Patch : FirstPriorityFishPatch { public override string Description { get; } = "Caches/throttles the psylink method to only update every 32 ticks."; diff --git a/Source/PerformanceFish/IdeoBuildingPresenceDemandOptimization.cs b/Source/PerformanceFish/IdeoBuildingPresenceDemandOptimization.cs new file mode 100644 index 0000000..8a9b256 --- /dev/null +++ b/Source/PerformanceFish/IdeoBuildingPresenceDemandOptimization.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; + +namespace PerformanceFish; + +public sealed class IdeoBuildingPresenceDemandOptimization : ClassWithFishPrepatches +{ + public sealed class BuildingPresentPatch : FishPrepatch + { + public override string? Description { get; } + = "Optimization to use cached ListerBuildings data instead of scanning through all colonist buildings, " + + "frequently"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(IdeoBuildingPresenceDemand), + nameof(IdeoBuildingPresenceDemand.BuildingPresent)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static bool ReplacementBody(IdeoBuildingPresenceDemand __instance, Map map) + { + var buildings = map.listerBuildings.AllBuildingsColonistOfDef(__instance.parent.ThingDef); + + if (buildings is IndexedFishSet fishSet) + { + var list = fishSet.ReadOnlyList; + for (var i = list.Count; i-- > 0;) + { + if (list[i].StyleSourcePrecept == __instance.parent) + return true; + } + + return false; + } + else + { + return FallbackCheck(__instance, buildings); + } + } + + public static bool FallbackCheck(IdeoBuildingPresenceDemand __instance, IEnumerable buildings) + { + foreach (var building in buildings) + { + if (building.StyleSourcePrecept == __instance.parent) + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/IdeoUtilityCaching.cs b/Source/PerformanceFish/IdeoUtilityCaching.cs index 109f81e..79f1b31 100644 --- a/Source/PerformanceFish/IdeoUtilityCaching.cs +++ b/Source/PerformanceFish/IdeoUtilityCaching.cs @@ -10,9 +10,9 @@ using StyleDominanceCache namespace PerformanceFish; -public class IdeoUtilityCaching : ClassWithFishPrepatches +public sealed class IdeoUtilityCaching : ClassWithFishPrepatches { - public class GetStyleDominance_Patch : FishPrepatch + public sealed class GetStyleDominance_Patch : FishPrepatch { public override string Description { get; } = "Caches IdeoUtility.GetStyleDominance to only update about once every 900 ticks or so."; diff --git a/Source/PerformanceFish/JobSystem/DisabledWorkTypesOptimization.cs b/Source/PerformanceFish/JobSystem/DisabledWorkTypesOptimization.cs index 6fdd3e9..e3e3cea 100644 --- a/Source/PerformanceFish/JobSystem/DisabledWorkTypesOptimization.cs +++ b/Source/PerformanceFish/JobSystem/DisabledWorkTypesOptimization.cs @@ -3,12 +3,16 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +using PerformanceFish.ModCompatibility; + namespace PerformanceFish.JobSystem; -public class DisabledWorkTypesOptimization : ClassWithFishPatches +public sealed class DisabledWorkTypesOptimization : ClassWithFishPatches { - public class PawnGetDisabledWorkTypes : FishPatch + public sealed class PawnGetDisabledWorkTypes : FishPatch { + public override List IncompatibleModIDs { get; } = [PackageIDs.MULTIPLAYER]; + public override string? Description { get; } = "Essentially fixes a bug in Pawn.GetDisabledWorkTypes that causes the method to discard its cache and " + "constantly keep recalculating. With the patch applied it'll return early when a valid cache exists."; diff --git a/Source/PerformanceFish/JobSystem/GenClosestPatches.cs b/Source/PerformanceFish/JobSystem/GenClosestPatches.cs index e0b909b..e64f722 100644 --- a/Source/PerformanceFish/JobSystem/GenClosestPatches.cs +++ b/Source/PerformanceFish/JobSystem/GenClosestPatches.cs @@ -9,9 +9,9 @@ using Verse.AI; namespace PerformanceFish.JobSystem; -public class GenClosestPatches : ClassWithFishPatches +public sealed class GenClosestPatches : ClassWithFishPatches { - public class ClosestThing_Global_Patch : FishPatch + public sealed class ClosestThing_Global_Patch : FishPatch { public override string Description { get; } = "Threading for job scanning. Disabled and unstable."; public override bool DefaultState => false; @@ -226,29 +226,4 @@ public class GenClosestPatches : ClassWithFishPatches } } #endif - -#if threadingEnabled - public class CanReach_Patch : FishPatch - { - protected CanReach_Patch() => FishSettings.ThreadingPatches.Add(this); - public override bool DefaultState => false; - public override string Description { get; } - = "Lock for threading. Only needed with threading enabled and off by default"; - public override Expression TargetMethod { get; } - = static () => default(Reachability)!.CanReach(default, null, default, default); - public static void Prefix() - { - if (FishSettings.ThreadingEnabled) - Monitor.Enter(Lock); - } - - public static Exception Finalizer(Exception __exception) - { - if (FishSettings.ThreadingEnabled) - Monitor.Exit(Lock); - return __exception; - } - private static object Lock { get; } = new(); - } -#endif //} \ No newline at end of file diff --git a/Source/PerformanceFish/JobSystem/JobGiver_HaulOptimization.cs b/Source/PerformanceFish/JobSystem/JobGiver_HaulOptimization.cs index be476da..202747e 100644 --- a/Source/PerformanceFish/JobSystem/JobGiver_HaulOptimization.cs +++ b/Source/PerformanceFish/JobSystem/JobGiver_HaulOptimization.cs @@ -7,22 +7,21 @@ using PerformanceFish.ModCompatibility; namespace PerformanceFish.JobSystem; -public class JobGiver_HaulOptimization : ClassWithFishPatches +public sealed class JobGiver_HaulOptimization : ClassWithFishPatches { - public class PotentialWorkThingsGlobal_Patch : FishPatch + public sealed class PotentialWorkThingsGlobal_Patch : FishPatch { + public override List IncompatibleModIDs { get; } = [PackageIDs.MULTIPLAYER]; + public override string Description { get; } = "Sorts haulables by distance before running expensive hauling calculations on them to avoid checks on " - + "far away items"; + + "far away items"; public override MethodBase TargetMethodInfo { get; } = AccessTools.Method(typeof(JobGiver_Haul), nameof(JobGiver_Haul.TryGiveJob)); public static CodeInstructions? Transpiler(CodeInstructions codes, MethodBase method) { - if (ActiveMods.Multiplayer) // PUAH has known issues with sorting and multiplayer - return codes; - var pawn_Map_listerHaulables_ThingsPotentiallyNeedingHauling = FishTranspiler.Call(static () => default(ListerHaulables)!.ThingsPotentiallyNeedingHauling()); diff --git a/Source/PerformanceFish/JobSystem/JobGiver_WorkPatches.cs b/Source/PerformanceFish/JobSystem/JobGiver_WorkPatches.cs index 19f14e3..8ef86cc 100644 --- a/Source/PerformanceFish/JobSystem/JobGiver_WorkPatches.cs +++ b/Source/PerformanceFish/JobSystem/JobGiver_WorkPatches.cs @@ -13,9 +13,9 @@ using static PerformanceFish.TranspilerHelpers; namespace PerformanceFish.JobSystem; -public class JobGiver_WorkPatches : ClassWithFishPatches +public sealed class JobGiver_WorkPatches : ClassWithFishPatches { - public class TryIssueJobPackage_InnerDelegate_Patch : FishPatch + public sealed class TryIssueJobPackage_InnerDelegate_Patch : FishPatch { public override string Description => "Work scanning optimization. Splits the IsForbidden and HasJobOnThing checks into separate loops, then threads the IsForbidden loop, sorts it, caches the result and skips any following duplicate checks"; public override MethodBase TargetMethodInfo => AccessTools.FindIncludingInnerTypes(typeof(JobGiver_Work), @@ -44,7 +44,7 @@ public class JobGiver_WorkPatches : ClassWithFishPatches } } - public class TryIssueJobPackage_Patch : FishPatch + public sealed class TryIssueJobPackage_Patch : FishPatch { public override string Description => "Work scanning optimization. Splits the IsForbidden and HasJobOnThing checks into separate loops, then threads the IsForbidden loop, sorts it, caches the result and skips any following duplicate checks"; public override MethodBase TargetMethodInfo => AccessTools.Method(typeof(JobGiver_Work), nameof(JobGiver_Work.TryIssueJobPackage)); @@ -243,7 +243,7 @@ public class JobGiver_WorkPatches : ClassWithFishPatches public static Dictionary, IEnumerable> PrefilteredPotentialWorkThingsGlobal { get; } = new(); } - public class ThingRequest_IsUndefined_Patch : FishPatch + public sealed class ThingRequest_IsUndefined_Patch : FishPatch { public override string Description => "Part of the work scanning optimization"; public override MethodBase TargetMethodInfo => AccessTools.PropertyGetter(typeof(ThingRequest), nameof(ThingRequest.IsUndefined)); @@ -259,7 +259,7 @@ public class JobGiver_WorkPatches : ClassWithFishPatches => __instance.group == ThingRequestGroup.Undefined && !((FishCache)__instance.singleDef).IsSingleDef; } - public class ThingRequest_CanBeFoundInRegion_Patch : FirstPriorityFishPatch + public sealed class ThingRequest_CanBeFoundInRegion_Patch : FirstPriorityFishPatch { public override string Description => "Part of the work scanning optimization"; public override MethodBase TargetMethodInfo => AccessTools.PropertyGetter(typeof(ThingRequest), nameof(ThingRequest.CanBeFoundInRegion)); @@ -276,7 +276,7 @@ public class JobGiver_WorkPatches : ClassWithFishPatches || (__instance.group != ThingRequestGroup.Undefined && (__instance.group == ThingRequestGroup.Nothing || __instance.group.StoreInRegion())); } - public class ListerThings_ThingsMatching_Patch : FirstPriorityFishPatch + public sealed class ListerThings_ThingsMatching_Patch : FirstPriorityFishPatch { public override string Description => "Part of the work scanning optimization"; public override Expression TargetMethod => () => new ListerThings(default).ThingsMatching(default); diff --git a/Source/PerformanceFish/JobSystem/WorkGiver_DoBillOptimization.cs b/Source/PerformanceFish/JobSystem/WorkGiver_DoBillOptimization.cs index 50a63a5..9ee418f 100644 --- a/Source/PerformanceFish/JobSystem/WorkGiver_DoBillOptimization.cs +++ b/Source/PerformanceFish/JobSystem/WorkGiver_DoBillOptimization.cs @@ -5,44 +5,55 @@ using System.Linq; using System.Reflection.Emit; +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; using Verse.AI; using IngredientCacheList = PerformanceFish.Utility.KeyedList Defs, bool Found)>; +using OpCodes = System.Reflection.Emit.OpCodes; using RecipeIngredientCache = PerformanceFish.Cache.ByIndex; namespace PerformanceFish.JobSystem; -public class WorkGiver_DoBillOptimization : ClassWithFishPatches +public sealed class WorkGiver_DoBillPrepatches : ClassWithFishPrepatches { - public static List BlackListedModExtensions - => _blackListedModExtensions ??= new() { Reflection.Type("VFEAncients", "VFEAncients.RecipeExtension_Mend") }; + public static List BlackListedModExtensions + => _blackListedModExtensions ??= InitializeBlackListedModExtensions(); - private static List? _blackListedModExtensions; - - public class TryFindBestIngredientsHelper_InnerDelegate_Patch : FirstPriorityFishPatch + [MethodImpl(MethodImplOptions.NoInlining)] + private static List InitializeBlackListedModExtensions() { + var list = new List(); + + if (ModCompatibility.Types.VFEAncients.RecipeExtensionMend is { } VFEAncientsRecipeExtension) + list.Add(VFEAncientsRecipeExtension); + + return list; + } + + private static List? _blackListedModExtensions; + + public sealed class TryFindBestIngredientsHelper_InnerDelegate_Patch : FishPrepatch + { + public override List IncompatibleModIDs { get; } + = [ModCompatibility.PackageIDs.USE_BEST_MATERIALS]; + + public override List LinkedPatches { get; } = + [ + typeof(WorkGiver_DoBillOptimization.TryFindBestIngredientsHelper_Patch), + typeof(WorkGiver_DoBillOptimization.TryFindBestIngredientsInSet_NoMixHelper_Patch) + ]; + public override string Description { get; } = "Optimizes WorkGiver_DoBill to only check things that get accepted by a bill's filter for its " + "ingredient lookup, instead of looping over everything and then checking filters afterwards. " + "Also caches these filter acceptances. Large performance impact."; - public override bool Enabled - { - set - { - if (base.Enabled == value) - return; - - Get().Enabled - = Get().Enabled - = base.Enabled = value; - } - } - - public override MethodBase TargetMethodInfo { get; } + public override MethodBase TargetMethodBase { get; } = AccessTools.FirstMethod(DelegateInstanceType, static m => m.GetParameters() is { Length: 1 } parameterInfos && parameterInfos[0].ParameterType == typeof(Region) @@ -52,12 +63,15 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches && instruction.operand is MethodInfo info && info == AccessTools.Method(typeof(ListerThings), nameof(ListerThings.ThingsMatching)))); - public static CodeInstructions Transpiler(CodeInstructions codeInstructions, MethodBase method) - => Reflection.MakeReplacementCall(AccessTools.Method( - typeof(TryFindBestIngredientsHelper_InnerDelegate_Patch), nameof(Replacement), - generics: new[] { DelegateInstanceType })); + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); - public static bool Replacement(T __instance, Region r) where T : class + // public static CodeInstructions Transpiler(CodeInstructions codeInstructions, MethodBase method) + // => Reflection.MakeReplacementCall(AccessTools.Method( + // typeof(TryFindBestIngredientsHelper_InnerDelegate_Patch), nameof(ReplacementBody), + // generics: new[] { DelegateInstanceType })); + + public static bool ReplacementBody(object __instance, Region r) { var instance = _delegateInstance.Get(__instance); var thingValidator = _thingValidatorInstance.Get(instance.ThingValidator.Target); @@ -75,7 +89,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches #if V1_4 || bill.billStack?.billGiver is Building_MechGestator #endif - || (BlackListedModExtensions.Exists(static b => b != null) + || (BlackListedModExtensions.Count > 0 && IsBlackListed(bill))) // <-- Fuck these too { list = GetDefaultList(r); @@ -89,7 +103,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches cache.Update(bill); } - list = GetList(r.listerThings.listsByDef, cache.thingDefs); + list = GetList(r, cache.thingDefs); } } @@ -110,10 +124,9 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches instance.FoundAll = true; return true; - } - private static bool HasNoFilters(Bill bill) + public static bool HasNoFilters(Bill bill) => bill.ingredientFilter is null || bill.recipe.fixedIngredientFilter is null; @@ -135,13 +148,13 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches return false; }*/ - private static bool IsBlackListed(Bill? bill) + public static bool IsBlackListed(Bill? bill) => bill is null || bill.recipe.modExtensions.ExistsAndNotNull(static e => BlackListedModExtensions.Contains(e.GetType())); - private static List GetDefaultList(Region r) - => r.ListerThings.ThingsMatching(ThingRequest.ForGroup(ThingRequestGroup.HaulableEver)); + public static List GetDefaultList(Region r) + => r.listerThings.ThingsMatching(ThingRequest.ForGroup(ThingRequestGroup.HaulableEver)); public static void InsertAtCorrectPosition(List list, List thingsToInsert, Pawn pawn) { @@ -159,7 +172,8 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches } } - private static readonly ThingPositionComparer _insertAtCorrectPositionComparer = new(); + private static readonly WorkGiver_DoBillOptimization.ThingPositionComparer _insertAtCorrectPositionComparer + = new(); public static void ActualLoop(List list, Region r, Pawn pawn, Predicate baseValidator, bool billGiverIsPawn) @@ -182,8 +196,10 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches } } - public static List GetList(Dictionary> listsByDef, IngredientCacheList cacheList) + public static List GetList(Region region, IngredientCacheList cacheList) { + var listsByDef = region.listerThings.listsByDef; + var list = _tempListForIngredients; list.Clear(); var set = _tempSetForIngredientDefs; @@ -216,7 +232,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches { var recipe = bill.recipe; var ingredients = recipe.ingredients; - var listsByDef = r.Map.listerThings.listsByDef; + var listsByDef = r.GetMap().listerThings.listsByDef; var result = cacheList ?? new(3); for (var i = 0; i < ingredients.Count; @@ -229,7 +245,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches } else { - result[ingredients[i]] = ingredientList = new(new(), false); + result[ingredients[i]] = ingredientList = new([], false); } var filter = ingredients[i].filter; @@ -291,8 +307,8 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches (mediumFilter, largestFilter) = (largestFilter, mediumFilter); } - private static readonly List _tempListForIngredients = new(); - private static readonly HashSet _tempSetForIngredientDefs = new(); + private static readonly List _tempListForIngredients = []; + private static readonly HashSet _tempSetForIngredientDefs = []; } private static Type DelegateInstanceType { get; } @@ -308,8 +324,8 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches && AccessTools.Field(t, "foundAllIngredientsAndChoose") != null && AccessTools.Field(t, "foundAll") != null && AccessTools.Field(t, "thingValidator") != null) - ?? throw new("Performance Fish failed to find the target type for its WorkGiver_DoBill patch. " - + "This means that specific patch won't apply."); + ?? ThrowHelper.ThrowInvalidOperationException("Performance Fish failed to find the target type " + + "for its WorkGiver_DoBill patch. This means that specific patch won't apply."); private static Type ThingValidatorInstanceType { get; } = AccessTools.FirstInner(typeof(WorkGiver_DoBill), static t @@ -320,20 +336,20 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches //&& AccessTools.Field(t, "chosen") != null //&& AccessTools.Field(t, "billGiver") != null /*&& AccessTools.Field(t, "pawn") != null*/) - ?? throw new("Performance Fish failed to find the thingValidator type for its WorkGiver_DoBill patch. " - + "This likely flat out breaks the entire patch."); + ?? ThrowHelper.ThrowInvalidOperationException("Performance Fish failed to find the thingValidator " + + "type for its WorkGiver_DoBill patch. This likely flat out breaks the entire patch."); // ReSharper disable once InconsistentNaming - private static DelegateInstance _delegateInstance { get; } + public static DelegateInstance _delegateInstance { get; } = (DelegateInstance)Activator.CreateInstance( typeof(DelegateInstanceImplementation<>).MakeGenericType(DelegateInstanceType)); // ReSharper disable once InconsistentNaming - private static ThingValidatorInstance _thingValidatorInstance { get; } + public static ThingValidatorInstance _thingValidatorInstance { get; } = (ThingValidatorInstance)Activator.CreateInstance( typeof(ThingValidatorInstanceImplementation<>).MakeGenericType(ThingValidatorInstanceType)); - private abstract class DelegateInstance + public abstract class DelegateInstance { public abstract DelegateInstance Get(object obj); @@ -347,7 +363,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches public abstract Predicate ThingValidator { get; set; } } - private class DelegateInstanceImplementation : DelegateInstance where T : class + private sealed class DelegateInstanceImplementation : DelegateInstance where T : class { #pragma warning disable CS8618 private T _instance; @@ -430,7 +446,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches = AccessTools.FieldRefAccess>("thingValidator"); } - private abstract class ThingValidatorInstance + public abstract class ThingValidatorInstance { public abstract ThingValidatorInstance? Get(object obj); @@ -438,7 +454,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches //public abstract Thing BillGiver { get; set; } } - private class ThingValidatorInstanceImplementation : ThingValidatorInstance where T : class + private sealed class ThingValidatorInstanceImplementation : ThingValidatorInstance where T : class { #pragma warning disable CS8618 private T _instance; @@ -467,27 +483,21 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches private static AccessTools.FieldRef _bill = AccessTools.FieldRefAccess("bill"); //private static AccessTools.FieldRef _billGiver = AccessTools.FieldRefAccess("billGiver"); } +} - public class TryFindBestIngredientsHelper_Patch : FishPatch +public sealed class WorkGiver_DoBillOptimization : ClassWithFishPatches +{ + public sealed class TryFindBestIngredientsHelper_Patch : FishPatch { + public override bool ShowSettings => false; + public override string Description { get; } = "Part of the DoBill optimization"; - public override bool Enabled - { - set - { - if (base.Enabled == value) - return; - - Get().Enabled = base.Enabled = value; - } - } - public override Delegate TargetMethodGroup { get; } = WorkGiver_DoBill.TryFindBestIngredientsHelper; public static void Postfix(Predicate thingValidator) { - var thingValidatorInstance = _thingValidatorInstance.Get(thingValidator.Target); + var thingValidatorInstance = WorkGiver_DoBillPrepatches._thingValidatorInstance.Get(thingValidator.Target); if (thingValidatorInstance is null) return; @@ -505,8 +515,10 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches } } - public class TryFindBestIngredientsInSet_NoMixHelper_Patch : FishPatch + public sealed class TryFindBestIngredientsInSet_NoMixHelper_Patch : FishPatch { + public override List LinkedPatches { get; } = [typeof(TryFindBestIngredientsHelper_Patch)]; + public override string Description { get; } = "Part of the DoBill optimization. Bills that need multiple ingredients, like for example steel and " + "components for guns, normally scan their surroundings for all those ingredients and count them, until " @@ -515,17 +527,6 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches + "case of steel and components that could mean not having to go over 100s of steel stacks at times. " + "Prevents large spikes from these situations, does nothing for all other bills."; - public override bool Enabled - { - set - { - if (base.Enabled == value) - return; - - Get().Enabled = base.Enabled = value; - } - } - public override Delegate TargetMethodGroup { get; } = WorkGiver_DoBill.TryFindBestIngredientsInSet_NoMixHelper; public static void Prefix(ref bool alreadySorted, Bill? bill) @@ -549,16 +550,15 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches && codes[i + 1] == FishTranspiler.Constant(1) // i++; && codes[i + 2] == FishTranspiler.Add && codes[i + 3] == i_variable.Store(), - code - => new[] - { + code => + [ new(OpCodes.Br_S, new_i_label), // else FishTranspiler.FirstLocalVariable(method, typeof(IngredientCount)) .WithLabels(code.ExtractLabels()), FishTranspiler.FirstArgument(method, typeof(Bill)), FishTranspiler.Call(MarkIngredientCountAsFound), code.WithLabels(new_i_label) - }); + ]); // if (!flag) // { @@ -599,7 +599,7 @@ public class WorkGiver_DoBillOptimization : ClassWithFishPatches } } - public class ThingPositionComparer : IComparer + public sealed class ThingPositionComparer : IComparer { public IntVec3 rootCell; diff --git a/Source/PerformanceFish/JobSystem/WorkGiver_HaulOptimization.cs b/Source/PerformanceFish/JobSystem/WorkGiver_HaulOptimization.cs index 8f095b9..3a92005 100644 --- a/Source/PerformanceFish/JobSystem/WorkGiver_HaulOptimization.cs +++ b/Source/PerformanceFish/JobSystem/WorkGiver_HaulOptimization.cs @@ -7,15 +7,15 @@ using PerformanceFish.ModCompatibility; namespace PerformanceFish.JobSystem; -public class WorkGiver_HaulOptimization : ClassWithFishPatches +public sealed class WorkGiver_HaulOptimization : ClassWithFishPatches { - public class PotentialWorkThingsGlobal_Patch : FishPatch + public sealed class PotentialWorkThingsGlobal_Patch : FishPatch { - public override bool Enabled => base.Enabled && !ActiveMods.Multiplayer; - + public override List IncompatibleModIDs { get; } = [PackageIDs.MULTIPLAYER]; + public override string Description { get; } = "Sorts haulables by distance before running expensive hauling calculations on them to avoid checks on " - + "far away items"; + + "far away items"; public override MethodBase TargetMethodInfo { get; } = AccessTools.Method(typeof(WorkGiver_Haul), nameof(WorkGiver_Haul.PotentialWorkThingsGlobal)); diff --git a/Source/PerformanceFish/JobSystem/WorkGiver_MergeOptimization.cs b/Source/PerformanceFish/JobSystem/WorkGiver_MergeOptimization.cs index 2b69252..ccf3afe 100644 --- a/Source/PerformanceFish/JobSystem/WorkGiver_MergeOptimization.cs +++ b/Source/PerformanceFish/JobSystem/WorkGiver_MergeOptimization.cs @@ -7,13 +7,13 @@ using Verse.AI; namespace PerformanceFish.JobSystem; -public class WorkGiver_MergeOptimization : ClassWithFishPatches +public sealed class WorkGiver_MergeOptimization : ClassWithFishPatches { - public class JobOnThing_Patch : FishPatch + public sealed class JobOnThing_Patch : FishPatch { public override string Description { get; } = "Removes mergeables from ListerMergeables if they no longer qualify as mergeable when trying to check " - + "for a merging job on them. Vanilla RimWorld just fails and keeps them in"; + + "for a merging job on them. Vanilla RimWorld just fails and keeps them in"; public override MethodBase TargetMethodInfo { get; } = AccessTools.Method(typeof(WorkGiver_Merge), nameof(WorkGiver_Merge.JobOnThing)); @@ -23,14 +23,16 @@ public class WorkGiver_MergeOptimization : ClassWithFishPatches if (t.GetSlotGroup() is not null) return true; - RemoveFromListerMergeables(t); + TryRemoveFromListerMergeables(t); __result = null; return false; } - private static void RemoveFromListerMergeables(Thing t) + private static void TryRemoveFromListerMergeables(Thing t) { - var map = t.MapHeld; + if (t.TryGetMapHeld() is not { } map) + return; + map.listerMergeables.TryRemove(t); map.listerHaulables.Check(t); } diff --git a/Source/PerformanceFish/ListerThingsExtensions.cs b/Source/PerformanceFish/ListerThingsExtensions.cs new file mode 100644 index 0000000..61e9f4b --- /dev/null +++ b/Source/PerformanceFish/ListerThingsExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using JetBrains.Annotations; +using PerformanceFish.Listers; + +namespace PerformanceFish; + +[PublicAPI] +public static class ListerThingsExtensions +{ + public static void RemoveFromGroup(this ListerThings listerThings, Thing thing, ThingRequestGroup group) + => ThingsPrepatches.RemoveFromGroupList(listerThings, thing, group); + + public static void AddToGroup(this ListerThings listerThings, Thing thing, ThingRequestGroup group) + => ThingsPrepatches.AddToGroupList(listerThings, thing, group); + + + public static void RemoveFromDefList(this ListerThings listerThings, Thing thing) + => ThingsPrepatches.RemoveFromDefList(listerThings, thing); + + public static void AddToDefList(this ListerThings listerThings, Thing thing) + => ThingsPrepatches.AddToDefList(listerThings, thing); +} \ No newline at end of file diff --git a/Source/PerformanceFish/Listers/Buildings.cs b/Source/PerformanceFish/Listers/Buildings.cs index 776e33e..e69a416 100644 --- a/Source/PerformanceFish/Listers/Buildings.cs +++ b/Source/PerformanceFish/Listers/Buildings.cs @@ -3,300 +3,246 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; namespace PerformanceFish.Listers; -public class Buildings : ClassWithFishPatches +public sealed class Buildings : ClassWithFishPrepatches { - /// - /// Get Things from the ListerThings dictionary with a rather quick lookup instead of looping over every single element in the lists of listerBuildings - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static List ThingsOfDefFast(ListerThings listerThings, ThingDef def) - => listerThings.listsByDef.TryGetValue(def) ?? ListerThings.EmptyList; - - public class ColonistsHaveBuilding_Patch : FishPatch + public sealed class AllBuildingsColonistOfClassPatch : FishPrepatch { public override string Description { get; } - = "ListerBuildings optimization. Should be relatively riskfree, but doesn't impact performance much " - + "outside of a few prevented spikes here and there either"; + = "Optimizes building lookups for generic types on ListerBuildings to directly return the result of cached " + + "data instead of iterating over all buildings on the map trying to find all matches"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.AllBuildingsColonistOfClass)); - public override Expression TargetMethod { get; } - = static () => default(ListerBuildings)!.ColonistsHaveBuilding(default(ThingDef)); + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); - public static bool Prefix(ListerBuildings __instance, ref bool __result, ThingDef def) - { - var lister = GetListerThings(__instance); - if (lister is null) - return true; + public static IEnumerable ReplacementBody(ListerBuildings __instance) where T : Building + => (IEnumerable)__instance.Cache().ColonistBuildingsByType.GetOrAdd(typeof(T)); + } + + public sealed class ColonistsHaveResearchBenchPatch : FishPrepatch + { + public override string Description { get; } + = "Optimizes research bench lookups on ListerBuildings to directly return the result of cached data " + + "instead of iterating over all buildings on the map trying to find the bench"; - __result = ContainsDefForColonists(lister, def); - return false; - } + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.ColonistsHaveResearchBench)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static bool ReplacementBody(ListerBuildings __instance) + => __instance.Cache().ColonistResearchBenches.Count > 0; + } + + public sealed class ColonistsHaveBuildingPatch : FishPrepatch + { + public override string Description { get; } + = "Optimizes building lookups by def on ListerBuildings to directly return the result of cached " + + "data instead of iterating over all buildings on the map trying to find any matches"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.ColonistsHaveBuilding), + [typeof(ThingDef)]); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static bool ReplacementBody(ListerBuildings __instance, ThingDef def) + => __instance.Cache().ColonistBuildingsByDef.GetOrAdd(def).Count > 0; } - public class ColonistsHaveResearchBench_Patch : FishPatch + public sealed class AllBuildingsColonistOfDef_Patch : FishPrepatch { public override string Description { get; } - = "ListerBuildings optimization. Should be relatively riskfree, but doesn't impact performance much " - + "outside of a few prevented spikes here and there either"; + = "Optimizes building lookups by def on ListerBuildings to directly return the result of cached " + + "data instead of iterating over all buildings on the map trying to find all matches. Relatively large " + + "performance impact when having water power plants on the map"; - public override Expression TargetMethod { get; } - = static () => default(ListerBuildings)!.ColonistsHaveResearchBench(); + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.AllBuildingsColonistOfDef)); - public static bool Prefix(ListerBuildings __instance, ref bool __result) - { - var lister = GetListerThings(__instance); - if (lister is null) - return true; + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); - var researchBenches = LazyInit.ResearchBenches; - for (var i = 0; i < researchBenches.Length; i++) - { - if (!ContainsDefForColonists(lister, researchBenches[i])) - continue; - - __result = true; - break; - } - - return false; - } - - public static class LazyInit - { - public static ThingDef[] ResearchBenches = DefDatabase.AllDefsListForReading - .Where(static def => def.thingClass == typeof(Building_ResearchBench)).ToArray(); - - static LazyInit() - { - // beforefieldinit - } - } + public static IEnumerable ReplacementBody(ListerBuildings __instance, ThingDef def) + => __instance.Cache().ColonistBuildingsByDef.GetOrAdd(def); } - public class ColonistsHaveBuildingWithPowerOn_Patch : FishPatch + public sealed class AllBuildingsNonColonistOfDef_Patch : FishPrepatch { public override string Description { get; } - = "ListerBuildings optimization. Should be relatively riskfree, but doesn't impact performance much " - + "outside of a few prevented spikes here and there either"; + = "Optimizes building lookups by def on ListerBuildings to directly return the result of cached " + + "data instead of iterating over all buildings on the map trying to find all matches"; - public override Expression TargetMethod { get; } - = static () => default(ListerBuildings)!.ColonistsHaveBuildingWithPowerOn(null); + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.AllBuildingsNonColonistOfDef)); - public static bool Prefix(ListerBuildings __instance, ref bool __result, ThingDef def) + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static IEnumerable ReplacementBody(ListerBuildings __instance, ThingDef def) + => __instance.Cache().NonColonistBuildingsByDef.GetOrAdd(def); + } + + public sealed class ColonistsHaveBuildingWithPowerOnPatch : FishPrepatch + { + public override string Description { get; } + = "Optimizes lookups for powered buildings of a given def on ListerBuildings to directly return the result " + + "of cached data instead of iterating over all buildings on the map trying to find any matches"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerBuildings), + nameof(ListerBuildings.ColonistsHaveBuildingWithPowerOn)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static bool ReplacementBody(ListerBuildings __instance, ThingDef def) { - var lister = GetListerThings(__instance); - if (lister is null) - return true; - - var things = ThingsOfDefFast(lister, def); - var playerFaction = Faction.OfPlayerSilentFail; - for (var i = 0; i < things.Count; i++) + var colonistBuildingsOfDef = __instance.Cache().ColonistBuildingsByDef.GetOrAdd(def); + + for (var i = 0; i < colonistBuildingsOfDef.Count; i++) { - if (things[i].Faction != playerFaction - || things[i].TryGetComp() is { PowerOn: false }) - { - continue; - } - - __result = true; - break; + if (colonistBuildingsOfDef[i].TryGetComp() is not { PowerOn: false }) + return true; } return false; } } - - public class AllBuildingsColonistOfDef_Patch : FishPatch + + public sealed class AddPatch : FishPrepatch { - public override string Description { get; } - = "ListerBuildings optimization. Relatively large performance impact when having water power plants on the " - + "map."; + public override List LinkedPatches { get; } = + [ + typeof(AllBuildingsColonistOfClassPatch), typeof(ColonistsHaveResearchBenchPatch), + typeof(ColonistsHaveBuildingPatch), typeof(AllBuildingsColonistOfDef_Patch), + typeof(ColonistsHaveBuildingWithPowerOnPatch), + typeof(DeepResourceGridOptimization.AnyActiveDeepScannersOnMapPatch) + ]; + + public override string? Description { get; } + = "Required for all other ListerBuildings patches. This assigns buildings to the cache when they're " + + "spawned"; - public override Expression TargetMethod { get; } - = static () => default(ListerBuildings)!.AllBuildingsColonistOfDef(null); + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.Add)); - public static bool Prefix(ListerBuildings __instance, ref IEnumerable __result, ThingDef def) + public static void Postfix(ListerBuildings __instance, Building b) { - var lister = GetListerThings(__instance); - if (lister is null) - return true; + var def = b.def; + + if (def.building is { isNaturalRock: true }) + return; - __result = AllBuildingsColonistOfDefReplacement(lister, def); - return false; - } - - public static IEnumerable AllBuildingsColonistOfDefReplacement(ListerThings lister, ThingDef def) - { - var things = ThingsOfDefFast(lister, def); - var playerFaction = Faction.OfPlayerSilentFail; - for (var i = 0; i < things.Count; i++) + var cache = __instance.Cache(); + + if (b.Faction == Faction.OfPlayer) { - if (things[i] is Building building) - { - if (building.Faction == playerFaction) - yield return building; - } - else - { - ThrowWarning(def, things[i]); - yield break; - } - } - } - } - - public record struct AllBuildingsColonistOfDefCacheValue - { - public List Buildings; - } - - public class AllBuildingsNonColonistOfDef_Patch : FishPatch - { - public override string Description { get; } - = "ListerBuildings optimization. Should be relatively riskfree, but doesn't impact performance much " - + "outside of a few prevented spikes here and there either"; - - public override Expression TargetMethod { get; } - = static () => default(ListerBuildings)!.AllBuildingsNonColonistOfDef(null); - - public static bool Prefix(ListerBuildings __instance, ref IEnumerable __result, ThingDef def) - { - var lister = GetListerThings(__instance); - if (lister is null) - return true; - - __result = AllBuildingsNonColonistOfDefReplacement(lister, def); - return false; - } - - public static IEnumerable AllBuildingsNonColonistOfDefReplacement(ListerThings lister, ThingDef def) - { - var things = ThingsOfDefFast(lister, def); - var playerFaction = Faction.OfPlayerSilentFail; - for (var i = 0; i < things.Count; i++) - { - if (things[i] is Building building) - { - if (building.Faction != playerFaction) - yield return building; - } - else - { - ThrowWarning(def, things[i]); - yield break; - } - } - } - } - -#if harmonyIsFailingOnThisOne - public class AllBuildingsColonistOfClass_Patch : FishPatch - { - public override string Description { get; } - = "ListerBuildings optimization. Should be relatively riskfree, but doesn't impact performance much " - + "outside of a few prevented spikes here and there either"; - - public override Expression TargetMethod { get; } - = static () => default(ListerBuildings)!.AllBuildingsColonistOfClass(); - - public static bool Prefix(ListerBuildings __instance, MethodBase __originalMethod, ref IEnumerable __result) - { - var lister = GetListerThings(__instance); - if (lister is null) - return true; - - var type = __originalMethod.GetGenericArguments()[0]; //<-- This always returns Building instead of the - // type it's actually meant to look for - DebugLog.Message($"AllBuildingsColonistOfClass running for type {type.Name}"); - if (!Replacements.TryGetValue(type, out var replacement)) - { - Replacements.Add(type, - replacement = (GenericReplacementBase)Activator.CreateInstance( - typeof(GenericReplacement<>).MakeGenericType(type))); - } - - __result = replacement.Replacement(lister); - return false; - } - - public static Dictionary Replacements { get; } = new(); - - public abstract class GenericReplacementBase - { - public abstract IEnumerable Replacement(ListerThings lister); - } - - public class GenericReplacement : GenericReplacementBase where T : Building - { - public override IEnumerable Replacement(ListerThings lister) - { - buildingsOfClass ??= DefDatabase.AllDefsListForReading - .Where(static def => def.thingClass == typeof(T)).ToArray(); + cache.ColonistBuildingsByDef.GetOrAdd(def).Add(b); + if (b is Building_ResearchBench researchBench) + cache.ColonistResearchBenches.Add(researchBench); - foreach (var def in buildingsOfClass) + if (b.TryGetComp() != null) + cache.ColonistDeepScanners.Add(b); + + var buildingType = b.GetType(); + + do { - var things = ThingsOfDefFast(lister, def); - for (var i = 0; i < things.Count; i++) - { - if (things[i].Faction == Faction.OfPlayerSilentFail) - yield return things[i]; - } + if (buildingType == null) + break; + + cache.ColonistBuildingsByType.GetOrAdd(buildingType).Add(b); } + while ((buildingType = buildingType.BaseType) != typeof(Building)); + } + else + { + cache.NonColonistBuildingsByDef.GetOrAdd(def).Add(b); } - - public ThingDef[]? buildingsOfClass; } } -#endif - public class Map_Patch : FishPatch + public sealed class RemovePatch : FishPrepatch { - public override string Description { get; } - = "Part of the ListerBuildings optimizations. Without this patch mods like Colony Manager throw errors " - + "during loading"; + public override List LinkedPatches { get; } = + [ + typeof(AllBuildingsColonistOfClassPatch), typeof(ColonistsHaveResearchBenchPatch), + typeof(ColonistsHaveBuildingPatch), typeof(AllBuildingsColonistOfDef_Patch), + typeof(ColonistsHaveBuildingWithPowerOnPatch), + typeof(DeepResourceGridOptimization.AnyActiveDeepScannersOnMapPatch) + ]; - public override MethodBase TargetMethodInfo { get; } = AccessTools.Constructor(typeof(Map)); - public static void Postfix(Map __instance) => _tempMap = __instance; - } + public override string? Description { get; } + = "Required for all other ListerBuildings patches. This removes buildings from the map when they're " + + "despawned"; - private static Map? _tempMap; - // mapComps get constructed before maps are added to Game.Maps, so this makes the patches work there too + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerBuildings), nameof(ListerBuildings.Remove)); - public static void ThrowWarning(ThingDef def, Thing thing) - => Log.Warning($"Tried to get building of def {def}, but its type is {thing.GetType()} instead of Building"); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ListerThings? GetListerThings(ListerBuildings listerBuildings) - { - var maps = Current.gameInt.Maps; - for (var i = 0; i < maps.Count; i++) + public static void Postfix(ListerBuildings __instance, Building b) { - if (maps[i].listerBuildings == listerBuildings) - return maps[i].listerThings; - } + var def = b.def; + + if (def.building is { isNaturalRock: true }) + return; - return GetListerThingsUsingTempMapFallback(listerBuildings); + var cache = __instance.Cache(); + + if (b.Faction == Faction.OfPlayer) + { + cache.ColonistBuildingsByDef.GetOrAdd(def).Remove(b); + if (b is Building_ResearchBench researchBench) + cache.ColonistResearchBenches.Remove(researchBench); + + cache.ColonistDeepScanners.Remove(b); + + var buildingType = b.GetType(); + + do + { + if (buildingType == null) + break; + + cache.ColonistBuildingsByType.GetOrAdd(buildingType).Remove(b); + } + while ((buildingType = buildingType.BaseType) != typeof(Building)); + } + else + { + cache.NonColonistBuildingsByDef.GetOrAdd(def).Remove(b); + } + } } - [MethodImpl(MethodImplOptions.NoInlining)] - private static ListerThings? GetListerThingsUsingTempMapFallback(ListerBuildings listerBuildings) - => _tempMap!.listerBuildings == listerBuildings ? _tempMap.listerThings : null; - - public static bool ContainsDefForColonists(ListerThings listerThings, ThingDef def) + public readonly record struct Cache { - if (!listerThings.listsByDef.TryGetValue(def, out var things)) - return false; + public readonly FishTable> + ColonistBuildingsByDef = new(), + NonColonistBuildingsByDef = new(); - var playerFaction = Faction.OfPlayerSilentFail; - for (var i = 0; i < things.Count; i++) + public readonly IndexedFishSet ColonistResearchBenches = []; + + public readonly IndexedFishSet ColonistDeepScanners = []; + + public readonly FishTable ColonistBuildingsByType = new() { - if (things[i].Faction == playerFaction) - return true; - } + ValueInitializer = static type + => (IList)Activator.CreateInstance(typeof(IndexedFishSet<>).MakeGenericType(type)) + }; - return false; + public Cache() + { + } } } \ No newline at end of file diff --git a/Source/PerformanceFish/Listers/Haulables.cs b/Source/PerformanceFish/Listers/Haulables.cs index 7460a54..fb819d3 100644 --- a/Source/PerformanceFish/Listers/Haulables.cs +++ b/Source/PerformanceFish/Listers/Haulables.cs @@ -3,375 +3,177 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -global using HaulablesCache - = PerformanceFish.Cache.ByReference; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Reflection.Emit; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using PerformanceFish.Cache; -using PerformanceFish.ModCompatibility; +using Mono.Cecil; +using Mono.Cecil.Cil; +using MonoMod.Utils; +using PerformanceFish.Prepatching; namespace PerformanceFish.Listers; -public class Haulables : ClassWithFishPatches +public sealed class Haulables : ClassWithFishPrepatches { - public class MapPreTick_Patch : FishPatch + public sealed class CheckPatch : FishPrepatch { - public override bool Enabled => base.Enabled && !ActiveMods.Multiplayer; - - public override string Description { get; } - = "Throttling of the ListerHaulablesTick. This marks whether items should be moved from one stockpile to " - + "another. When throttled these checks get spread out more evenly, helping against spikes that can " - + "happen with large deep storage containers."; + public override string? Description { get; } + = "Optimizes the ListerHaulables by making frequent checks that happen every tick to only check for adding " + + "new haulables, skipping things that are already inside the lister. This generally results in them " + + "getting removed when actively checked by pawns or otherwise getting destroyed instead, which happens " + + "far less frequently. Hauling behaviour visible to the player remains identical"; - public override Expression TargetMethod { get; } = static () => default(Map)!.MapPreTick(); + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerHaulables), nameof(ListerHaulables.Check)); - public static CodeInstructions Transpiler(CodeInstructions codes) - => codes.MethodReplacer(new ListerHaulables(null).ListerHaulablesTick, - ThreadedOrThrottled_ListerHaulablesTick); - - public static void ThreadedOrThrottled_ListerHaulablesTick(ListerHaulables instance) - { - MainThread = Environment.CurrentManagedThreadId; - - if (false && FishSettings.ThreadingEnabled) - Threaded_ListerHaulablesTick(instance); - else - Throttled_ListerHaulablesTick(instance); - } - - //disabled - public static void Threaded_ListerHaulablesTick(ListerHaulables instance) - { - DequeueQueuedThings(instance); - - var currentCellCheckCount = GetCellCheckCount(instance); - if (currentCellCheckCount is null) - return; - - if (_currentTask is { IsCompleted: false }) - { - if (currentCellCheckCount.value > 1) - currentCellCheckCount.value--; - if (Volatile.Read(ref _queue) == 0) - { - Interlocked.Increment(ref _queue); - _currentTask = _currentTask.ContinueWith(t => - { - if (t.Exception is { } taskException) - Log.Error(taskException.ToString()); - - lock (Lock) - { - _skippedTicks = 0; - Interlocked.Decrement(ref _queue); - ListerHaulablesTick_Patch.CurrentCellCheckCount = currentCellCheckCount.value; - instance.ListerHaulablesTick(); - } - }); - } - else - { - if (_skippedTicks >= 9) - { - Log.Warning("Skipped 10 or more ListerHaulables ticks. This often means there's some " - + "kind of hauling error happening"); - _skippedTicks = 0; - } - else - { - _skippedTicks++; - } - } - - return; - } - else - { - if (currentCellCheckCount.value < ListerHaulables.CellsPerTick) - currentCellCheckCount.value++; - } - - if (_currentTask?.Exception is { } exception) - Log.Error(exception.ToString()); - - _currentTask = Task.Run(() => - { - lock (Lock) - { - _skippedTicks = 0; - ListerHaulablesTick_Patch.CurrentCellCheckCount = currentCellCheckCount.value; - instance.ListerHaulablesTick(); - } - }); - } - - public static void DequeueQueuedThings(ListerHaulables instance) - { - while (ThingsToRemove.TryDequeue(out var thing)) - TryRemove(instance, thing); - - while (ThingsToAdd.TryDequeue(out var thing)) - TryAdd(instance, thing); - } - - public static void Throttled_ListerHaulablesTick(ListerHaulables instance) - { - var cellCheckCount = GetCellCheckCount(instance); - if (cellCheckCount is null) - return; - - ListerHaulablesTick_Patch.CurrentCellCheckCount = cellCheckCount.value; - Stopwatch.Start(); - try - { - instance.ListerHaulablesTick(); - } - catch (Exception ex) - { - Log.Error(ex.ToString()); - } - - Stopwatch.Stop(); - if (Stopwatch.ElapsedTicks > 4000L) - { - if (cellCheckCount.value > 0) - cellCheckCount.value--; - } - else - { - if (cellCheckCount.value < 4) - cellCheckCount.value++; - } - - Stopwatch.Reset(); - } - - private static ListerHaulablesTick_Patch.CellCheckCount? GetCellCheckCount(ListerHaulables instance) - { - var groupCycleIndex = ListerHaulables.groupCycleIndex + 1; - if (groupCycleIndex >= int.MaxValue) - groupCycleIndex = 0; - if (groupCycleIndex == _lastGroupCycleIndex) - { - groupCycleIndex++; - if (groupCycleIndex >= int.MaxValue) - groupCycleIndex = 0; - } - - _lastGroupCycleIndex = groupCycleIndex; - - var allGroupsListForReading = instance.map.haulDestinationManager.AllGroupsListForReading; - if (allGroupsListForReading.Count == 0) - return null; - - var num = groupCycleIndex % allGroupsListForReading.Count; - var slotGroup = allGroupsListForReading[num]; - return slotGroup.CellsList.Count == 0 - ? null - : ListerHaulablesTick_Patch.CellCheckCountsBySlotGroup.GetOrCreateValue(slotGroup); - } - - public static int MainThread { get; private set; } - public static ConcurrentQueue ThingsToRemove { get; } = new(); - public static ConcurrentQueue ThingsToAdd { get; } = new(); - private static Stopwatch Stopwatch { get; } = new(); - - private static int _lastGroupCycleIndex; - private static int _skippedTicks; - private static int _queue; - private static Task? _currentTask; - - private static object Lock { get; } = new(); + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(CheckAddPatch.ReplacementBody); } - - public class ListerHaulablesTick_Patch : FishPatch + + public sealed class CheckAddPatch : FishPrepatch { - public override bool Enabled => base.Enabled && !ActiveMods.Multiplayer; - - public override string Description { get; } = "Part of the ListerHaulables throttling optimization"; + public override string? Description { get; } + = "Caches haulables in a fast custom hash set to not have to look them up in a large list"; - public override Expression TargetMethod { get; } - = static () => default(ListerHaulables)!.ListerHaulablesTick(); + public override List LinkedPatches { get; } = [typeof(TryRemovePatch)]; - public static CodeInstructions Transpiler(CodeInstructions codes) - => codes //.MethodReplacer(GridsUtility.GetThingList, GetCopyOfThingList) - .Manipulator(static c => c.LoadsConstant(4), static c => - { - c.opcode = OpCodes.Call; - c.operand = typeof(ListerHaulablesTick_Patch).GetProperty(nameof(CurrentCellCheckCount))!.GetMethod; - }); + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerHaulables), nameof(ListerHaulables.CheckAdd)); - /*public static List GetCopyOfThingList(IntVec3 c, Map map) + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(ListerHaulables __instance, Thing t) { - if (FishSettings.ThreadingEnabled) - { - PrivateThingList.ReplaceContentsWith(map.thingGrid.ThingsListAt(c)); - return PrivateThingList; - } - else - { - return map.thingGrid.ThingsListAt(c); - } - }*/ - public static int CurrentCellCheckCount { get; set; } = ListerHaulables.CellsPerTick; - public static ConditionalWeakTable CellCheckCountsBySlotGroup { get; } = new(); - - public class CellCheckCount - { - public int value = ListerHaulables.CellsPerTick; - } - //private static List PrivateThingList { get; } = new(); - } - - public static void TryAdd(ListerHaulables instance, Thing t) - { - ref var cache = ref HaulablesCache.GetAndCheck(t, instance); - if (cache.Contains) - return; - - if (!instance.haulables.Contains(t)) - instance.haulables.Add(t); - - cache.SetValue(true, t); - } - - public static void ThreadSafeTryAdd(ListerHaulables instance, Thing t) - { - if (Environment.CurrentManagedThreadId == MapPreTick_Patch.MainThread || !FishSettings.ThreadingEnabled) - { - TryAdd(instance, t); - } - else - { - if (!HaulablesCache.GetAndCheck(t, instance).Contains) - MapPreTick_Patch.ThingsToAdd.Enqueue(t); - } - } - - public static void TryRemove(ListerHaulables instance, Thing t) - { - ref var cache = ref HaulablesCache.GetAndCheck(t, instance); - if (!cache.Contains) - return; - - instance.haulables.Remove(t); - cache.SetValue(false, t); - } - - public static void ThreadSafeTryRemove(ListerHaulables instance, Thing t) - { - if (Environment.CurrentManagedThreadId == MapPreTick_Patch.MainThread || !FishSettings.ThreadingEnabled) - { - TryRemove(instance, t); - } - else - { - if (HaulablesCache.GetAndCheck(t, instance).Contains) - MapPreTick_Patch.ThingsToRemove.Enqueue(t); - } - } - - public class Check_Patch : FishPatch - { - public override string Description { get; } = "Optimization for haulables checking"; - public override Expression TargetMethod { get; } = static () => default(ListerHaulables)!.Check(null); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Check(ListerHaulables instance, Thing t) - { - // So the situation here is this: Vanilla RimWorld normally checks 4 storage cells per tick for valid haul - // destinations, usually with the intent of reaching higher priority storage. This check itself can get - // quite expensive, but 4 of them aren't at all enough to be worth bothering with. Now problems arise when - // LWM's deep storage comes into play: You put 5 stacks of items in one cell and suddenly 4 checks turn - // into 20. 20 hauling checks in turn is a number large enough to cause performance issues in colonies with - // high wealth. - - if (!t.def.alwaysHaulable && !t.def.designateHaulable) - return; - - if (instance.ShouldBeHaulable(t)) - ThreadSafeTryAdd(instance, t); - else - ThreadSafeTryRemove(instance, t); - } - - public static CodeInstructions Transpiler(CodeInstructions codeInstructions, ILGenerator generator) - => Reflection.GetCodeInstructions(Check, generator); - } - - public class TryRemove_Patch : FishPatch - { - public override string Description { get; } = "Part of the ListerHaulables optimization"; - - public override Expression TargetMethod { get; } - = static () => default(ListerHaulables)!.TryRemove(null); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void TryRemove(ListerHaulables instance, Thing t) - { - if (t.def.category == ThingCategory.Item) - ThreadSafeTryRemove(instance, t); - } - - public static CodeInstructions Transpiler(CodeInstructions codeInstructions, ILGenerator generator) - => Reflection.GetCodeInstructions(TryRemove, generator); - } - - public class CheckAdd_Patch : FishPatch - { - public override string Description { get; } = "Part of the ListerHaulables optimization"; - - public override Expression TargetMethod { get; } - = static () => default(ListerHaulables)!.CheckAdd(null); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CheckAdd(ListerHaulables instance, Thing t) - { - if (!t.def.alwaysHaulable && !t.def.designateHaulable) + if (!t.IsItem()) return; - if (HaulablesCache.GetAndCheck(t, instance).Contains - || !instance.ShouldBeHaulable(t)) - { + var haulables = __instance.haulables; + + ref var cache = ref __instance.Cache(); + cache.CheckUpdate(haulables); + + var haulablesInCache = cache.ContainedThings; + var thingKey = t.GetKey(); + if (haulablesInCache.Contains(thingKey)) return; - } - - ThreadSafeTryAdd(instance, t); + + if (!__instance.ShouldBeHaulable(t)) + return; + + haulables.Add(t); + haulablesInCache.Add(thingKey); } + } + + public sealed class TryRemovePatch : FishPrepatch + { + public override string? Description { get; } + = "Caches haulables in a fast custom hash set to not have to look them up in a large list"; + + public override List LinkedPatches { get; } = [typeof(CheckAddPatch)]; - public static CodeInstructions Transpiler(CodeInstructions codeInstructions, ILGenerator generator) - => Reflection.GetCodeInstructions(CheckAdd, generator); + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerHaulables), nameof(ListerHaulables.TryRemove)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(ListerHaulables __instance, Thing t) + { + if (t.def.category != ThingCategory.Item) + return; + + var haulables = __instance.haulables; + + ref var cache = ref __instance.Cache(); + cache.CheckUpdate(haulables); + + if (cache.ContainedThings.Remove(t.GetKey())) + haulables.Remove(t); + } } - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public record struct HaulablesCacheValue : ICacheable + public sealed class TickPatch : FishPrepatch { - private int _nextRefreshTick; - public bool Contains; + public override string? Description { get; } + = "Essentially fixes a bug that was causing the method to tick cells multiple times for storages with less " + + "than 4 cells. Not ticking multiple times leads to a performance improvement"; - public void Update(ref HaulablesCache key) + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerHaulables), nameof(ListerHaulables.ListerHaulablesTick)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) { - Contains = key.Second.haulables.Contains(key.First); - SetDirty(false, key.First.thingIDNumber); + var instructions = ilProcessor.instructions; + + var fourInstructionIndex = instructions.FirstIndexOf(static code + => code.OpCode == OpCodes.Ldc_I4_4 || (code.OpCode == OpCodes.Ldc_I4 && code.Operand is 4)); + + ilProcessor.InsertRange(fourInstructionIndex + 1, + (OpCodes.Ldloc, + ilProcessor.Body.Variables.First(static local => local.VariableType.Is(typeof(SlotGroup)))), + (OpCodes.Call, methodof(GetCellCountToTick))); } - public void SetValue(bool contains, Thing thing) - { - Contains = contains; - SetDirty(false, thing.thingIDNumber); - } - - public void SetDirty(bool value, int offset) - => _nextRefreshTick = value ? 0 : TickHelper.Add(3072, offset, 2048); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetCellCountToTick(int original, SlotGroup slotGroup) + => Math.Min(original, slotGroup.CellsList.Count); + } + + public record struct Cache + { + public readonly FishSet ContainedThings = []; - public bool Dirty + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CheckUpdate(List haulables) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => TickHelper.Past(_nextRefreshTick); + if (IsDirty(haulables)) + Update(haulables); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDirty(List haulables) => haulables.Count != ContainedThings.Count; + + [MethodImpl(MethodImplOptions.NoInlining)] + public void Update(List haulables) + { + ContainedThings.Clear(); + + for (var i = haulables.Count; i-- > 0;) + ContainedThings.Add(haulables[i].GetKey()); + } + + public Cache() + { + } + } +} + +public static class ListerHaulablesExtensions +{ + public static void TryAddDirectly(this ListerHaulables lister, Thing t) + { + if (!t.IsItem()) + return; + + var haulables = lister.haulables; + + ref var cache = ref lister.Cache(); + cache.CheckUpdate(haulables); + + if (cache.ContainedThings.Add(t.GetKey())) + haulables.Add(t); + } + + public static void TryRemoveDirectly(this ListerHaulables lister, Thing t) + { + if (!t.IsItem()) + return; + + var haulables = lister.haulables; + + ref var cache = ref lister.Cache(); + cache.CheckUpdate(haulables); + + if (cache.ContainedThings.Remove(t.GetKey())) + haulables.Remove(t); } } \ No newline at end of file diff --git a/Source/PerformanceFish/Listers/Mergeables.cs b/Source/PerformanceFish/Listers/Mergeables.cs index 5aabf8f..7c39b9a 100644 --- a/Source/PerformanceFish/Listers/Mergeables.cs +++ b/Source/PerformanceFish/Listers/Mergeables.cs @@ -3,6 +3,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +#if bad + global using MergeablesCache = PerformanceFish.Cache.ByReference; @@ -10,10 +12,21 @@ using PerformanceFish.Cache; namespace PerformanceFish.Listers; -public class Mergeables : ClassWithFishPatches +public sealed class Mergeables : ClassWithFishPatches { - public class Check_Patch : FishPatch + public sealed class Check_Patch : FishPatch { + protected internal override void OnPatchingCompleted() + { + Hauling.StorageSettingsPatches.TryNotifyChanged_Patch.Changed += _ => MergeablesCache.Get.Clear(); + + Events.ThingEvents.Initialized += thing => + { + if (thing is IHaulDestination) + thing.Events().Destroying += _ => MergeablesCache.Get.Clear(); + }; + } + public override string Description { get; } = "Optimization for mergeables checking"; public override Expression TargetMethod { get; } = static () => default(ListerMergeables)!.Check(null); @@ -46,7 +59,7 @@ public class Mergeables : ClassWithFishPatches => Reflection.MakeReplacementCall(Check); } - public class CheckAdd_Patch : FishPatch + public sealed class CheckAdd_Patch : FishPatch { public override string Description { get; } = "Part of the ListerMergeables optimization"; @@ -70,7 +83,7 @@ public class Mergeables : ClassWithFishPatches => Reflection.MakeReplacementCall(CheckAdd); } - public class TryRemove_Patch : FishPatch + public sealed class TryRemove_Patch : FishPatch { public override string Description { get; } = "Part of the ListerMergeables optimization"; @@ -79,16 +92,16 @@ public class Mergeables : ClassWithFishPatches public static void TryRemove(ListerMergeables instance, Thing t) { - if (t.def.category == ThingCategory.Item) - { - ref var cache = ref MergeablesCache.GetAndCheck(t, instance); + if (!t.IsItem()) + return; - if (!cache.Contains) - return; + ref var cache = ref MergeablesCache.GetAndCheck(t, instance); - instance.mergeables.Remove(t); - cache.SetValue(false, t); - } + if (!cache.Contains) + return; + + instance.mergeables.Remove(t); + cache.SetValue(false, t); } public static CodeInstructions Transpiler(CodeInstructions codeInstructions) @@ -121,4 +134,5 @@ public class Mergeables : ClassWithFishPatches get => TickHelper.Past(_nextRefreshTick); } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/Source/PerformanceFish/Listers/Misc.cs b/Source/PerformanceFish/Listers/Misc.cs deleted file mode 100644 index 05b28ba..0000000 --- a/Source/PerformanceFish/Listers/Misc.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2023 bradson -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -namespace PerformanceFish.Listers; - -public class Misc : ClassWithFishPatches -{ - public class Building_Destroy_Patch : FishPatch - { - public override string Description { get; } - = "Used for haulables caching. Items don't become haulable after deconstructing stockpiles without this " - + "patch"; - - public override Expression TargetMethod { get; } = static () => default(Building)!.Destroy(default); - - public static void Prefix(Building __instance) - { - if (__instance is not IHaulDestination) - return; - - HaulablesCache.Get.Clear(); - MergeablesCache.Get.Clear(); - } - } -} \ No newline at end of file diff --git a/Source/PerformanceFish/Listers/StorageSettingsPatches.cs b/Source/PerformanceFish/Listers/StorageSettingsPatches.cs deleted file mode 100644 index 4606a03..0000000 --- a/Source/PerformanceFish/Listers/StorageSettingsPatches.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2023 bradson -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -using System.Runtime.InteropServices; -using PerformanceFish.Cache; -using StorageSettingsCache - = PerformanceFish.Cache.ByReference; - -namespace PerformanceFish.Listers; - -public class StorageSettingsPatches : ClassWithFishPatches -{ - public class AllowedToAccept_Patch : FirstPriorityFishPatch - { - public override string Description { get; } = "StorageSettings caching"; - - public override Expression TargetMethod { get; } - = static () => default(StorageSettings)!.AllowedToAccept(default(Thing)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Prefix(StorageSettings __instance, Thing t, ref bool __result, out bool __state) - { - ref var cache = ref StorageSettingsCache.GetOrAddReference(new(t, __instance)); - - if (cache.Dirty) - return __state = true; - - __result = cache.AllowedToAccept; - return __state = false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Postfix(StorageSettings __instance, Thing t, bool __result, bool __state) - { - if (!__state) - return; - - StorageSettingsCache.Update(t, __instance, __result); - } - } - - public class TryNotifyChanged_Patch : FishPatch - { - public override string Description { get; } = "StorageSettings cache resetting"; - public override Expression TargetMethod { get; } = static () => default(StorageSettings)!.TryNotifyChanged(); - - public static void Postfix(StorageSettings __instance) - { - StorageSettingsCache.Get.Clear(); - HaulablesCache.Get.Clear(); - MergeablesCache.Get.Clear(); - } - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public record struct StorageSettingsCacheValue : ICacheable - { - private int _nextRefreshTick; - public bool AllowedToAccept; - - public bool Dirty - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => TickHelper.Past(_nextRefreshTick); - } - - public void Update(ref StorageSettingsCache key, bool allowedToAccept) - { - AllowedToAccept = allowedToAccept; - _nextRefreshTick = TickHelper.Add(3072, key.First.thingIDNumber, 2048); - } - } -} \ No newline at end of file diff --git a/Source/PerformanceFish/Listers/ThingOwnerOptimization.cs b/Source/PerformanceFish/Listers/ThingOwnerOptimization.cs new file mode 100644 index 0000000..f920952 --- /dev/null +++ b/Source/PerformanceFish/Listers/ThingOwnerOptimization.cs @@ -0,0 +1,148 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; + +namespace PerformanceFish.Listers; + +public sealed class ThingOwnerOptimization : ClassWithFishPrepatches +{ + public sealed class ExposeDataPatch : FishPrepatch + { + public override string? Description { get; } + = "Required to keep the ThingOwner cache synced"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ThingOwner), nameof(ThingOwner.ExposeData)); + + public static void Postfix(ThingOwner __instance) where T : Thing + { + if (Scribe.mode != LoadSaveMode.PostLoadInit) + return; + + var indexMap = __instance.IndexMap(); + var innerList = __instance.innerList; + innerList.RemoveAll(static thing => thing is null); + + indexMap.Clear(); + for (var i = innerList.Count; i-- > 0;) + indexMap[innerList[i].GetKey()] = i; + } + } + + public sealed class TryAddPatch : FishPrepatch + { + public override string? Description { get; } + = "Required to keep the ThingOwner cache synced"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ThingOwner), nameof(ThingOwner.TryAdd), + [typeof(Thing), typeof(bool)]); + + public static void Postfix(ThingOwner __instance, Thing? item, bool canMergeWithExistingStacks, + bool __result) where T : Thing + { + if (!__result + || item is null + || (canMergeWithExistingStacks && (item.Destroyed || item.stackCount == 0))) + { + return; + } + + __instance.IndexMap()[item.GetKey()] = __instance.innerList.Count - 1; + } + } + + /*public sealed class IndexOfPatch : FishPrepatch // not really used enough to be worth patching + { + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ThingOwner), nameof(ThingOwner.IndexOf)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static int ReplacementBody(ThingOwner instance, Thing? item) where T : Thing + => item is not T itemOfT || item.holdingOwner != instance + ? -1 + : instance.innerList is var innerList + && instance.IndexMap().TryGetValue(GetKey(itemOfT), out var knownIndex) + && knownIndex < innerList.Count + && innerList[knownIndex] == item + ? knownIndex + : innerList.IndexOf(itemOfT); + }*/ + + public sealed class RemovePatch : FishPrepatch + { + public override string? Description { get; } + = "Optimizes ThingOwner to keep a cache of thing indices, to greatly speed up removal and in turn improve " + + "performance when despawning happens. Late game colonies and maps in biomes with large amounts of plants " + + "benefit most from this"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ThingOwner), nameof(ThingOwner.Remove)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + { + var codes = ilProcessor.instructions; + + var lastIndexOfIndex = codes.FirstIndexOf(static code + => code.Operand is MethodReference { Name: "LastIndexOf" }); + + for (var i = lastIndexOfIndex; i-- > 0;) + { + if (codes[i].OpCode != OpCodes.Ldfld || !codes[i - 1].LoadsArgument(0)) + continue; + + codes.RemoveAt(i); + lastIndexOfIndex--; + break; + } + + var removeAtIndex = codes.FirstIndexOf(static code + => code.Operand is MethodReference { Name: "RemoveAt" }); + + for (var i = removeAtIndex + 1; i-- > 0;) + { + if (i == lastIndexOfIndex) + break; + + codes.RemoveAt(i); + } + + codes[lastIndexOfIndex].OpCode = OpCodes.Call; + codes[lastIndexOfIndex].Operand + = module.ImportMethodWithGenericArguments(methodof(Remove).GetGenericMethodDefinition(), + ilProcessor.Body.Method.DeclaringType.GenericParameters); + + var variables = ilProcessor.Body.Variables; + variables.RemoveAt(variables.FirstIndexOf(variable => variable.VariableType == module.TypeSystem.Int32)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Remove(ThingOwner instance, Thing item) where T : Thing + { + var indexMap = instance.IndexMap(); + var innerList = instance.innerList; + var itemKey = item.GetKey(); + + var index = indexMap.TryGetValue(itemKey, out var knownIndex) + && knownIndex < innerList.Count + && innerList[knownIndex] == item + ? knownIndex + : innerList.LastIndexOf((T)item); + + if (index >= 0) + innerList.RemoveAtFastUnordered(index); + + indexMap.Remove(itemKey); + + if (index < innerList.Count && index >= 0) + indexMap[innerList[index].GetKey()] = index; + } + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Listers/Things.cs b/Source/PerformanceFish/Listers/Things.cs index 12ddc23..b7886cf 100644 --- a/Source/PerformanceFish/Listers/Things.cs +++ b/Source/PerformanceFish/Listers/Things.cs @@ -3,27 +3,196 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; + namespace PerformanceFish.Listers; -public class Things : ClassWithFishPatches +public sealed class ThingsPrepatches : ClassWithFishPrepatches { - public class ThingsMatching_Patch : FirstPriorityFishPatch + public sealed class AddPatch : FishPrepatch { + public override string? Description { get; } + = "Required to keep the ListerThings cache synced"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerThings), nameof(ListerThings.Add)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(ListerThings __instance, Thing t) + { + if (!ListerThings.EverListable(t.def, __instance.use)) + return; + + AddToDefList(__instance, t); + + var allGroups = ThingListGroupHelper.AllGroups; + foreach (var thingRequestGroup in allGroups) + { + if ((__instance.use == ListerThingsUse.Region && !thingRequestGroup.StoreInRegion()) + || !thingRequestGroup.Includes(t.def)) + { + continue; + } + + AddToGroupList(__instance, t, thingRequestGroup); + } + } + } + + public sealed class RemovePatch : FishPrepatch + { + public override string? Description { get; } + = "Optimizes ListerThings to keep a cache of thing indices within the lister, to greatly speed up removal " + + "and in turn improve performance when despawning happens. Late game colonies and maps in biomes with " + + "large amounts of plants benefit most from this"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerThings), nameof(ListerThings.Remove)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(ListerThings __instance, Thing t) + { + if (!ListerThings.EverListable(t.def, __instance.use)) + return; + + RemoveFromDefList(__instance, t); + + var allGroups = ThingListGroupHelper.AllGroups; + for (var i = 0; i < allGroups.Length; i++) + { + var thingRequestGroup = allGroups[i]; + if ((__instance.use == ListerThingsUse.Region && !thingRequestGroup.StoreInRegion()) + || !thingRequestGroup.Includes(t.def)) + { + continue; + } + + RemoveFromGroupList(__instance, t, thingRequestGroup); + } + } + } + + public static void AddToDefList(ListerThings lister, Thing thing) + { + if (!lister.listsByDef.TryGetValue(thing.def, out var value)) + { + value = []; + lister.listsByDef.Add(thing.def, value); + } + + value.Add(thing); + lister.IndexMapByDef()[thing.GetKey()] = lister.listsByDef[thing.def].Count - 1; + } + + public static void AddToGroupList(ListerThings lister, Thing thing, ThingRequestGroup thingRequestGroup) + { + var list = lister.listsByGroup[(uint)thingRequestGroup]; + if (list == null) + { + list = []; + lister.listsByGroup[(uint)thingRequestGroup] = list; + lister.stateHashByGroup[(uint)thingRequestGroup] = 0; + } + + list.Add(thing); + lister.stateHashByGroup[(uint)thingRequestGroup]++; + + lister.IndexMapByGroup()[new(thingRequestGroup, thing.GetKey())] + = lister.listsByGroup[(uint)thingRequestGroup].Count - 1; + } + + public static void RemoveFromDefList(ListerThings lister, Thing thing) + { + var thingKey = thing.GetKey(); + var indexMapByDef = lister.IndexMapByDef(); + var listByDef = lister.listsByDef[thing.def]; + + var indexByDef = indexMapByDef.TryGetValue(thingKey, out var knownIndexByDef) + && knownIndexByDef < listByDef.Count + && listByDef[knownIndexByDef] == thing + ? knownIndexByDef + : listByDef.LastIndexOf(thing); + + if (indexByDef >= 0) + listByDef.RemoveAtFastUnordered(indexByDef); + + indexMapByDef.Remove(thingKey); + if (indexByDef < listByDef.Count && indexByDef >= 0) + indexMapByDef[listByDef[indexByDef].GetKey()] = indexByDef; + } + + public static void RemoveFromGroupList(ListerThings lister, Thing thing, ThingRequestGroup thingRequestGroup) + { + var indexMapByGroup = lister.IndexMapByGroup(); + var listByGroup = lister.listsByGroup[(int)thingRequestGroup]; + + var groupMapKey = new GroupThingPair(thingRequestGroup, thing.GetKey()); + + var indexByGroup + = indexMapByGroup.TryGetValue(groupMapKey, out var knownIndexByGroup) + && knownIndexByGroup < listByGroup.Count + && listByGroup[knownIndexByGroup] == thing + ? knownIndexByGroup + : listByGroup.LastIndexOf(thing); + + if (indexByGroup >= 0) + listByGroup.RemoveAtFastUnordered(indexByGroup); + + indexMapByGroup.Remove(groupMapKey); + if (indexByGroup < listByGroup.Count && indexByGroup >= 0) + indexMapByGroup[new(thingRequestGroup, listByGroup[indexByGroup].GetKey())] = indexByGroup; + + lister.stateHashByGroup[(uint)thingRequestGroup]++; + } + + public sealed class ContainsPatch : FishPrepatch + { + public override string? Description { get; } + = "Optimization utilizing a cache for faster Contains checks"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerThings), nameof(ListerThings.Contains)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static bool ReplacementBody(ListerThings __instance, Thing t) + => __instance.IndexMapByDef().ContainsKey(t.GetKey()) + || (__instance.listsByDef.TryGetValue(t.def)?.Contains(t) ?? false); + } + + public sealed class ClearPatch : FishPrepatch + { + public override string? Description { get; } + = "Required to keep the ListerThings cache synced"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ListerThings), nameof(ListerThings.Clear)); + + public static void Postfix(ListerThings __instance) + { + __instance.IndexMapByDef().Clear(); + __instance.IndexMapByGroup().Clear(); + } + } +} + +public sealed class Things : ClassWithFishPatches +{ + public sealed class ThingsMatching_Patch : FirstPriorityFishPatch + { + public override List LinkedPatches { get; } = [typeof(ThingFilterPatches.BestThingRequest_Patch)]; + public override string Description { get; } = "Part of the ThingFilter.BestThingRequest optimization. Required by that particular patch and gets " + "toggled alongside it"; - public override bool Enabled - { - set - { - if (base.Enabled == value) - return; - - Get().Enabled = base.Enabled = value; - } - } - public override Expression TargetMethod { get; } = static () => new ListerThings(default).ThingsMatching(default); @@ -61,7 +230,7 @@ public class Things : ClassWithFishPatches private static Dictionary, List>? _thingsOfDefsDictionary; #if disabled - public class ThingRequest_IsUndefined_Patch : FishPatch + public sealed class ThingRequest_IsUndefined_Patch : FishPatch { public override MethodBase TargetMethodInfo { get; } = AccessTools.PropertyGetter(typeof(ThingRequest), nameof(ThingRequest.IsUndefined)); @@ -79,7 +248,7 @@ public class Things : ClassWithFishPatches => __instance.group == ThingRequestGroup.Undefined && !((FishCache)__instance.singleDef).IsSingleDef; } - public class ThingRequest_CanBeFoundInRegion_Patch : FirstPriorityFishPatch + public sealed class ThingRequest_CanBeFoundInRegion_Patch : FirstPriorityFishPatch { public override MethodBase TargetMethodInfo { get; } = AccessTools.PropertyGetter(typeof(ThingRequest), nameof(ThingRequest.CanBeFoundInRegion)); @@ -99,7 +268,7 @@ public class Things : ClassWithFishPatches && (__instance.group == ThingRequestGroup.Nothing || __instance.group.StoreInRegion())); } - public class Add_Patch : FishPatch + public sealed class Add_Patch : FishPatch { public override Expression TargetMethod { get; } = static () => new ListerThings(default).Add(null); @@ -112,7 +281,7 @@ public class Things : ClassWithFishPatches } } - public class Remove_Patch : FishPatch + public sealed class Remove_Patch : FishPatch { public override Expression TargetMethod { get; } = static () => new ListerThings(default).Remove(null); @@ -125,7 +294,7 @@ public class Things : ClassWithFishPatches } } - public class Clear_Patch : FishPatch + public sealed class Clear_Patch : FishPatch { public override Expression TargetMethod { get; } = static () => new ListerThings(default).Clear(); @@ -138,7 +307,7 @@ public class Things : ClassWithFishPatches } } - public class ThingsMatching_Patch : FirstPriorityFishPatch + public sealed class ThingsMatching_Patch : FirstPriorityFishPatch { public override string Description { get; } = "Part of the work scanning optimization"; diff --git a/Source/PerformanceFish/Log.cs b/Source/PerformanceFish/Log.cs index fd9a4d0..7af0333 100644 --- a/Source/PerformanceFish/Log.cs +++ b/Source/PerformanceFish/Log.cs @@ -3,11 +3,11 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; using JetBrains.Annotations; -using PerformanceFish.Pools; +using FisheryLib.Pools; using PerformanceFish.Prepatching; namespace PerformanceFish; @@ -93,6 +93,7 @@ public static class Log public static UnhandledMessageQueue? UnhandledMessages { get; } = SetupUnhandledMessageQueue(); + [SuppressMessage("Reliability", "CA2000")] private static UnhandledMessageQueue? SetupUnhandledMessageQueue() { if (Ready) @@ -105,8 +106,7 @@ public static class Log return stashedMessages; } - stash.UnhandledMessages = stashedMessages = new(); - stash.MessageQueueGCHandle = GCHandle.Alloc(stashedMessages, GCHandleType.Pinned); + stash.MessageQueueGCHandle = GCHandle.Alloc(new UnhandledMessageQueue(), GCHandleType.Pinned); return stash.UnhandledMessages; } @@ -114,7 +114,7 @@ public static class Log } [StructLayout(LayoutKind.Sequential)] -public struct LogMessageData : IDisposable +public record struct LogMessageData : IDisposable { public IntPtr Text, StackTrace; @@ -188,18 +188,19 @@ public unsafe class UnhandledMessageQueue : IDisposable else { _arrayLength <<= 1; - _messages = (LogMessageData*)Marshal.ReAllocHGlobal((IntPtr)_messages, (IntPtr)_arrayLength); + _messages = (LogMessageData*)Marshal.ReAllocHGlobal((IntPtr)_messages, + (IntPtr)(_arrayLength * sizeof(LogMessageData))); } } // ReSharper disable once InconsistentlySynchronizedField - private void ReleaseUnmanagedResources() => Marshal.FreeHGlobal((IntPtr)_messages); + protected virtual void Dispose(bool disposing) => Marshal.FreeHGlobal((IntPtr)_messages); public void Dispose() { - ReleaseUnmanagedResources(); + Dispose(true); GC.SuppressFinalize(this); } - ~UnhandledMessageQueue() => ReleaseUnmanagedResources(); + ~UnhandledMessageQueue() => Dispose(false); } \ No newline at end of file diff --git a/Source/PerformanceFish/LogPatches.cs b/Source/PerformanceFish/LogPatches.cs index ad708f3..d2ac7f7 100644 --- a/Source/PerformanceFish/LogPatches.cs +++ b/Source/PerformanceFish/LogPatches.cs @@ -12,26 +12,28 @@ using FieldAttributes = Mono.Cecil.FieldAttributes; namespace PerformanceFish; -public class LogPatches : ClassWithFishPrepatches +public sealed class LogPatches : ClassWithFishPrepatches { public const string PERFORMANCE_FISH_WELCOME_MESSAGE = "Performance Fish!!!"; public const string PERFORMANCE_FISH_WELCOME_STACKTRACE = "UwU"; // This breaks my FishStash somehow // TODO: FIX - // public class LogVersionNumberPatch : FishPrepatch + // public sealed class LogVersionNumberPatch : FishPrepatch // { // public override MethodBase MethodBase { get; } = methodof(Root.CheckGlobalInit); // // public static void Prefix() => Log.Message("Performance Fish???"); // } - public class ClassPatch : FishClassPrepatch + public sealed class ClassPatch : FishClassPrepatch { public override string? Description { get; } = "Important logging patch"; public override bool Enabled => true; + public override bool ShowSettings => false; + public override Type Type => typeof(Verse.Log); public override void FreePatch(TypeDefinition typeDefinition) @@ -49,7 +51,7 @@ public class LogPatches : ClassWithFishPrepatches staticConstructorBody.GetILProcessor().InsertRange(staticConstructorBody.Instructions.Count - 2, (OpCodes.Ldc_I8, FishStash.StaticHandle), (OpCodes.Newobj, - module.ImportReference(typeof(FishStashHandle).GetConstructor(new[] { typeof(long) }))), + module.ImportReference(typeof(FishStashHandle).GetConstructor([typeof(long)]))), (OpCodes.Stsfld, fishStashHandleField), (OpCodes.Call, module.ImportReference(InsertEarlyMessages))); } diff --git a/Source/PerformanceFish/MapPrepatches.cs b/Source/PerformanceFish/MapPrepatches.cs new file mode 100644 index 0000000..608e936 --- /dev/null +++ b/Source/PerformanceFish/MapPrepatches.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using PerformanceFish.Prepatching; + +namespace PerformanceFish; + +public sealed class MapPrepatches : ClassWithFishPrepatches +{ + public sealed class FinalizeLoadingPatch : FishPrepatch + { + public override string? Description { get; } + = "An attempt at logging and fixing possible causes for the common FMOD error spam. None of " + + "Performance Fish's other patches interact with this in any way."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(Map), nameof(Map.FinalizeLoading)); + + public static void Postfix(Map __instance) + { + var listerThings = __instance.listerThings; + + var musicalInstruments = listerThings.ThingsInGroup(ThingRequestGroup.MusicalInstrument); + for (var i = musicalInstruments.Count; i-- > 0;) + { + var thing = musicalInstruments[i]; + if (thing is Building_MusicalInstrument) + continue; + + Log.Error($"Map '{__instance}' contains thing that is not of type 'Building_MusicalInstrument' " + + $"in ThingsInGroup for ThingRequestGroup.MusicalInstrument: '{ + thing}' at cell '{thing.Position}'. Removing now to prevent further issues."); + + if (!RefundOrRemoveIfUnassignable(__instance, thing)) + listerThings.RemoveFromGroup(thing, ThingRequestGroup.MusicalInstrument); + } + + var musicSources = listerThings.ThingsInGroup(ThingRequestGroup.MusicSource); + for (var i = musicSources.Count; i-- > 0;) + { + var thing = musicSources[i]; + if (thing.TryGetComp() != null) + continue; + + Log.Error($"Map '{__instance}' contains thing without comp of type 'CompPlaysMusic' in " + + $"ThingsInGroup for ThingRequestGroup.MusicSource: '{ + thing}' at cell '{thing.Position}'. Removing now to prevent further issues."); + + if (!RefundOrRemoveIfUnassignable(__instance, thing)) + listerThings.RemoveFromGroup(thing, ThingRequestGroup.MusicSource); + } + + var allThings = listerThings.AllThings; + for (var i = allThings.Count; i-- > 0;) + { + var thing = allThings[i]; + var thingDef = thing.def; + + if (thingDef is null) + { + Log.Error($"Map '{__instance}' contains thing '{thing}' with null def at cell '{ + thing.Position}'. This will likely cause issues. Removing now."); + + listerThings.Remove(thing); + // continue; + } + // else if (thing.GetType().IsAssignableTo(thingDef.thingClass)) + // { + // Log.Error($"Map '{__instance}' contains thing '{thing}' of incorrect type '{ + // thing.GetType()}' at cell '{thing.Position}'. This should be '{ + // thingDef.thingClass}' instead and will likely cause issues. Destroying and refunding now."); + // } + // else + // { + // continue; + // } + // + // if (!TryRefundOrRemove(__instance, thing)) + // listerThings.Remove(thing); + } + } + + private static bool RefundOrRemoveIfUnassignable(Map map, Thing thing) + { + if (thing.def != null && thing.GetType().IsAssignableTo(thing.def.thingClass)) + return false; + + return TryRefundOrRemove(map, thing); + } + + private static bool TryRefundOrRemove(Map map, Thing thing) + { + var previousAllowDestroy = Thing.allowDestroyNonDestroyable; + Thing.allowDestroyNonDestroyable = true; + + try + { + GenSpawn.Refund(thing, map, CellRect.Empty); + } + catch (Exception refundException) + { + Log.Error($"Failed to refund thing '{thing}'. Destroying.\n{refundException}"); + try + { + thing.Destroy(); + } + catch (Exception destroyException) + { + Log.Error($"Failed to destroy thing '{thing}'.\n{destroyException}"); + return false; + } + } + finally + { + Thing.allowDestroyNonDestroyable = previousAllowDestroy; + } + + return true; + } + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/MassUtilityCaching.cs b/Source/PerformanceFish/MassUtilityCaching.cs index fd11929..db204df 100644 --- a/Source/PerformanceFish/MassUtilityCaching.cs +++ b/Source/PerformanceFish/MassUtilityCaching.cs @@ -9,9 +9,9 @@ using InventoryMassCache namespace PerformanceFish; -public class MassUtilityCaching : ClassWithFishPatches +public sealed class MassUtilityCaching : ClassWithFishPatches { - public class GearMass_Patch : FirstPriorityFishPatch + public sealed class GearMass_Patch : FirstPriorityFishPatch { public override string Description { get; } = "Caches pawn gear mass. Hauling jobs check this quite a lot"; public override Delegate TargetMethodGroup { get; } = MassUtility.GearMass; @@ -41,7 +41,7 @@ public class MassUtilityCaching : ClassWithFishPatches => GearMassCache.GetExistingReference(p).Update(__result, p); } - public class InventoryMass_Patch : FirstPriorityFishPatch + public sealed class InventoryMass_Patch : FirstPriorityFishPatch { public override string Description { get; } = "Caches pawn inventory mass. Hauling jobs check this quite a lot"; public override Delegate TargetMethodGroup { get; } = MassUtility.InventoryMass; diff --git a/Source/PerformanceFish/MeditationUtilityCaching.cs b/Source/PerformanceFish/MeditationUtilityCaching.cs index fd738fd..1fa4a09 100644 --- a/Source/PerformanceFish/MeditationUtilityCaching.cs +++ b/Source/PerformanceFish/MeditationUtilityCaching.cs @@ -10,9 +10,9 @@ using PsyfocusGainCache namespace PerformanceFish; // Suggested by https://github.com/notfood -public class MeditationUtilityCaching : ClassWithFishPatches +public sealed class MeditationUtilityCaching : ClassWithFishPatches { - public class PsyfocusGainPerTick : FirstPriorityFishPatch + public sealed class PsyfocusGainPerTick : FirstPriorityFishPatch { public override string? Description { get; } = "Caches psyfocus gain per tick while the target remains " + "constant. Impactful in psycasting focused playthroughs"; diff --git a/Source/PerformanceFish/MiscOptimizations.cs b/Source/PerformanceFish/MiscOptimizations.cs index 75a4d2d..a817122 100644 --- a/Source/PerformanceFish/MiscOptimizations.cs +++ b/Source/PerformanceFish/MiscOptimizations.cs @@ -12,9 +12,9 @@ using static System.Reflection.Emit.OpCodes; namespace PerformanceFish; -public class MiscPrepatchedOptimizations : ClassWithFishPrepatches +public sealed class MiscPrepatchedOptimizations : ClassWithFishPrepatches { - public class GenList_ListFullCopy : FishPrepatch + public sealed class GenList_ListFullCopy : FishPrepatch { public override string Description { get; } = "Optimization for a basic and frequently used list copying method. Replaces a for loop with an " @@ -32,7 +32,7 @@ public class MiscPrepatchedOptimizations : ClassWithFishPrepatches public static List ListFullCopy(List source) => source.Copy(); } - public class GenCollection_FirstOrDefault : FishPrepatch + public sealed class GenCollection_FirstOrDefault : FishPrepatch { public override string? Description { get; } = "Minor optimization for a RimWorld method used for list lookups, by looping in a more efficient manner " @@ -46,11 +46,28 @@ public class MiscPrepatchedOptimizations : ClassWithFishPrepatches [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T? FirstOrDefault(List list, Predicate predicate) => list.FirstOrDefaultFast(predicate); } + + public sealed class GenGrid_InBoundsPatch : FishPrepatch + { + public override string? Description { get; } = "Tiny optimization by fixing an incorrect cast"; + + public override MethodBase TargetMethodBase => methodof((Func)GenGrid.InBounds); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ReplacementBody(IntVec3 c, Map map) + { + var size = map.Size; + return (uint)c.x < (uint)size.x && (uint)c.z < (uint)size.z; + } + } } -public class MiscOptimizations : ClassWithFishPatches +public sealed class MiscOptimizations : ClassWithFishPatches { - public class WindManager_WindManagerTick : FishPatch + public sealed class WindManager_WindManagerTick : FishPatch { public override string Description { get; } = "Throttles plant sway to update at most once per frame instead of every tick. Also fixes the plant sway " @@ -69,11 +86,10 @@ public class MiscOptimizations : ClassWithFishPatches && codes[i - 3] == find_CurrentMap && codes[i - 1] == this_map && codes[i].operand is Label, // goto return; - static code - => new[] - { - code, FishTranspiler.Call(ShouldSway), FishTranspiler.IfFalse_Short((Label)code.operand) - }); + static code => + [ + code, FishTranspiler.Call(ShouldSway), FishTranspiler.IfFalse_Short((Label)code.operand) + ]); } public static bool ShouldSway() @@ -95,7 +111,7 @@ public class MiscOptimizations : ClassWithFishPatches private static bool _lastPrefValue; } - public class CompRottable : FishPatch + public sealed class CompRottable : FishPatch { public override string? Description { get; } = "Throttles rotting on misconfigured defs to only recalculate every 256 ticks, instead of constantly."; @@ -114,7 +130,7 @@ public class MiscOptimizations : ClassWithFishPatches } } - public class RitualObligationTrigger_Date : FirstPriorityFishPatch + public sealed class RitualObligationTrigger_Date : FirstPriorityFishPatch { public override string? Description { get; } = "Literally just reorders instructions in this method. They were nonsensical, discarding 90% of the " @@ -141,7 +157,7 @@ public class MiscOptimizations : ClassWithFishPatches } } - public class ReportProbablyMissingAttributesFix : FishPatch + public sealed class ReportProbablyMissingAttributesFix : FishPatch { public override string? Description { get; } = """Fixes the "probably needs a StaticConstructorOnStartup attribute" log warning to display full """ @@ -159,7 +175,7 @@ public class MiscOptimizations : ClassWithFishPatches methodof(Reflection.FullDescription)); } - public class DrawBatch_Flush : FishPatch + public sealed class DrawBatch_Flush : FishPatch { public override string Description { get; } = "Optimization of a fleck rendering method. Low - medium performance impact"; @@ -168,7 +184,7 @@ public class MiscOptimizations : ClassWithFishPatches public static CodeInstructions? Transpiler(CodeInstructions codeInstructions, ILGenerator generator) { - var codes = codeInstructions.ToList(); + var codes = codeInstructions.AsOrToList(); var instance_tmpPropertyBlocks = FishTranspiler.Field(typeof(DrawBatch), nameof(DrawBatch.tmpPropertyBlocks)); diff --git a/Source/PerformanceFish/ModCompatibility/ActiveMods.cs b/Source/PerformanceFish/ModCompatibility/ActiveMods.cs index f49b6d5..f67e6c0 100644 --- a/Source/PerformanceFish/ModCompatibility/ActiveMods.cs +++ b/Source/PerformanceFish/ModCompatibility/ActiveMods.cs @@ -12,14 +12,38 @@ public static class ActiveMods // GradientHair = Contains(PackageIDs.GRADIENT_HAIR), GraphicsSetter = Contains(PackageIDs.GRAPHICS_SETTINGS), Harmony = Contains(PackageIDs.HARMONY), + HumanResources = Contains(PackageIDs.HUMAN_RESOURCES), Multiplayer = Contains(PackageIDs.MULTIPLAYER), PerformanceOptimizer = Contains(PackageIDs.PERFORMANCE_OPTIMIZER), Prepatcher = Contains(PackageIDs.PREPATCHER), + UseBestMaterials = Contains(PackageIDs.USE_BEST_MATERIALS), VanillaExpandedFramework = Contains(PackageIDs.VANILLA_EXPANDED_FRAMEWORK); // ModsConfig.IsActive does not ignore _steam postfixes, causing it to fail when local copies were made public static bool Contains(string packageID) => TryGetModMetaData(packageID) != null; - + + public static bool ContainsAnyOf(T packageIDs) where T : IEnumerable + { + foreach (var id in packageIDs) + { + if (Contains(id)) + return true; + } + + return false; + } + + public static bool ContainsAllOf(T packageIDs) where T : IEnumerable + { + foreach (var id in packageIDs) + { + if (!Contains(id)) + return false; + } + + return true; + } + public static ModMetaData? TryGetModMetaData(string packageID) => ModLister.GetActiveModWithIdentifier(packageID, true); @@ -55,7 +79,7 @@ public static class ActiveMods ThrowForMissingModDependency(nameof(Fishery)); if (!Harmony) - ThrowForMissingModDependency(nameof(Harmony)); + WarnForMissingModDependency(nameof(Harmony)); VerifyLoadOrder(nameof(Fishery), PackageIDs.FISHERY, PackageIDs.PERFORMANCE_FISH, PackageIDs.BETTER_LOG); VerifyLoadOrder(nameof(Harmony), PackageIDs.HARMONY, PackageIDs.PERFORMANCE_FISH); @@ -70,14 +94,14 @@ public static class ActiveMods if (activeModsInLoadOrder.FindIndex(mod => mod.PackageId == dependencyID) > activeModsInLoadOrder.FindIndex(mod => dependingModIDs.Contains(mod.PackageId))) { - ThrowForIncorrectLoadOrder(dependencyName); + ErrorForIncorrectLoadOrder(dependencyName); } } private static void CheckFisheryVersion() { if (FisheryLib.FisheryLib.VERSION > FisheryLib.FisheryLib.CurrentlyLoadedVersion) - ThrowForOutdatedModDependency(nameof(Fishery)); + ErrorForOutdatedModDependency(nameof(Fishery)); } private static void CheckHarmonyVersion() @@ -86,23 +110,27 @@ public static class ActiveMods private static void CheckDependencyVersion(string dependencyName, Type dependencyType, string dependencyAssemblyName) { - if (typeof(PerformanceFishMod).Assembly.GetReferencedAssemblies().First(assembly - => assembly.FullName.StartsWith($"{dependencyAssemblyName}, Version", StringComparison.Ordinal)).Version - > dependencyType.Assembly.GetName().Version) + if (typeof(PerformanceFishMod).Assembly.GetReferencedAssemblyVersion(dependencyAssemblyName) + > dependencyType.Assembly.GetLoadedVersion()) { - ThrowForOutdatedModDependency(dependencyName); + ErrorForOutdatedModDependency(dependencyName); } } - private static void ThrowForIncorrectLoadOrder(string dependencyName) - => ThrowHelper.ThrowOperationCanceledException($"Incorrect load order detected. { - dependencyName} must be loaded before any mod depending on it."); + private static void ErrorForIncorrectLoadOrder(string dependencyName) + => Log.Error($"Incorrect load order detected. {dependencyName} must be loaded before any mod depending on it."); - private static void ThrowForOutdatedModDependency(string modName) - => ThrowHelper.ThrowOperationCanceledException($"Outdated {modName} mod detected. { + private static void ErrorForOutdatedModDependency(string modName) + => Log.Error($"Outdated {modName} mod detected. { modName} must be updated for Performance Fish to work correctly"); private static void ThrowForMissingModDependency(string modName) - => ThrowHelper.ThrowOperationCanceledException( - $"{modName} mod is missing. Performance Fish requires this mod in order to function."); + { + Log.Error($"{modName} mod is missing. Performance Fish requires this mod in order to function."); + ThrowHelper.ThrowOperationCanceledException(); + } + + private static void WarnForMissingModDependency(string modName) + => Log.Warning( + $"{modName} mod is missing. Performance Fish may not work correctly without this dependency."); } \ No newline at end of file diff --git a/Source/PerformanceFish/ModCompatibility/PackageIDs.cs b/Source/PerformanceFish/ModCompatibility/PackageIDs.cs index ba653a8..df087af 100644 --- a/Source/PerformanceFish/ModCompatibility/PackageIDs.cs +++ b/Source/PerformanceFish/ModCompatibility/PackageIDs.cs @@ -13,9 +13,11 @@ public static class PackageIDs // GRADIENT_HAIR = "automatic.gradienthair", GRAPHICS_SETTINGS = "Telefonmast.GraphicsSettings", HARMONY = "brrainz.harmony", + HUMAN_RESOURCES = "JPT.HumanResources", MULTIPLAYER = "rwmt.Multiplayer", PERFORMANCE_FISH = "bs.performance", PERFORMANCE_OPTIMIZER = "Taranchuk.PerformanceOptimizer", PREPATCHER = "zetrith.prepatcher", + USE_BEST_MATERIALS = "kathanon.UseBestMaterials", VANILLA_EXPANDED_FRAMEWORK = "OskarPotocki.VanillaFactionsExpanded.Core"; } \ No newline at end of file diff --git a/Source/PerformanceFish/ModCompatibility/PerformanceOptimizer.cs b/Source/PerformanceFish/ModCompatibility/PerformanceOptimizer.cs index 02e1a90..49cfed3 100644 --- a/Source/PerformanceFish/ModCompatibility/PerformanceOptimizer.cs +++ b/Source/PerformanceFish/ModCompatibility/PerformanceOptimizer.cs @@ -20,10 +20,10 @@ public static class PerformanceOptimizer public static string[] BlockedPatchNames { get; } = - { + [ "PerformanceOptimizer.Optimization_FasterGetCompReplacement", "PerformanceOptimizer.Optimization_IdeoUtility_GetStyleDominance" - }; + ]; static PerformanceOptimizer() { diff --git a/Source/PerformanceFish/ModCompatibility/Types.cs b/Source/PerformanceFish/ModCompatibility/Types.cs new file mode 100644 index 0000000..de48ff7 --- /dev/null +++ b/Source/PerformanceFish/ModCompatibility/Types.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace PerformanceFish.ModCompatibility; + +public static class Types +{ + public static class RealRuins + { + public static Type? POIWorldObject = Reflection.Type("RealRuins", "RealRuins.RealRuinsPOIWorldObject"); + } + + public static class VFEAncients + { + public static Type? RecipeExtensionMend = Reflection.Type("VFEAncients", "VFEAncients.RecipeExtension_Mend"); + } + + public static class PerformanceAnalyzer + { + public static Type? + ModBase = Reflection.Type("PerformanceAnalyzer", "Analyzer.Modbase"), + ProfilingEntry = Reflection.Type("PerformanceAnalyzer", "Analyzer.Profiling.Entry"), + Window_Analyzer = Reflection.Type("PerformanceAnalyzer", "Analyzer.Window_Analyzer"); + } + + public static class LWM + { + public static Type? CompProperties = Reflection.Type("LWM.DeepStorage.Properties"); + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/ModCompatibility/VanillaExpandedFixes.cs b/Source/PerformanceFish/ModCompatibility/VanillaExpandedFixes.cs index 1b5f225..bec7210 100644 --- a/Source/PerformanceFish/ModCompatibility/VanillaExpandedFixes.cs +++ b/Source/PerformanceFish/ModCompatibility/VanillaExpandedFixes.cs @@ -3,18 +3,20 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +#if fixed // with commit from Jul 3, 2023 + namespace PerformanceFish.ModCompatibility; -public class VanillaExpandedFixes : ClassWithFishPatches +public sealed class VanillaExpandedFixes : ClassWithFishPatches { - public class ExpandableGraphicData : FishPatch + public sealed class ExpandableGraphicData : FishPatch { public override string? Description { get; } = "Fixes the LoadAllFiles method in Vanilla Expanded Framework's ExpandableGraphicData to not scan through " + "all texture folders of all mods for every graphic and instead just use cached files the game already " + "loaded. Improves load times and prevents dds related errors."; - public override bool Enabled => ActiveMods.VanillaExpandedFramework; + public override bool Enabled => ActiveMods.VanillaExpandedFramework && base.Enabled; public override MethodBase TargetMethodInfo { get; } = AccessTools.Method("VFECore.ExpandableGraphicData:LoadAllFiles"); @@ -27,20 +29,20 @@ public class VanillaExpandedFixes : ClassWithFishPatches var paths = new List(); var mods = LoadedModManager.RunningModsListForReading; var count = mods.Count; + + var prefix = !folderPath.NullOrEmpty() && folderPath![^1] == '/' + ? folderPath + : folderPath + '/'; for (var i = 0; i < count; i++) { - var contentListTrie = mods[i].GetContentHolder().contentListTrie; - - var prefix = !folderPath.NullOrEmpty() && folderPath![^1] == '/' - ? folderPath - : folderPath + '/'; - - foreach (var path in contentListTrie.GetByPrefix(prefix)) + foreach (var path in mods[i].GetContentHolder().contentListTrie.GetByPrefix(prefix)) paths.Add(path); } return paths; } } -} \ No newline at end of file +} + +#endif \ No newline at end of file diff --git a/Source/PerformanceFish/ModListerPatches.cs b/Source/PerformanceFish/ModListerPatches.cs new file mode 100644 index 0000000..1ec73f7 --- /dev/null +++ b/Source/PerformanceFish/ModListerPatches.cs @@ -0,0 +1,63 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; + +namespace PerformanceFish; + +public sealed class ModListerPatches : ClassWithFishPrepatches +{ + public sealed class AnyFromListActivePatch : FishPrepatch + { + public override string? Description { get; } + = "Fixes a LoadFolders bug causing it to normally not recognize steam versions of mods if another local " + + "copy exists."; + + public override MethodBase TargetMethodBase { get; } = methodof(ModLister.AnyFromListActive); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => GetActiveModWithIdentifierFix(ilProcessor, module); + } + + public sealed class ModIncompatibilityIsSatisfiedPatch : FishPrepatch + { + public override string? Description { get; } + = "Fixes a mod manager bug causing it to normally not recognize steam versions of mods for incompatibility " + + "checks if another local copy exists."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredPropertyGetter(typeof(ModIncompatibility), nameof(ModIncompatibility.IsSatisfied)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => GetActiveModWithIdentifierFix(ilProcessor, module); + } + + private static void GetActiveModWithIdentifierFix(ILProcessor ilProcessor, ModuleDefinition module) + { + var instructions = ilProcessor.instructions; + var success = false; + + for (var i = 0; i + 1 < instructions.Count; i++) + { + if (instructions[i].OpCode == OpCodes.Ldc_I4_0 + && instructions[i + 1].Operand is MethodReference + { + Name: nameof(ModLister.GetActiveModWithIdentifier) + }) + { + instructions[i].OpCode = OpCodes.Ldc_I4_1; + success = true; + } + } + + if (!success) + { + Log.Error($"Performance Fish failed to apply its patch on '{ + ilProcessor.GetMethod().FullName}'. This should be harmless as it's meant to be just a bugfix."); + } + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/ModsConfigPatches.cs b/Source/PerformanceFish/ModsConfigPatches.cs new file mode 100644 index 0000000..b6c3ad8 --- /dev/null +++ b/Source/PerformanceFish/ModsConfigPatches.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; + +namespace PerformanceFish; + +public sealed class ModsConfigPatches : ClassWithFishPrepatches +{ + public sealed class AreAllActivePatch : FishPrepatch + { + public override string? Description { get; } + = "Fixes a MayRequire bug causing it to normally not recognize steam versions of mods if another local " + + "copy exists."; + + public override MethodBase TargetMethodBase { get; } = methodof(ModsConfig.AreAllActive); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => IsActiveFix(ilProcessor, module); + } + + public sealed class IsAnyActiveOrEmptyPatch : FishPrepatch + { + public override string? Description { get; } + = "Fixes a MayRequireAnyOf bug causing it to normally not recognize steam versions of mods if another " + + "local copy exists, as well as its inability to strip whitespace."; + + public override MethodBase TargetMethodBase { get; } = methodof(ModsConfig.IsAnyActiveOrEmpty); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => IsActiveFix(ilProcessor, module); + + public static void Prefix(out bool trimNames) => trimNames = true; + } + + private static void IsActiveFix(ILProcessor ilProcessor, ModuleDefinition module) + { + var instructions = ilProcessor.instructions; + var success = false; + + for (var i = 0; i + 1 < instructions.Count; i++) + { + if (instructions[i].Operand is MethodReference { Name: nameof(ModsConfig.IsActive) }) + { + instructions[i].Operand = module.ImportReference(IsActiveReplacement); + success = true; + } + } + + if (!success) + { + Log.Error($"Performance Fish failed to apply its patch on '{ + ilProcessor.GetMethod().FullName}'. This should be harmless as it's meant to be just a bugfix."); + } + } + + public static bool IsActiveReplacement(string id) => ModLister.GetActiveModWithIdentifier(id, true) != null; +} \ No newline at end of file diff --git a/Source/PerformanceFish/OnAssembliesLoaded.cs b/Source/PerformanceFish/OnAssembliesLoaded.cs index 6772263..ad0c1c0 100644 --- a/Source/PerformanceFish/OnAssembliesLoaded.cs +++ b/Source/PerformanceFish/OnAssembliesLoaded.cs @@ -32,6 +32,8 @@ public static class OnAssembliesLoaded TryInitialize(AnalyzerFixes.Patch, true); _ = FishSettings.Instance; + + PerformanceFishMod.AllPrepatchClasses.BeforeHarmonyPatching(); Parallel.ForEach(PerformanceFishMod.AllPatchClasses, static patchClass => { @@ -46,6 +48,10 @@ public static class OnAssembliesLoaded patchClass.GetType().Name}:\n{ex}"); } }); + + PerformanceFishMod.AllPrepatchClasses.OnPatchingCompleted(); + + Cache.Utility.Initialize(); } private static void TryPatchGraphicsSetter() diff --git a/Source/PerformanceFish/ParallelNoAlloc.cs b/Source/PerformanceFish/ParallelNoAlloc.cs new file mode 100644 index 0000000..96dc308 --- /dev/null +++ b/Source/PerformanceFish/ParallelNoAlloc.cs @@ -0,0 +1,308 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Diagnostics.CodeAnalysis; +using FisheryLib.Pools; +using JetBrains.Annotations; +using Mono.Cecil; + +namespace PerformanceFish; + +public static class ParallelNoAlloc +{ + private static FishTable> _groupWorkers = new(); + + public static void Invoke(Action action) => FishPool.Get().Invoke(action); + + public static void Invoke(Action[] actions) + { + for (var i = 0; i < actions.Length; i++) + Invoke(actions[i]); + } + + public static void Invoke(Span actions) + { + for (var i = 0; i < actions.Length; i++) + Invoke(actions[i]); + } + + public static object RegisterBackgroundWaitingWorkers(Action[] actions) + { + Guard.IsGreaterThanOrEqualTo(actions.Length, 0); + + var monitorObject = new MonitorObject.Group(new Worker[actions.Length]); + var workerGroup = new FishSet(); + + for (var i = 0; i < actions.Length; i++) + workerGroup.Add(monitorObject.Subscribers[i] = new Worker.Continuous(monitorObject, actions[i])); + + lock (_groupWorkers) + _groupWorkers.Add(monitorObject, workerGroup); + + return monitorObject; + } + + public static void RegisterBackgroundWaitingWorkers(object monitorObject, Action[] actions) + { + Guard.IsGreaterThanOrEqualTo(actions.Length, 0); + + if (monitorObject is not MonitorObject.Group typedMonitorObject) + { + ThrowHelper.ThrowArgumentException(); + return; + } + + lock (_groupWorkers) + { + var workerGroup = _groupWorkers.GetOrAdd(typedMonitorObject); + + for (var i = 0; i < actions.Length; i++) + { + var worker = new Worker.Continuous(typedMonitorObject, actions[i]); + typedMonitorObject.Subscribe(worker); + workerGroup.Add(worker); + } + } + } + + public static void RegisterBackgroundWaitingWorker(object monitorObject, Action action) + { + Guard.IsNotNull(action); + + if (monitorObject is not MonitorObject.Group typedMonitorObject) + { + ThrowHelper.ThrowArgumentException(); + return; + } + + var worker = new Worker.Continuous(typedMonitorObject, action); + + lock (_groupWorkers) + { + typedMonitorObject.Subscribe(worker); + _groupWorkers.GetOrAdd(typedMonitorObject).Add(worker); + } + } + + public static object RegisterBackgroundWaitingWorker(Action action) + { + Guard.IsNotNull(action); + + var monitorObject = new MonitorObject.Group(Array.Empty()); + var worker = new Worker.Continuous(monitorObject, action); + monitorObject.Subscribe(worker); + + lock (_groupWorkers) + _groupWorkers.GetOrAdd(monitorObject).Add(worker); + + return monitorObject; + } + + public static void DeregisterBackgroundWaitingWorker(object monitorObject, Action action) + { + Guard.IsNotNull(action); + + if (monitorObject is not MonitorObject.Group typedMonitorObject) + { + ThrowHelper.ThrowArgumentException(); + return; + } + + lock (_groupWorkers) + { + var workerSet = _groupWorkers[typedMonitorObject]!; + + foreach (var worker in workerSet) + { + if (worker.Action != action) + continue; + + workerSet.Remove(worker); + typedMonitorObject.Unsubscribe(worker); + return; + } + } + + ThrowHelper.ThrowInvalidOperationException($"No worker found to deregister from for object { + monitorObject} and action {action.Method.FullDescription()}"); + } + + public static void PulseRegisteredBackgroundWorkers(object monitorObject) + { + if (monitorObject is not MonitorObject.Group typedMonitorObject) + { + ThrowHelper.ThrowArgumentException(); + return; + } + + typedMonitorObject.Pulse(); + } + + private abstract class Worker : IFishPoolable + { + public Thread Thread { get; private set; } + + public MonitorObject MonitorObject { get; private set; } + + public bool Pulsed + { + get => _pulsed; + set => _pulsed = value; + } + + public Action? Action { get; private set; } + private bool _cancelled; + private volatile bool _pulsed; + + protected Worker() => Initialize(new MonitorObject.Single(this)); + + protected Worker(MonitorObject monitorObject) => Initialize(monitorObject); + + [MemberNotNull(nameof(Thread)), MemberNotNull(nameof(MonitorObject))] + private void Initialize(MonitorObject monitorObject) + { + Guard.IsNotNull(monitorObject); + MonitorObject = monitorObject; + Thread = new(Callback, 131072); + Thread.Start(); + } + + protected Worker(MonitorObject monitorObject, Action action) : this(monitorObject) + => Action = action; + + public void Cancel() => _cancelled = true; + + private void Callback() + { + while (true) + { + lock (MonitorObject) + { + while (!Pulsed) + Monitor.Wait(MonitorObject); + } + + InvokeAction(); + Pulsed = false; + + Thread.MemoryBarrier(); + if (_cancelled) + return; + + Return(); + } + } + + void IFishPoolable.Reset() + { + + } + + protected abstract void InvokeAction(); + + protected abstract void Return(); + + [UsedImplicitly] + public sealed class Single : Worker + { + protected override void InvokeAction() + { + Action?.Invoke(); + Action = null; + } + + protected override void Return() => FishPool.Return(this); + + public void Invoke(Action action) + { + Guard.IsNotAssignableToType(MonitorObject); + + if (Pulsed) + Spin(); + + Action = action; + + MonitorObject.Pulse(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void Spin() + { + var spinWait = new SpinWait(); + + while (Pulsed) + { + Thread.MemoryBarrier(); + spinWait.SpinOnce(); + } + } + } + + public sealed class Continuous : Worker + { + public Continuous(MonitorObject monitorObject, Action action) : base(monitorObject, action) + => Guard.IsNotNull(action); + + protected override void InvokeAction() => Action!.Invoke(); + + protected override void Return() + { + } + } + } + + private abstract class MonitorObject + { + public abstract void Pulse(); + + public sealed class Single(Worker worker) : MonitorObject + { + public Worker Worker { get; } = worker; + + public override void Pulse() + { + lock (this) + { + Worker.Pulsed = true; + Thread.MemoryBarrier(); + Monitor.Pulse(this); + } + } + } + + public sealed class Group(Worker[] subscribers) : MonitorObject + { + public Worker[] Subscribers => subscribers; + + public override void Pulse() + { + lock (this) + { + for (var i = subscribers.Length; i-- > 0;) + subscribers[i].Pulsed = true; + + Thread.MemoryBarrier(); + Monitor.PulseAll(this); + } + } + + public void Subscribe(Worker worker) => subscribers = subscribers.Add(worker); + + public void Unsubscribe(Worker worker) + { + Guard.IsGreaterThan(Subscribers.Length, 0); + + var index = Array.IndexOf(Subscribers, worker); + Guard.IsGreaterThanOrEqualTo(index, 0); + + var subscribersLength = subscribers.Length; + if (index != subscribersLength - 1) + Array.Copy(subscribers, index + 1, subscribers, index, subscribersLength - index - 1); + + Array.Resize(ref subscribers, subscribersLength - 1); + } + } + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Patching/ClassWithFishPatches.cs b/Source/PerformanceFish/Patching/ClassWithFishPatches.cs index 83f4908..f212ef4 100644 --- a/Source/PerformanceFish/Patching/ClassWithFishPatches.cs +++ b/Source/PerformanceFish/Patching/ClassWithFishPatches.cs @@ -15,5 +15,19 @@ public abstract class ClassWithFishPatches : SingletonFactory false; + public virtual bool ShowSettings + { + get + { + foreach (var patch in Patches) + { + if (patch.ShowSettings) + return true; + } + + return false; + } + } + protected ClassWithFishPatches() => Patches = new(GetType()); } \ No newline at end of file diff --git a/Source/PerformanceFish/Patching/FishPatch.cs b/Source/PerformanceFish/Patching/FishPatch.cs index 540f6da..765d121 100644 --- a/Source/PerformanceFish/Patching/FishPatch.cs +++ b/Source/PerformanceFish/Patching/FishPatch.cs @@ -8,6 +8,8 @@ using System.Diagnostics; using System.Linq; using nuget::JetBrains.Annotations; using PerformanceFish.Cache; +using PerformanceFish.ModCompatibility; +using PerformanceFish.Prepatching; namespace PerformanceFish.Patching; @@ -15,11 +17,36 @@ namespace PerformanceFish.Patching; [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors | ImplicitUseTargetFlags.WithMembers)] public abstract class FishPatch : SingletonFactory, IExposable, IHasDescription { + private bool _enabled; + + private string? _descriptionWithNotes; + + private FishPrepatchBase[]? _resolvedLinkedPrepatches; + + private FishPatch[]? _resolvedLinkedHarmonyPatches; + + public virtual List IncompatibleModIDs { get; } = []; + + public virtual List LinkedPatches { get; } = []; + + private FishPrepatchBase[] ResolvedLinkedPrepatches + => _resolvedLinkedPrepatches ??= LinkedPatches + .Where(static type => type.IsAssignableTo(typeof(FishPrepatchBase))) + .Select(static type => FishPrepatchBase.Get(type)) + .ToArray(); + + private FishPatch[] ResolvedLinkedHarmonyPatches + => _resolvedLinkedHarmonyPatches ??= LinkedPatches + .Where(static type => type.IsAssignableTo(typeof(FishPatch))).Select(static type => Get(type)) + .ToArray(); + + public bool DisabledForModCompatibility => ActiveMods.ContainsAnyOf(IncompatibleModIDs); + public virtual bool ShouldBenchmark => false; public virtual bool Enabled { - get => _enabled; + get => _enabled && !DisabledForModCompatibility; set { if (value == _enabled) @@ -37,30 +64,45 @@ public abstract class FishPatch : SingletonFactory, IExposable, IHasD TryUnpatch(); ShouldBePatched = shouldBePatched; } + + for (var i = 0; i < ResolvedLinkedPrepatches.Length; i++) + ResolvedLinkedPrepatches[i].Enabled = value; + + for (var i = 0; i < ResolvedLinkedHarmonyPatches.Length; i++) + ResolvedLinkedHarmonyPatches[i].Enabled = value; } } - private bool _enabled; - public virtual bool DefaultState { [Pure] get => true; } + public virtual bool ShowSettings => true; + public virtual int PrefixMethodPriority => TryGetPriority(PrefixMethodInfo); public virtual int PostfixMethodPriority => TryGetPriority(PostfixMethodInfo); public virtual int TranspilerMethodPriority => TryGetPriority(TranspilerMethodInfo); public virtual int FinalizerMethodPriority => TryGetPriority(FinalizerMethodInfo); public MethodInfo? HarmonyMethodInfo => HarmonyMethodInfos.FirstOrDefault(); - public List HarmonyMethodInfos { get; } = new(); + public List HarmonyMethodInfos { get; } = []; public MethodInfo? BenchmarkHarmonyMethodInfo => BenchmarkHarmonyMethodInfos.FirstOrDefault(); - public List BenchmarkHarmonyMethodInfos { get; } = new(); + public List BenchmarkHarmonyMethodInfos { get; } = []; private bool Patched { get; set; } private bool ShouldBePatched { get; set; } public virtual string? Name => null; + public virtual string? Description => null; + public string? DescriptionWithNotes + => _descriptionWithNotes ??= !DisabledForModCompatibility + ? Description + : Description + + $"{(Description?.EndsWith(".") ?? true ? "" : ".")} Disabled for compatibility with { + IncompatibleModIDs.Select(static id => ActiveMods.TryGetModMetaData(id)?.Name) + .Where(static id => id != null).ToCommaList(true)}"; + public virtual Delegate? TargetMethodGroup => null; public virtual IEnumerable? TargetMethodGroups => null; public virtual Expression? TargetMethod => null; @@ -76,8 +118,8 @@ public abstract class FishPatch : SingletonFactory, IExposable, IHasD ??= (TargetMethods?.Select(SymbolExtensions.GetMethodInfo) ?? TargetMethodGroups?.Select(static m => m.Method))?.Cast().ToArray() ?? (TargetMethodInfo != null! - ? new[] { TargetMethodInfo } - : Array.Empty()); + ? [TargetMethodInfo] + : Array.Empty()); private MethodBase[]? _targetMethodInfos; @@ -102,7 +144,9 @@ public abstract class FishPatch : SingletonFactory, IExposable, IHasD public virtual MethodInfo? FinalizerMethodInfo => TryGetMethod("FINALIZER", static m => m.HasAttribute()); - internal Type ParentType => GetType().DeclaringType; + internal Type ParentType + => GetType().DeclaringType + ?? ThrowHelper.ThrowArgumentNullException($"{this} is missing parent class. This is not supported"); internal IHasFishPatch? ParentClass => PerformanceFishMod.AllPatchClasses?.FirstOrDefault(patchClass => patchClass.GetType() == ParentType); @@ -249,10 +293,14 @@ public abstract class FishPatch : SingletonFactory, IExposable, IHasD private static int TryGetPriority(MethodInfo? info) => info?.TryGetAttribute()?.info.priority ?? Priority.Normal; + + protected internal virtual void OnPatchingCompleted() + { + } public virtual void ExposeData() => Scribe_Values.Look(ref _enabled, "enabled", DefaultState); - public override string ToString() => GetType().FullName; + public override string ToString() => GetType().FullName ?? GetType().Name; } internal static class Benchmarking @@ -317,7 +365,7 @@ internal static class Benchmarking } } - public class Results + public sealed class Results { public int Count; public Stopwatch? Stopwatch; diff --git a/Source/PerformanceFish/Patching/FishPatchExtensions.cs b/Source/PerformanceFish/Patching/FishPatchExtensions.cs deleted file mode 100644 index b8c00ef..0000000 --- a/Source/PerformanceFish/Patching/FishPatchExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2023 bradson -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -namespace PerformanceFish.Patching; - -public static class FishPatchExtensions -{ - public static void PatchAll(this IEnumerable patches) - { - foreach (var patch in patches) - patch.TryPatch(); - } - - public static void UnpatchAll(this IEnumerable patches) - { - foreach (var patch in patches) - patch.TryUnpatch(); - } -} \ No newline at end of file diff --git a/Source/PerformanceFish/Patching/FishPatchHolder.cs b/Source/PerformanceFish/Patching/FishPatchHolder.cs index 9ea9029..77c9e2b 100644 --- a/Source/PerformanceFish/Patching/FishPatchHolder.cs +++ b/Source/PerformanceFish/Patching/FishPatchHolder.cs @@ -13,12 +13,33 @@ namespace PerformanceFish.Patching; [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] public class FishPatchHolder : IExposable, IEnumerable { + private volatile int _completedPatchingOnce; + public ConcurrentDictionary All { get; } = new(); public T Get() where T : FishPatch => (T)All[typeof(T)]; public void Add(FishPatch patch) => All[patch.GetType()] = patch; - public void PatchAll() => All.Values.PatchAll(); - public void UnpatchAll() => All.Values.UnpatchAll(); + + public void PatchAll() + { + using var patches = new PooledArray(All.Values); + + for (var i = 0; i < patches.Length; i++) + patches[i].TryPatch(); + + if (Interlocked.Exchange(ref _completedPatchingOnce, 1) == 0) + { + for (var i = 0; i < patches.Length; i++) + patches[i].OnPatchingCompleted(); + } + } + + public void UnpatchAll() + { + foreach (var patch in All.Values) + patch.TryUnpatch(); + } + public FishPatch this[Type type] => All[type]; public FishPatchHolder(Type type) @@ -62,8 +83,8 @@ public class FishPatchHolder : IExposable, IEnumerable patchClass.Patches[type].Enabled = false; patchClass.Patches.All.TryRemove(type, out _); - Log.Warning($"Performance Fish removed a duplicate patch from {patchClass.GetType().FullName - }. This is likely caused by no longer valid mod configs"); + Log.Warning($"Performance Fish removed a duplicate patch from { + patchClass.GetType().FullName}. This is likely caused by no longer valid mod configs"); } } diff --git a/Source/PerformanceFish/Patching/IHasFishPatch.cs b/Source/PerformanceFish/Patching/IHasFishPatch.cs index 92f617b..e5e2f21 100644 --- a/Source/PerformanceFish/Patching/IHasFishPatch.cs +++ b/Source/PerformanceFish/Patching/IHasFishPatch.cs @@ -13,4 +13,5 @@ public interface IHasFishPatch { public FishPatchHolder Patches { get; } public bool RequiresLoadedGameForPatching { get; } + public bool ShowSettings { get; } } \ No newline at end of file diff --git a/Source/PerformanceFish/PerformanceFish.csproj b/Source/PerformanceFish/PerformanceFish.csproj index cdd2203..52be3fb 100644 --- a/Source/PerformanceFish/PerformanceFish.csproj +++ b/Source/PerformanceFish/PerformanceFish.csproj @@ -16,9 +16,12 @@ 5 enable True - v1.4;v1.3 - 0.3.800.85 - 1701;1702;IL2121;CS8500;CS9082 + v1.4 + 0.3.999.7 + + 1701;1702;IL2121;CS8500;CS9082;CA1000;CA1002;CA1003;CA1707;CA1010;CA1715;CA1716;CA1724;CA1031;CA1033; + CA1034;CA1043;CA1051;CA1305;CA1310;CA1710;CA1711;CA1810;CA1812;CA1819;CA1851;CA2002;CA5394;CA1725;CA1062 + @@ -29,10 +32,11 @@ ..\..\..\Fishery\1.4\Assemblies\1Fishery.dll + False - + - + - + diff --git a/Source/PerformanceFish/PerformanceFish.sln b/Source/PerformanceFish/PerformanceFish.sln index d3a2dbe..9ca49fa 100644 --- a/Source/PerformanceFish/PerformanceFish.sln +++ b/Source/PerformanceFish/PerformanceFish.sln @@ -8,6 +8,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6948F7E6-7ED7-4716-87EF-CFF1F465F561}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + ..\..\1.4\Defs\GasDefs.xml = ..\..\1.4\Defs\GasDefs.xml + ..\..\1.4\Defs\MainButtonDefs.xml = ..\..\1.4\Defs\MainButtonDefs.xml EndProjectSection EndProject Global diff --git a/Source/PerformanceFish/PerformanceFishMod.cs b/Source/PerformanceFish/PerformanceFishMod.cs index f61b181..894a663 100644 --- a/Source/PerformanceFish/PerformanceFishMod.cs +++ b/Source/PerformanceFish/PerformanceFishMod.cs @@ -41,27 +41,55 @@ using PerformanceFish.Prepatching; namespace PerformanceFish; [PublicAPI] -public class PerformanceFishMod : Mod +public sealed class PerformanceFishMod : Mod { - public const string NAME = "Performance Fish"; - public const string PACKAGE_ID = PackageIDs.PERFORMANCE_FISH; + public const string + NAME = "Performance Fish", + PACKAGE_ID = PackageIDs.PERFORMANCE_FISH; + + [Obsolete("I am not incrementing this version number")] public const decimal VERSION = 0.4M; + public static Version LoadedVersion => typeof(PerformanceFishMod).Assembly.GetLoadedVersion(); + public static FishSettings? Settings { get; internal set; } public static PerformanceFishMod? Mod { get; private set; } public static Harmony Harmony { get; private set; } = new(PACKAGE_ID); public static IHasFishPatch[]? AllPatchClasses { get; internal set; } public static ClassWithFishPrepatches[]? AllPrepatchClasses { get; internal set; } + private static FishPatch[]? _allFishPatches; + + private static FishPrepatchBase[]? _allFishPrepatches; + + public static IEnumerable AllHarmonyPatches + => _allFishPatches ??= AllPatchClasses?.SelectMany(static patchClass => patchClass.Patches.All.Values).ToArray() + ?? Array.Empty(); + + public static IEnumerable AllPrepatches + => _allFishPrepatches ??= AllPrepatchClasses?.SelectMany(static patchClass => patchClass.Patches.All.Values) + .ToArray() + ?? Array.Empty(); + public PerformanceFishMod(ModContentPack content) : base(content) { - ActiveMods.CheckRequirements(); + try + { + ActiveMods.CheckRequirements(); + } + catch (Exception e) + { + Log.Error(e.ToString()); + ApplyModInactiveLetterPatch(); + return; + } + InitializeMod(); } private void InitializeMod() { - DebugLog.Message($"Initializing {NAME} v{VERSION}"); + DebugLog.Message($"Initializing {NAME} v{typeof(PerformanceFishMod).Assembly.GetLoadedVersion()}"); // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (Mod != null) @@ -79,14 +107,31 @@ public class PerformanceFishMod : Mod Log.Error("Prepatcher mod is missing. Performance Fish will not be working correctly in this state."); } - modSettings = Settings; - modSettings!.Mod = this; + if (ActiveMods.TryGetModContentPack(PackageIDs.PREPATCHER) is not { } prepatcherContent + || prepatcherContent.assemblies?.loadedAssemblies is not [_,..]) + { + Verse.Log.TryOpenLogWindow(); + Log.Error("Prepatcher assemblies are missing. This usually indicates an incomplete download or a " + + "copy of github sources instead of the actual mod."); + } if (!OnAssembliesLoaded.Loaded) { Verse.Log.TryOpenLogWindow(); Log.Error("Performance Fish failed to initialize entirely. There are either critical dependencies " + "missing or mods with hard incompatibilities present."); + ApplyModInactiveLetterPatch(); + } + + modSettings = Settings; + if (modSettings is null) + { + Verse.Log.TryOpenLogWindow(); + Log.Error("Performance Fish failed to initialize its mod settings."); + } + else + { + modSettings.Mod = this; } #if DEBUG @@ -94,6 +139,15 @@ public class PerformanceFishMod : Mod #endif } + private static void ApplyModInactiveLetterPatch() + => Harmony.Patch(AccessTools.DeclaredMethod(typeof(Game), nameof(Game.FinalizeInit)), + postfix: new(methodof(ThrowModInactiveLetter), Priority.Last)); + + public static void ThrowModInactiveLetter() + => Find.LetterStack.AddLetterSilently(LetterMaker.MakeLetter("Performance Fish inactive", + "Missing dependencies or hard incompatibilities caused Performance Fish to entirely fail to " + + "initialize. It will not be doing anything in this state.", LetterDefOf.Bossgroup)); + public static void LogPatchCount() { var allFishPatches = AllPatchClasses!.SelectMany(static patchClass => patchClass.Patches.All.Values).ToList(); @@ -135,7 +189,14 @@ public class PerformanceFishMod : Mod var types = Assembly.GetExecutingAssembly().GetTypes(); var list = new List(types.Length); - Parallel.ForEach(types, type => + if (typeof(T).IsAssignableTo(typeof(FishPatch))) + Parallel.ForEach(types, InitializePatchClass); + else + Array.ForEach(types, InitializePatchClass); + + return list.ToArray(); + + void InitializePatchClass(Type type) { if (!type.IsAssignableTo(typeof(T)) || type.IsInterface @@ -147,7 +208,7 @@ public class PerformanceFishMod : Mod try { var patchClass = SingletonFactory.Get(type); - + lock (((ICollection)list).SyncRoot) list.Add(patchClass); } @@ -155,9 +216,7 @@ public class PerformanceFishMod : Mod { Log.Error($"{NAME} encountered an exception while trying to initialize {type.Name}:\n{ex}"); } - }); - - return list.ToArray(); + } } public static T? Get() diff --git a/Source/PerformanceFish/Planet/MothballOptimization.cs b/Source/PerformanceFish/Planet/MothballOptimization.cs index 90939a2..f04fb23 100644 --- a/Source/PerformanceFish/Planet/MothballOptimization.cs +++ b/Source/PerformanceFish/Planet/MothballOptimization.cs @@ -11,14 +11,14 @@ using RimWorld.Planet; namespace PerformanceFish.Planet; -public class MothballOptimization : ClassWithFishPrepatches +public sealed class MothballOptimization : ClassWithFishPrepatches { public const string BASIC_DESCRIPTION = "Mothballing means to \"take something out of operation but maintain it so that it can be used in the " + "future.\" RimWorld has this system for world pawns along with a number of conditions which can block it " + "from taking effect."; - public class WorldPawnsDefPreventingMothball : FishPrepatch + public sealed class WorldPawnsDefPreventingMothball : FishPrepatch { public override string? Description { get; } = BASIC_DESCRIPTION + " This patch essentially serves as a whitelist to allow mothballing a lot more " diff --git a/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs b/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs index a0614e2..906f3d8 100644 --- a/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs +++ b/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs @@ -11,9 +11,9 @@ using RimWorld.Planet; namespace PerformanceFish.Planet; -public class WorldObjectsOptimization : ClassWithFishPrepatches +public sealed class WorldObjectsOptimization : ClassWithFishPrepatches { - public class WorldObjectsHolderTickPatch : FishPrepatch + public sealed class WorldObjectsHolderTickPatch : FishPrepatch { public override string? Description { get; } = "The world objects holder is responsible for ticking every world object. This includes settlements, " @@ -87,24 +87,46 @@ public class WorldObjectsOptimization : ClassWithFishPrepatches CachedWorldObjectsVersion = -2, CachedMapsVersion = -2; - private static Type[] - _whitelistedTickingCompTypes - = - { - typeof(WorldObjectComp), - typeof(FormCaravanComp), - typeof(TimedDetectionRaids), - typeof(EnterCooldownComp) - }, - _whitelistedWorldObjectTypes = { typeof(WorldObject), typeof(MapParent), typeof(Settlement) }; + private static Type?[] + _whitelistedTickingCompTypes = + [ + typeof(WorldObjectComp), + typeof(FormCaravanComp), + typeof(TimedDetectionRaids), + typeof(EnterCooldownComp) + ], + _whitelistedWorldObjectTypes = + [ + typeof(WorldObject), + typeof(MapParent), + typeof(Settlement), + ModCompatibility.Types.RealRuins.POIWorldObject + ]; + + public static void AddCompToWhiteList(Type compType) + { + _whitelistedTickingCompTypes = _whitelistedTickingCompTypes.Add(compType); + SkippableComps = InitializeSkippableComps(); + } + + public static void AddWorldObjectToWhiteList(Type worldObjectType) + { + _whitelistedWorldObjectTypes = _whitelistedWorldObjectTypes.Add(worldObjectType); + SkippableWorldObjects = InitializeSkippableWorldObjects(); + } public static HashSet - SkippableComps = MakeSubclassHashSet(typeof(WorldObjectComp), nameof(WorldObjectComp.CompTick), - _whitelistedTickingCompTypes), - SkippableWorldObjects = MakeSubclassHashSet(typeof(WorldObject), nameof(WorldObject.Tick), - _whitelistedWorldObjectTypes); + SkippableComps = InitializeSkippableComps(), + SkippableWorldObjects = InitializeSkippableWorldObjects(); - private static HashSet MakeSubclassHashSet(Type type, string name, Type[] allowedDeclaringTypes) + private static HashSet InitializeSkippableComps() + => MakeSubclassHashSet(typeof(WorldObjectComp), nameof(WorldObjectComp.CompTick), + _whitelistedTickingCompTypes); + + private static HashSet InitializeSkippableWorldObjects() + => MakeSubclassHashSet(typeof(WorldObject), nameof(WorldObject.Tick), _whitelistedWorldObjectTypes); + + private static HashSet MakeSubclassHashSet(Type type, string name, Type?[] allowedDeclaringTypes) => type.SubclassesWithNoMethodOverrideAndSelf(allowedDeclaringTypes, name).ToHashSet(); public static bool CacheDirty(WorldObjectsHolder instance) @@ -115,4 +137,112 @@ public class WorldObjectsOptimization : ClassWithFishPrepatches static WorldObjectsHolderTickPatch() => Cache.Utility.Cleared += SetDirty; } + + public sealed class ExpandingIconCaching : FishPrepatch + { + public override string? Description { get; } + = "Caches icons that get displayed for world objects like settlements on the planet view and adds various " + + "safety checks to better catch and log errors in case of missing icons"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredPropertyGetter(typeof(WorldObject), nameof(WorldObject.ExpandingIcon)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static Texture2D ReplacementBody(WorldObject __instance) + => __instance.ExpandingIconCache() ?? InitializeExpandingIcon(__instance); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static Texture2D InitializeExpandingIcon(WorldObject? worldObject) + { + if (worldObject is null) + return BaseContent.BadTex; + + var def = worldObject.def; + ref var cache = ref worldObject.ExpandingIconCache(); + + try + { + cache = def?.ExpandingIconTexture; + + if (cache != null) + return cache; + + var material = worldObject.Material; + if (material != null) + cache = material.mainTexture as Texture2D; + + if (cache == null) + { + cache = BaseContent.BadTex; + Log.Error($"No expanding icon found for '{worldObject.ToStringSafe()}' of def '{ + def?.ToStringSafe()}' from mod '{def?.modContentPack?.Name}'. Assigning default."); + } + + return cache; + } + catch (Exception ex) + { + Log.Error($"Exception while initializing icon for '{worldObject.ToStringSafe()}' of def '{ + def?.ToStringSafe()}' from mod '{def?.modContentPack?.Name}':\n{ex}"); + + return cache = BaseContent.BadTex; + } + } + } + + public sealed class ExpandingIconColorCaching : FishPrepatch + { + public override string? Description { get; } + = "Caches colors of icons that get displayed for world objects like settlements on the planet view and " + + "adds various safety checks to better catch and log errors in case of missing data"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredPropertyGetter(typeof(WorldObject), nameof(WorldObject.ExpandingIconColor)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static Color ReplacementBody(WorldObject __instance) + => __instance.ExpandingIconColorCache() ?? InitializeExpandingIconColor(__instance); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static Color InitializeExpandingIconColor(WorldObject? worldObject) + { + if (worldObject is null) + return Color.white; + + var def = worldObject.def; + ref var cache = ref worldObject.ExpandingIconColorCache(); + + try + { + cache = def?.expandingIconColor; + + if (cache != null) + return cache.GetValueOrDefault(); + + var material = worldObject.Material; + if (material != null) + cache = material.color; + + if (cache == null) + { + cache = Color.white; + Log.Error($"No expanding icon color found for '{worldObject.ToStringSafe()}' of def '{ + def?.ToStringSafe()}' from mod '{def?.modContentPack?.Name}'. Assigning default."); + } + + return cache.GetValueOrDefault(); + } + catch (Exception ex) + { + Log.Error($"Exception while initializing icon color for '{worldObject.ToStringSafe()}' of def '{ + def?.ToStringSafe()}' from mod '{def?.modContentPack?.Name}':\n{ex}"); + + return (cache = Color.white).GetValueOrDefault(); + } + } + } } \ No newline at end of file diff --git a/Source/PerformanceFish/Planet/WorldPawnGCOptimization.cs b/Source/PerformanceFish/Planet/WorldPawnGCOptimization.cs index 4eb3caa..3f97a75 100644 --- a/Source/PerformanceFish/Planet/WorldPawnGCOptimization.cs +++ b/Source/PerformanceFish/Planet/WorldPawnGCOptimization.cs @@ -5,14 +5,15 @@ using Mono.Cecil; using Mono.Cecil.Cil; +using PerformanceFish.ModCompatibility; using PerformanceFish.Prepatching; using RimWorld.Planet; namespace PerformanceFish.Planet; -public class WorldPawnGCOptimization : ClassWithFishPrepatches +public sealed class WorldPawnGCOptimization : ClassWithFishPrepatches { - public class AddRelationshipsPatch : FishPrepatch + public sealed class AddRelationshipsPatch : FishPrepatch { public override string? Description { get; } = "The world pawn GC is responsible for deleting no longer relevant pawns. AddAllRelationships here tries " @@ -29,8 +30,10 @@ public class WorldPawnGCOptimization : ClassWithFishPrepatches => AcceptedCriticalPawnReasons.Contains(keptPawns[pawn]); } - public class WorldPawnGCTickPatch : FishPrepatch + public sealed class WorldPawnGCTickPatch : FishPrepatch { + public override List IncompatibleModIDs { get; } = [PackageIDs.MULTIPLAYER]; + public override string? Description { get; } = "Normally the world pawn GC processes 1 pawn per tick, and speeds up if it fails to complete its " + "processing before the game had to add or remove world pawns through other means. This patch changes the " @@ -42,7 +45,7 @@ public class WorldPawnGCOptimization : ClassWithFishPrepatches public static bool Prefix() => TickHelper.MatchesModulo(4); } - public class CancelGCPassPatch : FishPrepatch + public sealed class CancelGCPassPatch : FishPrepatch { public override string? Description { get; } = "When the game determines that it has to speed up its GC process because of world pawn additions or " @@ -67,7 +70,7 @@ public class WorldPawnGCOptimization : ClassWithFishPrepatches } } - public class GetCriticalPawnReasonPatch : FishPrepatch + public sealed class GetCriticalPawnReasonPatch : FishPrepatch { public override string? Description { get; } = "Critical pawns are pawns the game's world pawn GC does not delete. Relationships other than parents of " @@ -102,5 +105,5 @@ public class WorldPawnGCOptimization : ClassWithFishPrepatches } public static HashSet AcceptedCriticalPawnReasons - = new(new[] { "Colonist", "Generating", "Kidnapped", "CaravanMember", "TransportPod", "Spawned" }); + = [..new[] { "Colonist", "Generating", "Kidnapped", "CaravanMember", "TransportPod", "Spawned" }]; } \ No newline at end of file diff --git a/Source/PerformanceFish/Planet/WorldPawnsOptimization.cs b/Source/PerformanceFish/Planet/WorldPawnsOptimization.cs index 6dca958..89e4602 100644 --- a/Source/PerformanceFish/Planet/WorldPawnsOptimization.cs +++ b/Source/PerformanceFish/Planet/WorldPawnsOptimization.cs @@ -10,9 +10,9 @@ using RimWorld.Planet; namespace PerformanceFish.Planet; -public class WorldPawnsOptimization : ClassWithFishPrepatches +public sealed class WorldPawnsOptimization : ClassWithFishPrepatches { - public class AllPawnsAlivePatch : FishPrepatch + public sealed class AllPawnsAlivePatch : FishPrepatch { public override string? Description { get; } = "Minor optimization to reduce the amount of copying happening in the WorldPawns.AllPawnsAlive method"; @@ -52,7 +52,7 @@ public class WorldPawnsOptimization : ClassWithFishPrepatches || instance.pawnsMothballed._version != cache.PawnsMothballedVersion; } - public class AllPawnsAliveOrDeadPatch : FishPrepatch + public sealed class AllPawnsAliveOrDeadPatch : FishPrepatch { public override string? Description { get; } = "Minor optimization to reduce the amount of copying happening in the WorldPawns.AllPawnsAliveOrDead " diff --git a/Source/PerformanceFish/Pools/FishPool.cs b/Source/PerformanceFish/Pools/FishPool.cs deleted file mode 100644 index 455858e..0000000 --- a/Source/PerformanceFish/Pools/FishPool.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2023 bradson -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -namespace PerformanceFish.Pools; - -public static class FishPool where T : IFishPoolable -{ - private static T?[] _freeItems = new T[4]; - private static int _freeItemCount; - - public static T Get() - { - if (_freeItemCount == 0) - return Reflection.New(); - - var freeItem = _freeItems[--_freeItemCount]; - _freeItems[_freeItemCount] = default; - return freeItem!; - } - - public static void Return(T item) - { - item.Reset(); - - if (++_freeItemCount >= _freeItems.Length) - ExpandFreeItems(); - - _freeItems[_freeItemCount - 1] = item; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ExpandFreeItems() => Array.Resize(ref _freeItems, _freeItems.Length << 1); -} - -public interface IFishPoolable -{ - public void Reset(); -} \ No newline at end of file diff --git a/Source/PerformanceFish/Pools/PooledIList.cs b/Source/PerformanceFish/Pools/PooledIList.cs deleted file mode 100644 index 6514823..0000000 --- a/Source/PerformanceFish/Pools/PooledIList.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2023 bradson -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -namespace PerformanceFish.Pools; - -public struct PooledIList : IDisposable where T : IList, new() -{ - public T List; - - private static T?[] _freeItems = new T[4]; - private static int _freeItemCount; - - public PooledIList() => List = Get(); - - public void Dispose() - { - if (List == null) - return; - - Return(List); - List = default!; - } - - public static T Get() - { - if (_freeItemCount == 0) - return Reflection.New(); - - var freeItem = _freeItems[--_freeItemCount]; - _freeItems[_freeItemCount] = default; - return freeItem!; - } - - public static void Return(T item) - { - item.Clear(); - - if (++_freeItemCount >= _freeItems.Length) - ExpandFreeItems(); - - _freeItems[_freeItemCount - 1] = item; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ExpandFreeItems() => Array.Resize(ref _freeItems, _freeItems.Length << 1); -} \ No newline at end of file diff --git a/Source/PerformanceFish/Pools/PooledStringBuilder.cs b/Source/PerformanceFish/Pools/PooledStringBuilder.cs deleted file mode 100644 index 3a4a68e..0000000 --- a/Source/PerformanceFish/Pools/PooledStringBuilder.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2023 bradson -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -using System.Security; -using System.Text; -using JetBrains.Annotations; - -namespace PerformanceFish.Pools; - -public struct PooledStringBuilder : IDisposable -{ - private FishPoolable _fishPoolable; - - public StringBuilder Builder => _fishPoolable.Builder; - - public StringBuilder Append(string? value) => Builder.Append(value); - public StringBuilder Append(char value) => Builder.Append(value); - public StringBuilder Append(char[] value) => Builder.Append(value); - public StringBuilder Append(PooledStringBuilderHandler value) => value.AppendToStringBuilder(Builder); - - public StringBuilder AppendLine(string? value) => Builder.AppendLine(value); - - public StringBuilder Insert(int index, string? value) => Builder.Insert(index, value); - public StringBuilder Insert(int index, char value) => Builder.Insert(index, value); - - public StringBuilder Remove(int startIndex, int length) => Builder.Remove(startIndex, length); - - public void Clear() => Builder.Clear(); - - public override string ToString() => Builder.ToString(); - - [SecuritySafeCritical] - public PooledStringBuilder() => _fishPoolable = FishPool.Get(); - - public PooledStringBuilder(int minimumSize) : this() - { - if (_fishPoolable.Builder.Capacity < minimumSize) - _fishPoolable.Builder.Capacity = minimumSize; - } - - [SecuritySafeCritical] - public void Dispose() - { - if (_fishPoolable.Builder == null!) - return; - - FishPool.Return(_fishPoolable); - _fishPoolable.Builder = null!; - } - - private struct FishPoolable : IFishPoolable - { - public StringBuilder Builder = new(); - - public void Reset() => Builder.Clear(); - - [UsedImplicitly] - public FishPoolable() {} - } -} \ No newline at end of file diff --git a/Source/PerformanceFish/Pools/PooledStringBuilderHandler.cs b/Source/PerformanceFish/Pools/PooledStringBuilderHandler.cs deleted file mode 100644 index 4fe3946..0000000 --- a/Source/PerformanceFish/Pools/PooledStringBuilderHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2023 bradson -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -using System.Text; -using JetBrains.Annotations; - -namespace PerformanceFish.Pools; - -[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] -[InterpolatedStringHandler] -public struct PooledStringBuilderHandler : IDisposable -{ - private PooledIList> _chars; - - public PooledStringBuilderHandler(int literalLength, int formattedCount) - { - _chars = new(); - _chars.List.EnsureCapacity(literalLength + (formattedCount * 8)); - } - - public void AppendLiteral(string s) => _chars.List.Add(s); - - public void AppendFormatted(T t) - { - if (t != null) - _chars.List.Add(t.ToString()); - } - - public StringBuilder AppendToStringBuilder(StringBuilder destination) - { - destination.Append(_chars.List); - Dispose(); - return destination; - } - - public void Dispose() => _chars.Dispose(); -} \ No newline at end of file diff --git a/Source/PerformanceFish/Pools/PooledStringHandler.cs b/Source/PerformanceFish/Pools/PooledStringHandler.cs deleted file mode 100644 index a3c93a9..0000000 --- a/Source/PerformanceFish/Pools/PooledStringHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2023 bradson -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -using JetBrains.Annotations; - -namespace PerformanceFish.Pools; - -[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] -[InterpolatedStringHandler] -public struct PooledStringHandler : IDisposable -{ - private PooledStringBuilder _builder; - - public PooledStringHandler(int literalLength, int formattedCount) - => _builder = new(literalLength + (formattedCount * 8)); - - public void AppendLiteral(string s) => _builder.Append(s); - - public void AppendFormatted(T t) - { - if (typeof(T) == typeof(PooledStringBuilderHandler)) - _builder.Append(Unsafe.As(ref t)); - else - _builder.Append(t?.ToString()); - } - - public string GetFormattedText() - { - var result = _builder.ToString(); - Dispose(); - return result; - } - - public void Dispose() => _builder.Dispose(); -} \ No newline at end of file diff --git a/Source/PerformanceFish/PrepatcherFields.cs b/Source/PerformanceFish/PrepatcherFields.cs index 762d440..49bf80a 100644 --- a/Source/PerformanceFish/PrepatcherFields.cs +++ b/Source/PerformanceFish/PrepatcherFields.cs @@ -3,6 +3,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +using System.Linq; +using PerformanceFish.Events; +using PerformanceFish.Hauling; using PerformanceFish.Planet; using Prepatcher; using RimWorld.Planet; @@ -11,27 +14,179 @@ namespace PerformanceFish; public static class PrepatcherFields { + [PrepatcherField] + [ValueInitializer(nameof(CreateMapEvents))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern MapEvents.Instanced Events(this Map map); + + [PrepatcherField] + [ValueInitializer(nameof(CreateThingEvents))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern ThingEvents.Instanced Events(this Thing thing); + [PrepatcherField] [ValueInitializer(nameof(CreateGeneList))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static extern List GenesToTick(this Pawn_GeneTracker geneTracker); [PrepatcherField] [ValueInitializer(nameof(CreateWorldPawnsCache))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static extern ref WorldPawnsOptimization.Cache Cache(this WorldPawns worldPawns); [PrepatcherField] [ValueInitializer(nameof(CreateBedHashSet))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static extern HashSet UniqueContainedBeds(this Room room); + [PrepatcherField] + [ValueInitializer(nameof(CreateIndexMap))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern FishTable IndexMap(this ThingOwner thingOwner) where T : Thing; + + [PrepatcherField] + [ValueInitializer(nameof(CreateIndexMap))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern FishTable IndexMapByDef(this ListerThings listerThings); + + [PrepatcherField] + [ValueInitializer(nameof(CreateIndexMapByGroup))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern FishTable IndexMapByGroup(this ListerThings listerThings); + + [PrepatcherField] + [ValueInitializer(nameof(CreateItemCountGrid))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern Cache.CellGrid ItemCountGrid(this Map map); + + [PrepatcherField] + [ValueInitializer(nameof(CreateBitCellGrid))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern Cache.BitCellGrid StorageBlockerGrid(this Map map); + + [PrepatcherField] + [ValueInitializer(nameof(CreateStorageSettingsCache))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern ref StorageSettingsPatches.StorageSettingsCache Cache(this StorageSettings storageSettings); + + [PrepatcherField] + [ValueInitializer(nameof(CreateParallelGasGridArray))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern GasGridOptimization.ParallelGasGrid[] ParallelGasGrids(this GasGrid gasGrid); + + [PrepatcherField] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern ref Texture2D? ExpandingIconCache(this WorldObject worldObject); + + [PrepatcherField] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern ref Color? ExpandingIconColorCache(this WorldObject worldObject); + + [PrepatcherField] + [ValueInitializer(nameof(GetDefaultDistrictArray))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern ref StorageDistrict[] Districts(this SlotGroup slotGroup); + + [PrepatcherField] + [ValueInitializer(nameof(CreateStorageDistrictGrid))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern Cache.CellGrid StorageDistrictGrid(this Map map); + + [PrepatcherField] + [ValueInitializer(nameof(CreateListerBuildingsCache))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern ref Listers.Buildings.Cache Cache(this ListerBuildings listerBuildings); + + [PrepatcherField] + [ValueInitializer(nameof(CreateListerHaulablesCache))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern ref Listers.Haulables.Cache Cache(this ListerHaulables listerHaulables); + + [PrepatcherField] + [ValueInitializer(nameof(CreateHaulDestinationManagerCache))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern ref HaulDestinationManagerCache Cache(this HaulDestinationManager manager); + + [PrepatcherField] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static extern ref object MonitorObject(this GasGrid gasGrid); + + // [PrepatcherField] + // [ValueInitializer(nameof(CreateHediffCompList))] + // [MethodImpl(MethodImplOptions.AggressiveInlining)] + // public static extern List HediffCompsWithVisible(this HediffWithComps hediff); + // + // [PrepatcherField] + // [ValueInitializer(nameof(CreateHediffCompList))] + // [MethodImpl(MethodImplOptions.AggressiveInlining)] + // public static extern List HediffCompsWithShouldRemove(this HediffWithComps hediff); + // + // [PrepatcherField] + // [ValueInitializer(nameof(CreateHediffCompList))] + // [MethodImpl(MethodImplOptions.AggressiveInlining)] + // public static extern List HediffCompsWithPostTick(this HediffWithComps hediff); + #if false [PrepatcherField] [ValueInitializer(nameof(CreateHediffList))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static extern List HediffsToTick(this Pawn_HealthTracker healthTracker); public static List CreateHediffList() => new(); #endif - public static List CreateGeneList() => new(); + public static List CreateGeneList() => []; public static WorldPawnsOptimization.Cache CreateWorldPawnsCache() => new(); - public static HashSet CreateBedHashSet() => new(); + public static HashSet CreateBedHashSet() => []; + public static FishTable CreateIndexMap() => new(); + public static FishTable CreateIndexMapByGroup() => new(); + public static Cache.CellGrid CreateItemCountGrid(Map map) => new(map); + public static Cache.BitCellGrid CreateBitCellGrid(Map map) => new(map); + public static MapEvents.Instanced CreateMapEvents() => new(); + public static ThingEvents.Instanced CreateThingEvents() => new(); + public static StorageSettingsPatches.StorageSettingsCache CreateStorageSettingsCache() => new(); + public static StorageDistrict[] GetDefaultDistrictArray() => StorageDistrict.GetDefaultArray(); + public static Cache.CellGrid CreateStorageDistrictGrid(Map map) => new(map); + public static Listers.Buildings.Cache CreateListerBuildingsCache() => new(); + public static Listers.Haulables.Cache CreateListerHaulablesCache() => new(); + public static HaulDestinationManagerCache CreateHaulDestinationManagerCache() => new(); + + public static GasGridOptimization.ParallelGasGrid[] CreateParallelGasGridArray(GasGrid gasGrid) + { + DefDatabase.SetIndices(); + + var map = gasGrid.map; + var gasDefs = DefDatabase.AllDefsListForReading; + + if (gasDefs.Count < 3) + { + Log.Error($"GasDefs are missing! Expected are at least 3, DefDatabase contains { + gasDefs.Count} instead. ParallelGasGrid won't work correctly in this state."); + } + else + { + if (gasDefs[0] != GasDefOf.BlindSmoke || gasDefs[1] != GasDefOf.ToxGas || gasDefs[2] != GasDefOf.RotStink) + { + Log.Warning("GasDefs out of order. Sorting now to fix."); + var allGasDefs = gasDefs.ToList(); + + allGasDefs.Remove(gasDefs[0] = GasDefOf.BlindSmoke); + allGasDefs.Remove(gasDefs[1] = GasDefOf.ToxGas); + allGasDefs.Remove(gasDefs[2] = GasDefOf.RotStink); + + for (var i = 0; i < allGasDefs.Count; i++) + gasDefs[i + 3] = allGasDefs[i]; + + DefDatabase.SetIndices(); + } + } + + var grids = new GasGridOptimization.ParallelGasGrid[gasDefs.Count]; + for (var i = 0; i < grids.Length; i++) + grids[i] = new(map, gasDefs[i]); + + return grids; + } + + // public static List CreateHediffCompList() => new(); } \ No newline at end of file diff --git a/Source/PerformanceFish/Prepatching/ClassWithFishPrepatches.cs b/Source/PerformanceFish/Prepatching/ClassWithFishPrepatches.cs index 36f44e2..4b9e277 100644 --- a/Source/PerformanceFish/Prepatching/ClassWithFishPrepatches.cs +++ b/Source/PerformanceFish/Prepatching/ClassWithFishPrepatches.cs @@ -13,7 +13,22 @@ namespace PerformanceFish.Prepatching; public abstract class ClassWithFishPrepatches : SingletonFactory { public virtual FishPrepatchHolder Patches { get; } - public virtual bool RequiresLoadedGameForPatching => false; + + public virtual bool ShowSettings + { + get + { + foreach (var patch in Patches) + { + if (patch.ShowSettings) + return true; + } + + return false; + } + } + + public virtual bool Enabled => true; protected ClassWithFishPrepatches() => Patches = new(GetType()); } \ No newline at end of file diff --git a/Source/PerformanceFish/Prepatching/FishClassPrepatch.cs b/Source/PerformanceFish/Prepatching/FishClassPrepatch.cs index 31c26e4..d02e0be 100644 --- a/Source/PerformanceFish/Prepatching/FishClassPrepatch.cs +++ b/Source/PerformanceFish/Prepatching/FishClassPrepatch.cs @@ -11,7 +11,25 @@ public abstract class FishClassPrepatch : FishPrepatchBase { public abstract Type Type { get; } - public virtual void FreePatch(TypeDefinition typeDefinition) + public abstract void FreePatch(TypeDefinition typeDefinition); + + protected internal override void ApplyPatch(ModuleDefinition module) { + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + var patchType = Type + ?? ThrowHelper.ThrowInvalidOperationException( + $"Patch {GetType().FullName} has no target type."); + + if (patchType.IsGenericType) + patchType = patchType.GetGenericTypeDefinition(); + + DebugLog.Message($"Applying prepatch {GetType().FullName} on type { + patchType.FullName}"); + + var targetType = module.GetType(patchType.Namespace, patchType.Name) ?? ThrowForFailureToFindType(patchType); + + // PrepatchManager.AddSecurityAttributes(targetType.SecurityDeclarations, module); + + FreePatch(targetType); } } \ No newline at end of file diff --git a/Source/PerformanceFish/Prepatching/FishPrepatch.cs b/Source/PerformanceFish/Prepatching/FishPrepatch.cs index 1421a07..2e2d957 100644 --- a/Source/PerformanceFish/Prepatching/FishPrepatch.cs +++ b/Source/PerformanceFish/Prepatching/FishPrepatch.cs @@ -5,6 +5,9 @@ using Mono.Cecil; using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; +using Mono.Collections.Generic; +using MonoMod.Utils; namespace PerformanceFish.Prepatching; @@ -24,4 +27,331 @@ public abstract class FishPrepatch : FishPrepatchBase private MethodInfo? TryGetMethod(string nameCaseInsensitive, Predicate? predicate = null) => GetType().TryGetMethod(nameCaseInsensitive, predicate); + + protected internal override void ApplyPatch(ModuleDefinition module) + { + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + var patchMethod = TargetMethodBase + ?? ThrowHelper.ThrowInvalidOperationException( + $"Patch {GetType().FullName} has no target method."); + + if (patchMethod.IsGenericMethod && patchMethod is MethodInfo info) + patchMethod = info.GetGenericMethodDefinition(); + + var patchType = patchMethod.ReflectedType + ?? ThrowHelper.ThrowInvalidOperationException( + $"Method {patchMethod.FullDescription()} has no declaring type."); + + if (patchMethod.ReflectedType != patchMethod.DeclaringType) + { + ThrowHelper.ThrowInvalidOperationException($"Tried to apply patch on method { + patchMethod.Name} for type {patchMethod.ReflectedType}, but it is only declared in { + patchMethod.DeclaringType}."); + } + + if (patchType.IsGenericType) + { + patchType = patchType.GetGenericTypeDefinition(); + patchMethod = patchMethod is MethodInfo + ? AccessTools.DeclaredMethod(patchType, patchMethod.Name, patchMethod.GetParameters().GetTypes()) + : AccessTools.Constructor(patchType, patchMethod.GetParameters().GetTypes(), patchMethod.IsStatic); + } + + DebugLog.Message($"Applying prepatch {GetType().FullName} on method { + patchMethod.FullDescription()}"); + + var targetType = module.GetTypeDefinition(patchType) ?? ThrowForFailureToFindType(patchType); + var targetMethod = targetType.GetMethod(patchMethod.Name, patchMethod.GetParameters().GetTypes()) + ?? ThrowForFailureToFindMethod(targetType, patchMethod); + + // PrepatchManager.AddSecurityAttributes(targetMethod.SecurityDeclarations, module, targetMethod.FullName); + + var targetMethodBody = targetMethod.Body; + var ilProcessor = targetMethodBody.GetILProcessor(); + + try + { + Transpiler(ilProcessor, module); + } + catch (Exception e) + { + Log.Error($"Exception while applying transpiler for {GetType().FullName}\n{e}\n{ + Environment.StackTrace /*StackTraceUtility.ExtractStackTrace()*/}"); + } + + var patchVariables = new PatchVariables(); + + if (PrefixMethodInfo is { } prefixMethod) + { + try + { + ApplyPrefix(prefixMethod, ilProcessor, module, ref patchVariables); + } + catch (Exception e) + { + Log.Error($"Exception while applying prefix for {GetType().FullName}\n{e}\n{ + Environment.StackTrace /*StackTraceUtility.ExtractStackTrace()*/}"); + } + } + + if (PostfixMethodInfo is { } postfixMethod) + { + try + { + ApplyPostfix(postfixMethod, ilProcessor, module, ref patchVariables); + } + catch (Exception e) + { + Log.Error($"Exception while applying postfix for {GetType().FullName}\n{e}\n{ + Environment.StackTrace /*StackTraceUtility.ExtractStackTrace()*/}"); + } + } + + targetMethodBody.SimplifyMacros(); + targetMethodBody.OptimizeMacros(); + } + + private static void ApplyPrefix(MethodInfo prefixMethodInfo, ILProcessor ilProcessor, ModuleDefinition module, + ref PatchVariables patchVariables) + { + if (prefixMethodInfo.ReturnType != typeof(bool) && prefixMethodInfo.ReturnType != typeof(void)) + { + ThrowHelper.ThrowArgumentException($"Prefix has invalid return type: { + prefixMethodInfo.ReturnType.FullName}. Must be void or bool"); + } + + var instructions = ilProcessor.instructions; + var targetMethod = ilProcessor.GetMethod(); + + var genericParameters = GetGenericParameters(targetMethod); + var prefixParameters = prefixMethodInfo.GetParameters(); + + var resultParameter = prefixParameters.TryGetNamed("__result"); + VerifyResultParameter(module, resultParameter, targetMethod, true); + + var stateParameter = prefixParameters.TryGetNamed("__state"); + VerifyStateParameter(stateParameter); + + // var skipOriginalFlag = ilProcessor.DeclareLocal(typeof(bool)); + var resultVariable = GetResultVariable(ilProcessor, ref patchVariables, resultParameter, targetMethod); + var stateVariable = GetStateVariable(ilProcessor, ref patchVariables, stateParameter); + var startOfOriginal = instructions[0]; + + var index = 0; + foreach (var parameter in prefixParameters) + { + ilProcessor.InsertAt(index++, GetInstructionForParameter(module, parameter, resultParameter, resultVariable, + stateParameter, stateVariable, targetMethod)); + } + + if (genericParameters != null) + { + ilProcessor.InsertAt(index++, OpCodes.Call, + module.ImportMethodWithGenericArguments(prefixMethodInfo, genericParameters)); + } + else + { + ilProcessor.InsertAt(index++, OpCodes.Call, prefixMethodInfo); + } + + if (prefixMethodInfo.ReturnType == typeof(bool)) + { + ilProcessor.InsertAt(index++, (OpCodes.Brtrue, startOfOriginal)); + + if (resultVariable != null) + { + ilProcessor.InsertAt(index++, (OpCodes.Ldloc, resultVariable)); + } + else if (targetMethod.ReturnType.FullName != module.TypeSystem.Void.FullName) + { + Log.Error($"Prefix { + prefixMethodInfo.FullDescription()} with return type of bool must assign to a __result parameter"); + } + + ilProcessor.InsertAt(index, OpCodes.Ret); + } + } + + private static void ApplyPostfix(MethodInfo postfixMethodInfo, ILProcessor ilProcessor, ModuleDefinition module, + ref PatchVariables patchVariables) + { + var targetMethod = ilProcessor.GetMethod(); + + if (postfixMethodInfo.ReturnType != typeof(void) + && !postfixMethodInfo.ReturnType.Is(targetMethod.ReturnType)) + { + ThrowHelper.ThrowArgumentException($"Postfix has invalid return type: { + postfixMethodInfo.ReturnType.FullName}. Must be void or {targetMethod.ReturnType}"); + } + + var instructions = ilProcessor.instructions; + var genericParameters = GetGenericParameters(targetMethod); + var postfixParameters = postfixMethodInfo.GetParameters(); + + var resultParameter = postfixParameters.TryGetNamed("__result"); + VerifyResultParameter(module, resultParameter, targetMethod, false); + + var stateParameter = postfixParameters.TryGetNamed("__state"); + // Verify? It would make sense to match against prefix state + + // var skipOriginalFlag = ilProcessor.DeclareLocal(typeof(bool)); + var resultVariable = GetResultVariable(ilProcessor, ref patchVariables, resultParameter, targetMethod); + var stateVariable = GetStateVariable(ilProcessor, ref patchVariables, stateParameter); + + var postfixInstructions = new Collection(); + + if (resultVariable != null) + postfixInstructions.Add((OpCodes.Stloc, resultVariable)); + + foreach (var parameter in postfixParameters) + { + postfixInstructions.Add(GetInstructionForParameter(module, parameter, resultParameter, resultVariable, + stateParameter, stateVariable, targetMethod)); + } + + if (genericParameters != null) + { + postfixInstructions.Add((OpCodes.Call, + module.ImportMethodWithGenericArguments(postfixMethodInfo, genericParameters))); + } + else + { + postfixInstructions.Add((OpCodes.Call, postfixMethodInfo)); + } + + if (postfixMethodInfo.ReturnType == typeof(void) && resultVariable != null) + postfixInstructions.Add((OpCodes.Ldloc, resultVariable)); + + Span retInstructions = stackalloc int[instructions.Count]; + var retInstructionCount = 0; + + for (var i = 0; i < instructions.Count; i++) + { + if (instructions[i].OpCode == OpCodes.Ret) + retInstructions[retInstructionCount++] = i; + } + + var lastRetInstructionIndex = retInstructions[retInstructionCount - 1]; + ilProcessor.InsertRange(lastRetInstructionIndex, postfixInstructions); + + for (var i = 0; i < instructions.Count; i++) + { + var instruction = instructions[i]; + if (instruction.Operand is Instruction branchTarget && branchTarget.OpCode == OpCodes.Ret) + instruction.Operand = instructions[lastRetInstructionIndex]; + } + + for (var i = 0; i < retInstructionCount - 1; i++) + { + var instruction = instructions[retInstructions[i]]; + instruction.OpCode = OpCodes.Br; + instruction.Operand = instructions[lastRetInstructionIndex]; + } + } + + private static VariableDefinition? GetStateVariable(ILProcessor ilProcessor, ref PatchVariables patchVariables, + ParameterInfo? stateParameter) + => stateParameter != null + ? patchVariables.State ??= ilProcessor.DeclareLocal(GetUnderlyingType(stateParameter.ParameterType)) + : null; + + private static VariableDefinition? GetResultVariable(ILProcessor ilProcessor, ref PatchVariables patchVariables, + ParameterInfo? resultParameter, MethodDefinition targetMethod) + => resultParameter != null + ? patchVariables.Result ??= ilProcessor.DeclareLocal(targetMethod.ReturnType) + : null; + + private static void VerifyStateParameter(ParameterInfo? stateParameter) + { + if (stateParameter is { ParameterType.IsByRef: false }) + ThrowHelper.ThrowArgumentException("__state parameter must be declared with ref or out keyword."); + } + + private static ITuple GetInstructionForParameter(ModuleDefinition module, ParameterInfo parameter, + ParameterInfo? resultParameter, VariableDefinition? resultVariable, ParameterInfo? stateParameter, + VariableDefinition? stateVariable, MethodDefinition targetMethod) + => parameter == resultParameter + ? (parameter.ParameterType.IsByRef ? OpCodes.Ldloca : OpCodes.Ldloc, resultVariable) + : parameter == stateParameter + ? (parameter.ParameterType.IsByRef ? OpCodes.Ldloca : OpCodes.Ldloc, stateVariable) + : GetMatchingTargetParameter(targetMethod, parameter, module); + + private static void VerifyResultParameter(ModuleDefinition module, ParameterInfo? resultParameter, + MethodDefinition targetMethod, bool requiresByRef) + { + if (resultParameter is null) + return; + + if (targetMethod.ReturnType.IsGeneric() ? !GetUnderlyingType(resultParameter.ParameterType).IsGeneric() + : !GetUnderlyingType(resultParameter.ParameterType).Is(targetMethod.ReturnType)) + { + // var firstType = targetMethod.ReturnType; + // var secondType = GetUnderlyingType(resultParameter.ParameterType); + // Log.Message($"Type FullName: {firstType.FullName + // }: ContainsGenericParameter: {firstType.ContainsGenericParameter + // }, HasGenericParameters :{firstType.HasGenericParameters + // }, IsGenericInstance: {firstType.IsGenericInstance + // }, Type FullName: {secondType.FullName + // } ContainsGenericParameters: {secondType.ContainsGenericParameters + // }, IsGenericType {secondType.IsGenericType + // }, IsConstructedGenericType: {secondType.IsConstructedGenericType}"); + + ThrowHelper.ThrowArgumentException($"__result parameter has invalid type: { + resultParameter.ParameterType.FullName}. Must match the target's return type: { + targetMethod.ReturnType.FullName}"); + } + if (requiresByRef && !resultParameter.ParameterType.IsByRef) + { + ThrowHelper.ThrowArgumentException( + "__result parameter must be declared with ref or out keyword."); + } + } + + private static Collection? GetGenericParameters(MethodDefinition targetMethod) + => targetMethod.HasGenericParameters ? targetMethod.GenericParameters + : targetMethod.DeclaringType.HasGenericParameters ? targetMethod.DeclaringType.GenericParameters + : null; + + private static Type GetUnderlyingType(Type type) => type.IsByRef ? type.GetElementType()! : type; + + private static ITuple GetMatchingTargetParameter(MethodDefinition targetMethod, + ParameterInfo parameter, ModuleDefinition module) + { + var targetParameters = targetMethod.Parameters; + var targetParameter = parameter.Name == "__instance" + ? targetMethod.Body.ThisParameter + : targetParameters.TryGetNamed(parameter.Name); + if (targetParameter is null) + { + return ThrowHelper.ThrowArgumentException($"No parameter named { + parameter.Name} found in target method"); + } + + var targetParameterType = targetParameter.ParameterType; + var patchParameterType = parameter.ParameterType; + + return GetUnderlyingType(targetParameter).Name != GetUnderlyingType(patchParameterType).Name + ? ThrowHelper.ThrowArgumentException($"Parameter has invalid type: { + patchParameterType.FullName}. Must match the target method parameter's type: { + targetParameterType.FullName}") + : (patchParameterType.IsByRef && !targetParameter.ParameterType.IsByReference + ? OpCodes.Ldarga + : OpCodes.Ldarg, targetParameter); + } + + private static TypeReference GetUnderlyingType(ParameterInfo prefixParameter, ModuleDefinition module) + => module.ImportReference(GetUnderlyingType(prefixParameter.ParameterType)); + + private static TypeReference GetUnderlyingType(ParameterDefinition prefixParameter) + => prefixParameter.ParameterType.IsByReference ? prefixParameter.ParameterType.GetElementType()! + : prefixParameter.ParameterType; + + private static MethodDefinition ThrowForFailureToFindMethod(TypeDefinition? type, MethodBase patchMethod) + => ThrowHelper.ThrowInvalidOperationException( + $"Failed to find method with type {type} and name {patchMethod.Name}"); + + private record struct PatchVariables + { + public VariableDefinition? Result, State; + } } \ No newline at end of file diff --git a/Source/PerformanceFish/Prepatching/FishPrepatchBase.cs b/Source/PerformanceFish/Prepatching/FishPrepatchBase.cs index 4d989fc..17cf458 100644 --- a/Source/PerformanceFish/Prepatching/FishPrepatchBase.cs +++ b/Source/PerformanceFish/Prepatching/FishPrepatchBase.cs @@ -4,23 +4,73 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. extern alias nuget; +using System.Linq; +using Mono.Cecil; using nuget::JetBrains.Annotations; +using PerformanceFish.ModCompatibility; namespace PerformanceFish.Prepatching; [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors | ImplicitUseTargetFlags.WithMembers)] public abstract class FishPrepatchBase : SingletonFactory, IExposable, IHasDescription { - private bool _enabled; + private bool + _enabled, + _isActive; + + private string? _descriptionWithNotes; + + private FishPrepatchBase[]? _resolvedLinkedPrepatches; + private FishPatch[]? _resolvedLinkedHarmonyPatches; + + internal int IDNumber { get; } + + public virtual int Priority => HarmonyLib.Priority.Normal; + public virtual List IncompatibleModIDs { get; } = []; + + public virtual List LinkedPatches { get; } = []; + + private FishPrepatchBase[] ResolvedLinkedPrepatches + => _resolvedLinkedPrepatches ??= LinkedPatches + .Where(static type => type.IsAssignableTo(typeof(FishPrepatchBase))).Select(static type => Get(type)) + .ToArray(); + + private FishPatch[] ResolvedLinkedHarmonyPatches + => _resolvedLinkedHarmonyPatches ??= LinkedPatches + .Where(static type => type.IsAssignableTo(typeof(FishPatch))).Select(static type => FishPatch.Get(type)) + .ToArray(); + + public bool DisabledForModCompatibility => ActiveMods.ContainsAnyOf(IncompatibleModIDs); + public virtual string? Description => null; + public string? DescriptionWithNotes + => _descriptionWithNotes ??= !DisabledForModCompatibility + ? Description + : Description + + $"{(Description?.EndsWith(".") ?? true ? "" : ".")} Disabled for compatibility with { + IncompatibleModIDs.Select(static id => ActiveMods.TryGetModMetaData(id)?.Name) + .Where(static id => id != null).ToCommaList(true)}"; + public virtual string? Name => null; public virtual bool Enabled { - get => _enabled; - set => _enabled = value; + get => _enabled && !DisabledForModCompatibility; + set + { + if (_enabled == value) + return; + + _enabled = value; + + for (var i = 0; i < ResolvedLinkedPrepatches.Length; i++) + ResolvedLinkedPrepatches[i].Enabled = value; + + for (var i = 0; i < ResolvedLinkedHarmonyPatches.Length; i++) + ResolvedLinkedHarmonyPatches[i].Enabled = value; + } } public virtual bool DefaultState @@ -28,8 +78,38 @@ public abstract class FishPrepatchBase : SingletonFactory, IEx [Pure] get => true; } + public virtual bool ShowSettings => true; + + public bool IsActive => _isActive; + // ReSharper disable once VirtualMemberCallInConstructor - protected FishPrepatchBase() => _enabled = DefaultState; + protected FishPrepatchBase() + { + _enabled = DefaultState; + IDNumber = Interlocked.Increment(ref _idCounter); + + if (OnAssembliesLoaded.Loaded) + _isActive = FishStash.Get.IsPatchActive(this); + } + + protected internal abstract void ApplyPatch(ModuleDefinition module); + + protected internal virtual void OnPatchingCompleted() + { + } + + protected internal virtual void BeforeHarmonyPatching() + { + } public virtual void ExposeData() => Scribe_Values.Look(ref _enabled, "enabled", DefaultState); + + internal static Comparer PriorityComparer + = Comparer.Create(static (x, y) => y.Priority.CompareTo(x.Priority)); + + protected static TypeDefinition ThrowForFailureToFindType(Type patchType) + => ThrowHelper.ThrowInvalidOperationException( + $"Failed to find type with namespace {patchType.Namespace} and name {patchType.Name}"); + + private static volatile int _idCounter; } \ No newline at end of file diff --git a/Source/PerformanceFish/Prepatching/FishPrepatchExtensions.cs b/Source/PerformanceFish/Prepatching/FishPrepatchExtensions.cs new file mode 100644 index 0000000..50a2373 --- /dev/null +++ b/Source/PerformanceFish/Prepatching/FishPrepatchExtensions.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Mono.Cecil; + +namespace PerformanceFish.Prepatching; + +public static class FishPrepatchExtensions +{ + internal static void ApplyPatches(this ClassWithFishPrepatches[] allPrepatchClasses, ModuleDefinition module) + { + var prepatches = ToSortedList(allPrepatchClasses, true); + + foreach (var prepatch in prepatches) + { + if (!prepatch.Enabled) + continue; + + try + { + prepatch.ApplyPatch(module); + } + catch (Exception ex) + { + Log.Error($"Exception while applying prepatch '{prepatch}' for '{PerformanceFishMod.NAME}':\n{ex}"); + } + } + } + + internal static void BeforeHarmonyPatching(this ClassWithFishPrepatches[] allPrepatchClasses) + => allPrepatchClasses.FinalizePrepatchAction(static patch => patch.BeforeHarmonyPatching()); + + internal static void OnPatchingCompleted(this ClassWithFishPrepatches[] allPrepatchClasses) + => allPrepatchClasses.FinalizePrepatchAction(static patch => patch.OnPatchingCompleted()); + + private static void FinalizePrepatchAction(this ClassWithFishPrepatches[] allPrepatchClasses, + Action action) + { + var prepatches = ToSortedList(allPrepatchClasses, false); + + foreach (var prepatch in prepatches) + { + if (!prepatch.IsActive) + continue; + + try + { + action(prepatch); + } + catch (Exception ex) + { + Log.Error($"Exception while finalizing prepatch '{prepatch}' for '{PerformanceFishMod.NAME}':\n{ex}"); + } + } + } + + private static List ToSortedList(this ClassWithFishPrepatches[] allPrepatchClasses, + bool checkEnabledState) + { + var prepatches = new List(); + + foreach (var prepatchClass in allPrepatchClasses) + { + if (!checkEnabledState || prepatchClass.Enabled) + prepatches.AddRange(prepatchClass.Patches.All.Values); + } + + prepatches.Sort(FishPrepatchBase.PriorityComparer); + return prepatches; + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Prepatching/FishPrepatchHolder.cs b/Source/PerformanceFish/Prepatching/FishPrepatchHolder.cs index 64bcc8e..b436f83 100644 --- a/Source/PerformanceFish/Prepatching/FishPrepatchHolder.cs +++ b/Source/PerformanceFish/Prepatching/FishPrepatchHolder.cs @@ -5,12 +5,6 @@ extern alias nuget; using System.Collections.Concurrent; -using System.Diagnostics; -using Mono.Cecil; -using Mono.Cecil.Cil; -using Mono.Cecil.Rocks; -using Mono.Collections.Generic; -using MonoMod.Utils; using nuget::JetBrains.Annotations; namespace PerformanceFish.Prepatching; @@ -23,366 +17,6 @@ public class FishPrepatchHolder : IExposable, IEnumerable public T Get() where T : FishPrepatchBase => (T)All[typeof(T)]; public void Add(FishPrepatchBase patch) => All[patch.GetType()] = patch; - public void PatchAll(ModuleDefinition module) - { - var patches = All.Values; - foreach (var patch in patches) - { - if (!patch.Enabled) - continue; - - try - { - switch (patch) - { - case FishPrepatch methodPatch: - ApplyMethodPatch(module, methodPatch); - break; - case FishClassPrepatch classPatch: - ApplyClassPatch(module, classPatch); - break; - } - } - catch (Exception ex) - { - Log.Error($"Exception while applying prepatches for {PerformanceFishMod.NAME}:\n{ex}"); - } - } - } - - private static void ApplyMethodPatch(ModuleDefinition module, FishPrepatch patch) - { - // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - var patchMethod = patch.TargetMethodBase - ?? ThrowHelper.ThrowInvalidOperationException( - $"Patch {patch.GetType().FullName} has no target method."); - - if (patchMethod.IsGenericMethod && patchMethod is MethodInfo info) - patchMethod = info.GetGenericMethodDefinition(); - - var patchType = patchMethod.ReflectedType - ?? ThrowHelper.ThrowInvalidOperationException( - $"Method {patchMethod.FullDescription()} has no declaring type."); - - if (patchMethod.ReflectedType != patchMethod.DeclaringType) - { - ThrowHelper.ThrowInvalidOperationException($"Tried to apply patch on method { - patchMethod.Name} for type {patchMethod.ReflectedType}, but it is only declared in { - patchMethod.DeclaringType}."); - } - - if (patchType.IsGenericType) - { - patchType = patchType.GetGenericTypeDefinition(); - patchMethod = patchMethod is MethodInfo - ? AccessTools.DeclaredMethod(patchType, patchMethod.Name, patchMethod.GetParameters().GetTypes()) - : AccessTools.Constructor(patchType, patchMethod.GetParameters().GetTypes(), patchMethod.IsStatic); - } - - DebugLog.Message($"Applying prepatch {patch.GetType().FullName} on method { - patchMethod.FullDescription()}"); - - var targetType = module.GetTypeDefinition(patchType) ?? ThrowForFailureToFindType(patchType); - var targetMethod = targetType.GetMethod(patchMethod.Name, patchMethod.GetParameters().GetTypes()) - ?? ThrowForFailureToFindMethod(targetType, patchMethod); - - // PrepatchManager.AddSecurityAttributes(targetMethod.SecurityDeclarations, module, targetMethod.FullName); - - var ilProcessor = targetMethod.Body.GetILProcessor(); - - try - { - patch.Transpiler(ilProcessor, module); - } - catch (Exception e) - { - Log.Error($"Exception while applying transpiler for {patch.GetType().FullName}\n{e}\n{ - new StackTrace() /*StackTraceUtility.ExtractStackTrace()*/}"); - } - - var patchVariables = new PatchVariables(); - - if (patch.PrefixMethodInfo is { } prefixMethod) - { - try - { - ApplyPrefix(prefixMethod, ilProcessor, module, ref patchVariables); - } - catch (Exception e) - { - Log.Error($"Exception while applying prefix for {patch.GetType().FullName}\n{e}\n{ - new StackTrace() /*StackTraceUtility.ExtractStackTrace()*/}"); - } - } - - if (patch.PostfixMethodInfo is { } postfixMethod) - { - try - { - ApplyPostfix(postfixMethod, ilProcessor, module, ref patchVariables); - } - catch (Exception e) - { - Log.Error($"Exception while applying postfix for {patch.GetType().FullName}\n{e}\n{ - new StackTrace() /*StackTraceUtility.ExtractStackTrace()*/}"); - } - } - - targetMethod.Body.OptimizeMacros(); - } - - private static void ApplyPrefix(MethodInfo prefixMethodInfo, ILProcessor ilProcessor, ModuleDefinition module, - ref PatchVariables patchVariables) - { - if (prefixMethodInfo.ReturnType != typeof(bool) && prefixMethodInfo.ReturnType != typeof(void)) - { - ThrowHelper.ThrowArgumentException($"Prefix has invalid return type: { - prefixMethodInfo.ReturnType.FullName}. Must be void or bool"); - } - - var instructions = ilProcessor.instructions; - var targetMethod = ilProcessor.GetMethod(); - - var genericParameters = GetGenericParameters(targetMethod); - var prefixParameters = prefixMethodInfo.GetParameters(); - - var resultParameter = prefixParameters.TryGetNamed("__result"); - VerifyResultParameter(module, resultParameter, targetMethod, true); - - var stateParameter = prefixParameters.TryGetNamed("__state"); - VerifyStateParameter(stateParameter); - - // var skipOriginalFlag = ilProcessor.DeclareLocal(typeof(bool)); - var resultVariable = GetResultVariable(ilProcessor, ref patchVariables, resultParameter, targetMethod); - var stateVariable = GetStateVariable(ilProcessor, ref patchVariables, stateParameter); - var startOfOriginal = instructions[0]; - - var index = 0; - foreach (var parameter in prefixParameters) - { - ilProcessor.InsertAt(index++, GetInstructionForParameter(module, parameter, resultParameter, resultVariable, - stateParameter, stateVariable, targetMethod)); - } - - if (genericParameters != null) - { - ilProcessor.InsertAt(index++, OpCodes.Call, - module.ImportMethodWithGenericArguments(prefixMethodInfo, genericParameters)); - } - else - { - ilProcessor.InsertAt(index++, OpCodes.Call, prefixMethodInfo); - } - - if (prefixMethodInfo.ReturnType == typeof(bool)) - { - ilProcessor.InsertAt(index++, (OpCodes.Brtrue_S, startOfOriginal)); - - if (resultVariable != null) - { - ilProcessor.InsertAt(index++, (OpCodes.Ldloc_S, resultVariable)); - } - else if (targetMethod.ReturnType.FullName != module.TypeSystem.Void.FullName) - { - Log.Error($"Prefix {prefixMethodInfo.FullDescription() - } with return type of bool must assign to a __result parameter"); - } - - ilProcessor.InsertAt(index, OpCodes.Ret); - } - } - - private static VariableDefinition? GetStateVariable(ILProcessor ilProcessor, ref PatchVariables patchVariables, - ParameterInfo? stateParameter) - => stateParameter != null - ? patchVariables.State ??= ilProcessor.DeclareLocal(GetUnderlyingType(stateParameter.ParameterType)) - : null; - - private static VariableDefinition? GetResultVariable(ILProcessor ilProcessor, ref PatchVariables patchVariables, - ParameterInfo? resultParameter, MethodDefinition targetMethod) - => resultParameter != null - ? patchVariables.Result ??= ilProcessor.DeclareLocal(targetMethod.ReturnType) - : null; - - private static void VerifyStateParameter(ParameterInfo? stateParameter) - { - if (stateParameter is { ParameterType.IsByRef: false }) - ThrowHelper.ThrowArgumentException("__state parameter must be declared with ref or out keyword."); - } - - private static Collection? GetGenericParameters(MethodDefinition targetMethod) - => targetMethod.HasGenericParameters ? targetMethod.GenericParameters - : targetMethod.DeclaringType.HasGenericParameters ? targetMethod.DeclaringType.GenericParameters - : null; - - private static Type GetUnderlyingType(Type type) => type.IsByRef ? type.GetElementType()! : type; - - private static void ApplyPostfix(MethodInfo postfixMethodInfo, ILProcessor ilProcessor, ModuleDefinition module, - ref PatchVariables patchVariables) - { - var targetMethod = ilProcessor.GetMethod(); - - if (postfixMethodInfo.ReturnType != typeof(void) - && !postfixMethodInfo.ReturnType.Is(targetMethod.ReturnType)) - { - ThrowHelper.ThrowArgumentException($"Postfix has invalid return type: { - postfixMethodInfo.ReturnType.FullName}. Must be void or {targetMethod.ReturnType}"); - } - - var instructions = ilProcessor.instructions; - var genericParameters = GetGenericParameters(targetMethod); - var postfixParameters = postfixMethodInfo.GetParameters(); - - var resultParameter = postfixParameters.TryGetNamed("__result"); - VerifyResultParameter(module, resultParameter, targetMethod, false); - - var stateParameter = postfixParameters.TryGetNamed("__state"); - // Verify? It would make sense to match against prefix state - - // var skipOriginalFlag = ilProcessor.DeclareLocal(typeof(bool)); - var resultVariable = GetResultVariable(ilProcessor, ref patchVariables, resultParameter, targetMethod); - var stateVariable = GetStateVariable(ilProcessor, ref patchVariables, stateParameter); - - var postfixInstructions = new Collection(); - - if (resultVariable != null) - postfixInstructions.Add((OpCodes.Stloc_S, resultVariable)); - - foreach (var parameter in postfixParameters) - { - postfixInstructions.Add(GetInstructionForParameter(module, parameter, resultParameter, resultVariable, - stateParameter, stateVariable, targetMethod)); - } - - if (genericParameters != null) - { - postfixInstructions.Add((OpCodes.Call, - module.ImportMethodWithGenericArguments(postfixMethodInfo, genericParameters))); - } - else - { - postfixInstructions.Add((OpCodes.Call, postfixMethodInfo)); - } - - if (postfixMethodInfo.ReturnType == typeof(void) && resultVariable != null) - postfixInstructions.Add((OpCodes.Ldloc_S, resultVariable)); - - // TODO: use label instead of inserting multiple times - for (var i = 0; i < instructions.Count; i++) - { - if (instructions[i].OpCode != OpCodes.Ret) - continue; - - ilProcessor.InsertRange(i, postfixInstructions); - - i += postfixInstructions.Count; - } - } - - private static ITuple GetInstructionForParameter(ModuleDefinition module, ParameterInfo parameter, - ParameterInfo? resultParameter, VariableDefinition? resultVariable, ParameterInfo? stateParameter, - VariableDefinition? stateVariable, MethodDefinition targetMethod) - => parameter == resultParameter - ? (parameter.ParameterType.IsByRef ? OpCodes.Ldloca_S : OpCodes.Ldloc_S, resultVariable) - : parameter == stateParameter - ? (parameter.ParameterType.IsByRef ? OpCodes.Ldloca_S : OpCodes.Ldloc_S, stateVariable) - : GetMatchingTargetParameter(targetMethod, parameter, module); - - private static void VerifyResultParameter(ModuleDefinition module, ParameterInfo? resultParameter, - MethodDefinition targetMethod, bool requiresByRef) - { - if (resultParameter is null) - return; - - if (targetMethod.ReturnType.IsGeneric() ? !GetUnderlyingType(resultParameter.ParameterType).IsGeneric() - : !GetUnderlyingType(resultParameter.ParameterType).Is(targetMethod.ReturnType)) - { - // var firstType = targetMethod.ReturnType; - // var secondType = GetUnderlyingType(resultParameter.ParameterType); - // Log.Message($"Type FullName: {firstType.FullName - // }: ContainsGenericParameter: {firstType.ContainsGenericParameter - // }, HasGenericParameters :{firstType.HasGenericParameters - // }, IsGenericInstance: {firstType.IsGenericInstance - // }, Type FullName: {secondType.FullName - // } ContainsGenericParameters: {secondType.ContainsGenericParameters - // }, IsGenericType {secondType.IsGenericType - // }, IsConstructedGenericType: {secondType.IsConstructedGenericType}"); - - ThrowHelper.ThrowArgumentException($"__result parameter has invalid type: { - resultParameter.ParameterType.FullName}. Must match the target's return type: { - targetMethod.ReturnType.FullName}"); - } - if (requiresByRef && !resultParameter.ParameterType.IsByRef) - { - ThrowHelper.ThrowArgumentException( - "__result parameter must be declared with ref or out keyword."); - } - } - - private record struct PatchVariables - { - public VariableDefinition? Result, State; - } - - private static ITuple GetMatchingTargetParameter(MethodDefinition targetMethod, - ParameterInfo parameter, ModuleDefinition module) - { - var targetParameters = targetMethod.Parameters; - var targetParameter = parameter.Name == "__instance" - ? targetMethod.Body.ThisParameter - : targetParameters.TryGetNamed(parameter.Name); - if (targetParameter is null) - { - return ThrowHelper.ThrowArgumentException($"No parameter named { - parameter.Name} found in target method"); - } - - var targetParameterType = targetParameter.ParameterType; - var patchParameterType = parameter.ParameterType; - - return GetUnderlyingType(targetParameter).Name != GetUnderlyingType(patchParameterType).Name - ? ThrowHelper.ThrowArgumentException($"Parameter has invalid type: { - patchParameterType.FullName}. Must match the target method parameter's type: { - targetParameterType.FullName}") - : (patchParameterType.IsByRef && !targetParameter.ParameterType.IsByReference - ? OpCodes.Ldarga_S - : OpCodes.Ldarg_S, targetParameter); - } - - private static TypeReference GetUnderlyingType(ParameterInfo prefixParameter, ModuleDefinition module) - => module.ImportReference(GetUnderlyingType(prefixParameter.ParameterType)); - - private static TypeReference GetUnderlyingType(ParameterDefinition prefixParameter) - => prefixParameter.ParameterType.IsByReference ? prefixParameter.ParameterType.GetElementType()! - : prefixParameter.ParameterType; - - private static TypeDefinition ThrowForFailureToFindType(Type patchType) - => ThrowHelper.ThrowInvalidOperationException( - $"Failed to find type with namespace {patchType.Namespace} and name {patchType.Name}"); - - private static MethodDefinition ThrowForFailureToFindMethod(TypeDefinition? type, MethodBase patchMethod) - => ThrowHelper.ThrowInvalidOperationException( - $"Failed to find method with type {type} and name {patchMethod.Name}"); - - private static void ApplyClassPatch(ModuleDefinition module, FishClassPrepatch patch) - { - // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - var patchType = patch.Type - ?? ThrowHelper.ThrowInvalidOperationException( - $"Patch {patch.GetType().FullName} has no target type."); - - if (patchType.IsGenericType) - patchType = patchType.GetGenericTypeDefinition(); - - DebugLog.Message($"Applying prepatch {patch.GetType().FullName} on type { - patchType.FullName}"); - - var targetType = module.GetType(patchType.Namespace, patchType.Name) ?? ThrowForFailureToFindType(patchType); - - // PrepatchManager.AddSecurityAttributes(targetType.SecurityDeclarations, module); - - patch.FreePatch(targetType); - } public FishPrepatchBase this[Type type] => All[type]; diff --git a/Source/PerformanceFish/Prepatching/FishStash.cs b/Source/PerformanceFish/Prepatching/FishStash.cs index b7146f9..c734a43 100644 --- a/Source/PerformanceFish/Prepatching/FishStash.cs +++ b/Source/PerformanceFish/Prepatching/FishStash.cs @@ -3,19 +3,28 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +using System.Linq; using System.Runtime.InteropServices; namespace PerformanceFish.Prepatching; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] -public struct FishStash +public unsafe struct FishStash : IEquatable { - private const string ANCHOR_STRING_VALUE = "F1shSt4sh69"; + private const string ANCHOR_STRING_VALUE = "F1shSt4sh6912345"; + private const int ANCHOR_STRING_LENGTH = 16; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 11)] - private string? _anchorString; - public UnhandledMessageQueue? UnhandledMessages; - public GCHandle MessageQueueGCHandle; + private fixed char _anchorString[ANCHOR_STRING_LENGTH]; + + internal GCHandle MessageQueueGCHandle; + + private int _activePrepatchCount; + private IntPtr _activePrepatches; + + public UnhandledMessageQueue? UnhandledMessages + => MessageQueueGCHandle != default && MessageQueueGCHandle.IsAllocated + ? Unsafe.As(MessageQueueGCHandle.Target) + : null; public static ref FishStash Get => ref StaticHandle != default @@ -29,8 +38,12 @@ public struct FishStash ?? ThrowHelper.ThrowInvalidOperationException("Failed to fetch internal FishStashHandle."); ref var stash = ref InternalFishStashHandle.Stash; - if (stash._anchorString != ANCHOR_STRING_VALUE) - ThrowHelper.ThrowInvalidDataException($"FishStash started with invalid anchor: {stash._anchorString}"); + fixed (char* anchor = stash._anchorString) + { + var anchorString = new string(anchor, 0, ANCHOR_STRING_LENGTH); + if (anchorString != ANCHOR_STRING_VALUE) + ThrowHelper.ThrowInvalidDataException($"FishStash started with invalid anchor: {anchorString}"); + } return ref stash; } @@ -41,18 +54,53 @@ public struct FishStash internal static nint StaticHandle { get; private set; } - internal static unsafe ref FishStash FromHandle(IntPtr handle) => ref Unsafe.AsRef(handle.ToPointer()); + internal static ref FishStash FromHandle(IntPtr handle) => ref Unsafe.AsRef(handle.ToPointer()); - private static unsafe void Create() + private static void Create() { + Guard.IsEqualTo(ANCHOR_STRING_VALUE.Length, ANCHOR_STRING_LENGTH); + if (StaticHandle != default) ThrowHelper.ThrowInvalidOperationException("Tried creating duplicate FishStash."); + + var handle = (FishStash*)Marshal.AllocHGlobal(sizeof(FishStash)); + Unsafe.InitBlockUnaligned(handle, 0, (uint)sizeof(FishStash)); - var handle = Marshal.AllocHGlobal(sizeof(FishStash)); - Unsafe.InitBlockUnaligned(handle.ToPointer(), 0, (uint)sizeof(FishStash)); - ((FishStash*)handle)->_anchorString = ANCHOR_STRING_VALUE; - - StaticHandle = handle; + for (var i = 0; i < ANCHOR_STRING_VALUE.Length; i++) + handle->_anchorString[i] = ANCHOR_STRING_VALUE[i]; + + StaticHandle = (nint)handle; + } + + internal void InitializeActivePrepatchIDs() + { + var activePrepatches = PerformanceFishMod.AllPrepatchClasses!.SelectMany(static patchClass + => patchClass.Patches.Where(static patch + => patch.Enabled).Select(static patch => patch.IDNumber)).ToArray(); + + var activePrepatchCount = activePrepatches.Length; + _activePrepatchCount = activePrepatchCount; + _activePrepatches = Marshal.AllocHGlobal(sizeof(int) * activePrepatchCount); + Marshal.Copy(activePrepatches, 0, _activePrepatches, activePrepatchCount); + } + + internal void Release() => Marshal.FreeHGlobal(_activePrepatches); + + internal bool IsPatchActive(FishPrepatchBase patch) + { + if (_activePrepatchCount <= 0) + return false; + + var patchID = patch.IDNumber; + + var activePrepatches = (int*)_activePrepatches; + for (var i = _activePrepatchCount; i-- > 0;) + { + if (activePrepatches[i] == patchID) + return true; + } + + return false; } static FishStash() @@ -60,4 +108,26 @@ public struct FishStash if (!Log.Ready && InternalFishStashHandle is null) Create(); } + + public override bool Equals(object? obj) => obj is FishStash stash && Equals(stash); + + public override int GetHashCode() + => HashCode.Combine(MessageQueueGCHandle.GetHashCode(), _activePrepatchCount, _activePrepatches.GetHashCode()); + + public static bool operator ==(FishStash left, FishStash right) => left.Equals(right); + + public static bool operator !=(FishStash left, FishStash right) => !(left == right); + + public bool Equals(FishStash other) + { + fixed (char* anchor = _anchorString) + { + if (anchor != other._anchorString) + return false; + } + + return MessageQueueGCHandle == other.MessageQueueGCHandle + && _activePrepatchCount == other._activePrepatchCount + && _activePrepatches == other._activePrepatches; + } } \ No newline at end of file diff --git a/Source/PerformanceFish/Prepatching/FishStashHandle.cs b/Source/PerformanceFish/Prepatching/FishStashHandle.cs index 1ee32a4..b3297b3 100644 --- a/Source/PerformanceFish/Prepatching/FishStashHandle.cs +++ b/Source/PerformanceFish/Prepatching/FishStashHandle.cs @@ -7,9 +7,9 @@ using System.Runtime.InteropServices; namespace PerformanceFish.Prepatching; -public class FishStashHandle : IDisposable +public sealed class FishStashHandle : IDisposable { - private static readonly List _allHandles = new(); + private static readonly List _allHandles = []; public ref FishStash Stash => ref FishStash.FromHandle(Handle); @@ -25,17 +25,18 @@ public class FishStashHandle : IDisposable Handle = (nint)handle; } - private void ReleaseUnmanagedResources() + private void Dispose(bool _) { + Stash.Release(); Marshal.FreeHGlobal(Handle); _allHandles.Remove(Handle); } public void Dispose() { - ReleaseUnmanagedResources(); + Dispose(true); GC.SuppressFinalize(this); } - ~FishStashHandle() => ReleaseUnmanagedResources(); + ~FishStashHandle() => Dispose(false); } \ No newline at end of file diff --git a/Source/PerformanceFish/Prepatching/PrepatchManager.cs b/Source/PerformanceFish/Prepatching/PrepatchManager.cs index 3adc290..d2fc6ce 100644 --- a/Source/PerformanceFish/Prepatching/PrepatchManager.cs +++ b/Source/PerformanceFish/Prepatching/PrepatchManager.cs @@ -4,11 +4,9 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. extern alias nuget; -using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Security; -using System.Threading.Tasks; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; @@ -66,6 +64,8 @@ public static class PrepatchManager // SetReflectionOnly(fishAssembly, previousFishValue, out _); // } + Application.logMessageReceivedThreaded -= Verse.Log.Notify_MessageReceivedThreadedInternal; + AddAttributes(module); ModifyAllTypes(module); @@ -76,9 +76,9 @@ public static class PrepatchManager _ = FishSettings.Instance; - foreach (var prepatchClass in allPrepatchClasses) - prepatchClass.Patches.PatchAll(module); + allPrepatchClasses.ApplyPatches(module); + FishStash.Get.InitializeActivePrepatchIDs(); PatchingFinished = true; stopwatch.Stop(); Verse.Log.Message($"Performance Fish finished applying prepatches in { @@ -312,7 +312,7 @@ public static class PrepatchManager debuggableAttribute.Constructor = module.ImportReference(typeof(DebuggableAttribute) - .GetConstructor(new[] { typeof(bool), typeof(bool) })); + .GetConstructor([typeof(bool), typeof(bool)])); debuggableAttribute.ConstructorArguments.Clear(); debuggableAttribute.ConstructorArguments.Add(new(module.TypeSystem.Boolean, false)); diff --git a/Source/PerformanceFish/Rendering/GenDrawPatches.cs b/Source/PerformanceFish/Rendering/GenDrawPatches.cs index a4699e9..23148bb 100644 --- a/Source/PerformanceFish/Rendering/GenDrawPatches.cs +++ b/Source/PerformanceFish/Rendering/GenDrawPatches.cs @@ -4,9 +4,9 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. /*namespace PerformanceFish.Rendering; -public class GenDrawPatches : ClassWithFishPatches +public sealed class GenDrawPatches : ClassWithFishPatches { - public class DrawMeshNowOrLater_Patch : FishPatch + public sealed class DrawMeshNowOrLater_Patch : FishPatch { public override Delegate TargetMethodGroup { get; } = (Action)GenDraw.DrawMeshNowOrLater; @@ -26,7 +26,7 @@ public class GenDrawPatches : ClassWithFishPatches } } - public class DrawMeshNowOrLaterWithMatrix_Patch : FishPatch + public sealed class DrawMeshNowOrLaterWithMatrix_Patch : FishPatch { public override Delegate TargetMethodGroup { get; } = (Action)GenDraw.DrawMeshNowOrLater; diff --git a/Source/PerformanceFish/Rendering/GraphicPatches.cs b/Source/PerformanceFish/Rendering/GraphicPatches.cs index 31e7d6e..8479149 100644 --- a/Source/PerformanceFish/Rendering/GraphicPatches.cs +++ b/Source/PerformanceFish/Rendering/GraphicPatches.cs @@ -4,9 +4,9 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. /*namespace PerformanceFish.Rendering; -public class GraphicPatches : ClassWithFishPatches +public sealed class GraphicPatches : ClassWithFishPatches { - public class DrawMeshInt_Patch : FishPatch + public sealed class DrawMeshInt_Patch : FishPatch { public override MethodBase TargetMethodInfo { get; } = AccessTools.DeclaredMethod(typeof(Graphic), nameof(Graphic.DrawMeshInt)); @@ -17,7 +17,7 @@ public class GraphicPatches : ClassWithFishPatches } } - public class Shadow_DrawWorker_Patch : FishPatch + public sealed class Shadow_DrawWorker_Patch : FishPatch { public override MethodBase TargetMethodInfo { get; } = AccessTools.DeclaredMethod(typeof(Graphic_Shadow), nameof(Graphic_Shadow.DrawWorker)); diff --git a/Source/PerformanceFish/Rendering/PawnCacheRendererPatches.cs b/Source/PerformanceFish/Rendering/PawnCacheRendererPatches.cs index 010e664..565276b 100644 --- a/Source/PerformanceFish/Rendering/PawnCacheRendererPatches.cs +++ b/Source/PerformanceFish/Rendering/PawnCacheRendererPatches.cs @@ -5,13 +5,13 @@ /*namespace PerformanceFish.Rendering; -public class PawnCacheRendererPatches : ClassWithFishPatches +public sealed class PawnCacheRendererPatches : ClassWithFishPatches // The PawnCacheRenderer's Camera.Render() call has a considerable performance impact despite only being used for // rendering of single pawns. These are failed attempts at lowering its impact. { private static bool _optimizeCameraEnabled; - public class OnPostRender_Patch : FishPatch + public sealed class OnPostRender_Patch : FishPatch { public override MethodBase TargetMethodInfo => AccessTools.DeclaredMethod(typeof(PawnCacheRenderer), nameof(PawnCacheRenderer.OnPostRender)); @@ -33,7 +33,7 @@ public class PawnCacheRendererPatches : ClassWithFishPatches private static int? _defaultCullinkMask; } - public class RenderPawn_Patch : FishPatch + public sealed class RenderPawn_Patch : FishPatch { public override MethodBase TargetMethodInfo => AccessTools.DeclaredMethod(typeof(PawnCacheRenderer), nameof(PawnCacheRenderer.RenderPawn)); diff --git a/Source/PerformanceFish/RoomOptimizations.cs b/Source/PerformanceFish/RoomOptimizations.cs index a5613d1..876cce2 100644 --- a/Source/PerformanceFish/RoomOptimizations.cs +++ b/Source/PerformanceFish/RoomOptimizations.cs @@ -12,14 +12,15 @@ using RoomRoleCache using ContainedAndAdjacentThingsCache = PerformanceFish.Cache.ByIndex; +using OpCodes = System.Reflection.Emit.OpCodes; namespace PerformanceFish; -public class RoomOptimizations : ClassWithFishPatches, IHasDescription +public sealed class RoomOptimizations : ClassWithFishPatches, IHasDescription { public string Description { get; } = "Room stats related optimizations"; - public class Role_Patch : FirstPriorityFishPatch + public sealed class Role_Patch : FirstPriorityFishPatch { public override string Description { get; } = "Throttles room stats and role updating to happen at most once every 512 or so ticks per room, " @@ -38,13 +39,13 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription => i - 1 > 0 && codes[i - 1] == this_statsAndRoleDirty && codes[i].opcode == OpCodes.Brfalse_S - && codes[i].operand is Label, static code => new[] - { + && codes[i].operand is Label, static code => + [ code, FishTranspiler.This, FishTranspiler.Call(ShouldUpdate), FishTranspiler.IfFalse_Short((Label)code.operand) - }); + ]); } catch (Exception ex) { @@ -83,7 +84,7 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription } } - public class Owners_Patch : FirstPriorityFishPatch + public sealed class Owners_Patch : FirstPriorityFishPatch { public override string Description { get; } = "Caches room owner data and throttles them to only update at most once every ~512 or so ticks. " @@ -142,7 +143,7 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription { if (result is null) { - (Owners ??= new(0)).Clear(); + (Owners ??= []).Clear(); } else { @@ -153,7 +154,7 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription } else { - Owners = new(result); + Owners = [..result]; } } @@ -168,7 +169,7 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription } } - public class Regions_Patch : FirstPriorityFishPatch + public sealed class Regions_Patch : FirstPriorityFishPatch { public override string Description { get; } = "Minor optimization"; public override MethodBase TargetMethodInfo { get; } @@ -191,7 +192,7 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription } } - public class ContainedBeds_Patch : FirstPriorityFishPatch + public sealed class ContainedBeds_Patch : FirstPriorityFishPatch { public override string Description { get; } = "So RimWorld has a list of beds and a list of all things. For some reason Ludeon chose to have " @@ -225,7 +226,7 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription } } - public class ContainedAndAdjacentThings_Patch : FirstPriorityFishPatch + public sealed class ContainedAndAdjacentThings_Patch : FirstPriorityFishPatch { public override string Description { get; } = "Caching of room content info. Variable impact"; @@ -279,7 +280,7 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription var uniqueContainedThings = instance.uniqueContainedThings; var uniqueContainedThingsSet = instance.uniqueContainedThingsSet; uniqueContainedThingsSet.Clear(); - (cache.ListVersions ??= new()).Clear(); + (cache.ListVersions ??= []).Clear(); for (var i = 0; i < regions.Count; i++) { @@ -301,7 +302,7 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription if (cache.Things is { } list) list.ReplaceContentsWith(uniqueContainedThings); else - cache.Things = new(uniqueContainedThings); + cache.Things = [..uniqueContainedThings]; cache.SetDirty(false, instance); @@ -333,7 +334,7 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription var count = allThings.Count; for (var j = 0; j < count; j++) { - if (regionGrid[CellIndicesUtility.CellToIndex(allThings[j].Position, mapSizeX)] == region) + if (regionGrid[allThings[j].Position.CellToIndex(mapSizeX)] == region) uniqueContainedThings.Add(allThings[j]); else adjacentThings.Add(allThings[j]); @@ -376,7 +377,7 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription } } - public class RoomAt_Patch : FirstPriorityFishPatch + public sealed class RoomAt_Patch : FirstPriorityFishPatch { public override string? Description { get; } = "Minor optimization through inlining ond cleaning up of duplicate calls in the method. About 1/4 " @@ -390,8 +391,8 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Room? RoomAt(IntVec3 c, Map map, RegionType allowedRegionTypes = RegionType.Set_All) { - var mapSize = map.info.Size; - if ((uint)c.x >= (uint)mapSize.x || (uint)c.z >= (uint)mapSize.z) + var cellIndices = map.cellIndices; + if (((uint)c.x >= (uint)cellIndices.mapSizeX) | ((uint)c.z >= (uint)cellIndices.mapSizeZ)) return null; var regionAndRoomUpdater = map.regionAndRoomUpdater; @@ -401,11 +402,11 @@ public class RoomOptimizations : ClassWithFishPatches, IHasDescription } else if (regionAndRoomUpdater.AnythingToRebuild) { - Log.Warning($"Trying to get valid region at { - c.ToString()} but RegionAndRoomUpdater is disabled. The result may be incorrect."); + Verse.Log.Warning($"Trying to get valid region at { + c.ToString()} but RegionAndRoomUpdater is disabled. The result may be incorrect."); } - var region = map.regionGrid.regionGrid[(c.z * mapSize.x) + c.x]; + var region = map.regionGrid.regionGrid[(c.z * cellIndices.mapSizeX) + c.x]; return region is null || !region.valid || (region.type & allowedRegionTypes) == 0 ? null : region.District?.Room; diff --git a/Source/PerformanceFish/StoreUtilityPatches.cs b/Source/PerformanceFish/StoreUtilityPatches.cs deleted file mode 100644 index 7a6d8ab..0000000 --- a/Source/PerformanceFish/StoreUtilityPatches.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2023 bradson -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -namespace PerformanceFish; - -public class StoreUtilityPatches : ClassWithFishPatches -{ - public class IsInValidBestStorage_Patch : FishPatch - { - public override string Description { get; } - = "Patches IsInValidBestStorage to immediately remove things from haulables when they're found to be in " - + "the best storage. Default often has them checked several more times before removal happens"; - - public override Delegate TargetMethodGroup { get; } = StoreUtility.IsInValidBestStorage; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Postfix(Thing t, bool __result) - { - if (__result) - { - t.Map.listerHaulables.TryRemove(t); - } - /*else if (!t.IsInValidStorage()) - { - if (!StoreUtility.TryFindBestBetterStorageFor(t, null, t.Map, StoragePriority.Unstored, - Faction.OfPlayerSilentFail, out IntVec3 _, out IHaulDestination _, needAccurateResult: false)) - { - - } - }*/ - } - } - -#if threadingEnabled - public class IsGoodStoreCell_Patch : FishPatch - { - protected IsGoodStoreCell_Patch() => FishSettings.ThreadingPatches.Add(this); - public override bool DefaultState => false; - - public override string Description { get; } - = "Lock for threading. Only needed with threading enabled and off by default"; - - public override Delegate TargetMethodGroup { get; } = StoreUtility.IsGoodStoreCell; - - public static void Prefix() - { - if (FishSettings.ThreadingEnabled) - Monitor.Enter(Lock); - } - - public static Exception Finalizer(Exception __exception) - { - if (FishSettings.ThreadingEnabled) - Monitor.Exit(Lock); - return __exception; - } - - private static object Lock { get; } = new(); - } - - public class GetStatValue_Patch : FishPatch - { - protected GetStatValue_Patch() => FishSettings.ThreadingPatches.Add(this); - public override bool DefaultState => false; - - public override string Description { get; } - = "Lock for threading. Only needed with threading enabled and off by default"; - - public override Delegate TargetMethodGroup { get; } = StatExtension.GetStatValue; - - public static void Prefix() - { - if (FishSettings.ThreadingEnabled) - Monitor.Enter(Lock); - } - - public static Exception Finalizer(Exception __exception) - { - if (FishSettings.ThreadingEnabled) - Monitor.Exit(Lock); - return __exception; - } - - private static object Lock { get; } = new(); - } -#endif -} \ No newline at end of file diff --git a/Source/PerformanceFish/System/EqualityComparerOptimization.cs b/Source/PerformanceFish/System/EqualityComparerOptimization.cs index 5f0391b..3d509b3 100644 --- a/Source/PerformanceFish/System/EqualityComparerOptimization.cs +++ b/Source/PerformanceFish/System/EqualityComparerOptimization.cs @@ -4,14 +4,15 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; namespace PerformanceFish.System; -public class EqualityComparerOptimization : ClassWithFishPatches +public sealed class EqualityComparerOptimization : ClassWithFishPatches { - public class Optimization : FishPatch + public sealed class Optimization : FishPatch { public override string? Description { get; } = "Specialized EqualityComparers for slightly faster collection lookups on ValueTypes. Can reduce memory " @@ -74,7 +75,7 @@ public class EqualityComparerOptimization : ClassWithFishPatches } [Serializable] -public class EquatableValueTypeEqualityComparer : EqualityComparer where T : struct, IEquatable +public sealed class EquatableValueTypeEqualityComparer : EqualityComparer where T : struct, IEquatable { [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals(T x, T y) => x.Equals(y); @@ -85,71 +86,73 @@ public class EquatableValueTypeEqualityComparer : EqualityComparer where T /// /// Equals method for the comparer itself. /// - public override bool Equals(object obj) => obj is EquatableValueTypeEqualityComparer; + public override bool Equals(object? obj) => obj is EquatableValueTypeEqualityComparer; public override int GetHashCode() => GetType().Name.GetHashCode(); } [Serializable] -public class EquatableNullableEqualityComparer : EqualityComparer where T : struct, IEquatable +public sealed class EquatableNullableEqualityComparer : EqualityComparer where T : struct, IEquatable { [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals(T? x, T? y) => x.HasValue - ? y.HasValue && x.Value.Equals(y.Value) + ? y.HasValue && x.GetValueOrDefault().Equals(y.GetValueOrDefault()) : !y.HasValue; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode(T? obj) => !obj.HasValue ? 0 : HashCode.Get(obj); + public override int GetHashCode(T? obj) => !obj.HasValue ? 0 : HashCode.Get(obj.GetValueOrDefault()); /// /// Equals method for the comparer itself. /// - public override bool Equals(object obj) => obj is EquatableNullableEqualityComparer; + public override bool Equals(object? obj) => obj is EquatableNullableEqualityComparer; public override int GetHashCode() => GetType().Name.GetHashCode(); } [Serializable] -public class NullableEqualityComparer : EqualityComparer where T : struct +public sealed class NullableEqualityComparer : EqualityComparer where T : struct { [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals(T? x, T? y) => x.HasValue - ? y.HasValue && x.Value.Equals(y.Value) + ? y.HasValue && x.GetValueOrDefault().Equals(y.GetValueOrDefault()) : !y.HasValue; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode(T? obj) => !obj.HasValue ? 0 : HashCode.Get(obj); + public override int GetHashCode(T? obj) => !obj.HasValue ? 0 : HashCode.Get(obj.GetValueOrDefault()); /// /// Equals method for the comparer itself. /// - public override bool Equals(object obj) => obj is NullableEqualityComparer; + public override bool Equals(object? obj) => obj is NullableEqualityComparer; public override int GetHashCode() => GetType().Name.GetHashCode(); } [Serializable] -public class StringEqualityComparer : EqualityComparer +public sealed class StringEqualityComparer : EqualityComparer { [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override bool Equals(string x, string y) + [SuppressMessage("Globalization", "CA1307")] + [SuppressMessage("Globalization", "CA1309")] + public override bool Equals(string? x, string? y) => string.Equals(x, y); // already does a null check, so this skips the extra check that'd otherwise happen [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode(string obj) => obj.GetHashCode(); + public override int GetHashCode(string? obj) => obj?.GetHashCode() ?? 0; /// /// Equals method for the comparer itself. /// - public override bool Equals(object obj) => obj is StringEqualityComparer; + public override bool Equals(object? obj) => obj is StringEqualityComparer; public override int GetHashCode() => GetType().Name.GetHashCode(); } [Serializable] -public class IntEqualityComparer : EqualityComparer +public sealed class IntEqualityComparer : EqualityComparer { [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals(int x, int y) => x == y; @@ -160,13 +163,13 @@ public class IntEqualityComparer : EqualityComparer /// /// Equals method for the comparer itself. /// - public override bool Equals(object obj) => obj is IntEqualityComparer; + public override bool Equals(object? obj) => obj is IntEqualityComparer; public override int GetHashCode() => GetType().Name.GetHashCode(); } [Serializable] -public class UshortEqualityComparer : EqualityComparer +public sealed class UshortEqualityComparer : EqualityComparer { [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals(ushort x, ushort y) => x == y; @@ -177,13 +180,13 @@ public class UshortEqualityComparer : EqualityComparer /// /// Equals method for the comparer itself. /// - public override bool Equals(object obj) => obj is UshortEqualityComparer; + public override bool Equals(object? obj) => obj is UshortEqualityComparer; public override int GetHashCode() => GetType().Name.GetHashCode(); } [Serializable] -public class ReferenceEqualityComparer : EqualityComparer +public sealed class ReferenceEqualityComparer : EqualityComparer { public new static readonly ReferenceEqualityComparer Default = new(); @@ -196,13 +199,13 @@ public class ReferenceEqualityComparer : EqualityComparer /// /// Equals method for the comparer itself. /// - public override bool Equals(object obj) => obj is ReferenceEqualityComparer; + public override bool Equals(object? obj) => obj is ReferenceEqualityComparer; public override int GetHashCode() => GetType().Name.GetHashCode(); } [Serializable] -public class ValueTypeEqualityComparer : EqualityComparer where T : struct +public sealed class ValueTypeEqualityComparer : EqualityComparer where T : struct { public new static readonly ValueTypeEqualityComparer Default = new(); @@ -215,7 +218,7 @@ public class ValueTypeEqualityComparer : EqualityComparer where T : struct /// /// Equals method for the comparer itself. /// - public override bool Equals(object obj) => obj is ReferenceEqualityComparer; + public override bool Equals(object? obj) => obj is ReferenceEqualityComparer; public override int GetHashCode() => GetType().Name.GetHashCode(); } \ No newline at end of file diff --git a/Source/PerformanceFish/System/ReflectionCaching.cs b/Source/PerformanceFish/System/ReflectionCaching.cs index 614f60a..c99a38e 100644 --- a/Source/PerformanceFish/System/ReflectionCaching.cs +++ b/Source/PerformanceFish/System/ReflectionCaching.cs @@ -3,13 +3,13 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection.Emit; using System.Runtime.InteropServices; using System.Security; using System.Threading.Tasks; using PerformanceFish.Cache; +using PerformanceFish.ModCompatibility; using ConstructorInfoCache = PerformanceFish.Cache.ByReference, @@ -49,14 +49,14 @@ using TypeFullNameCache using ActivatorCache = PerformanceFish.Cache.ByReferenceUnclearable; -using ActivatorWithArgumentsCache - = PerformanceFish.Cache.ByReference, - PerformanceFish.System.ReflectionCaching.ActivatorPatches.ActivatorCacheValue>; +// using ActivatorWithArgumentsCache +// = PerformanceFish.Cache.ByReference, +// PerformanceFish.System.ReflectionCaching.ActivatorPatches.ActivatorCacheValue>; namespace PerformanceFish.System; -public class ReflectionCaching : ClassWithFishPatches +public sealed class ReflectionCaching : ClassWithFishPatches { static ReflectionCaching() // necessary to prevent recursion between patches and function pointer creation { @@ -85,15 +85,15 @@ public class ReflectionCaching : ClassWithFishPatches public BindingFlags Flags; } - public class TypePatches + public sealed class TypePatches { - public class GetField_Patch : FishPatch + public sealed class GetField_Patch : FishPatch { public override string Description { get; } = "Caches GetField lookups"; public override MethodBase TargetMethodInfo { get; } = AccessTools.Method(typeof(RuntimeType), nameof(RuntimeType.GetField), - new[] { typeof(string), typeof(BindingFlags) })!; + [typeof(string), typeof(BindingFlags)])!; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(Type __instance, string name, BindingFlags bindingAttr, out FieldInfo? __result, @@ -132,11 +132,12 @@ public class ReflectionCaching : ClassWithFishPatches = __result; } - public class GetMethodWithFlags_Patch : FishPatch + public sealed class GetMethodWithFlags_Patch : FishPatch { public override string Description { get; } = "Caches GetMethod lookups"; public override Expression TargetMethod { get; } + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed = static () => default(Type)!.GetMethod(null!, default(BindingFlags)); [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -168,11 +169,12 @@ public class ReflectionCaching : ClassWithFishPatches = __result; } - public class GetMethodWithTypes_Patch : FishPatch + public sealed class GetMethodWithTypes_Patch : FishPatch { public override string Description { get; } = "Caches GetMethod lookups"; public override Expression TargetMethod { get; } + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed = static () => default(Type)!.GetMethod(null!, null!); [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -201,7 +203,7 @@ public class ReflectionCaching : ClassWithFishPatches = __result; } - public class GetMethodWithFlagsAndTypes_Patch : FishPatch + public sealed class GetMethodWithFlagsAndTypes_Patch : FishPatch { public override string Description { get; } = "Caches GetMethod lookups"; @@ -244,7 +246,7 @@ public class ReflectionCaching : ClassWithFishPatches new(types)).Info = __result; } - public class GetConstructor_Patch : FishPatch + public sealed class GetConstructor_Patch : FishPatch { public override string Description { get; } = "Caches GetConstructor lookups"; @@ -285,11 +287,12 @@ public class ReflectionCaching : ClassWithFishPatches = __result; } - public class GetProperty_Patch : FishPatch + public sealed class GetProperty_Patch : FishPatch { public override string Description { get; } = "Caches GetProperty lookups"; public override Expression TargetMethod { get; } + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed = static () => default(Type)!.GetProperty(null!, default(BindingFlags)); [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -322,7 +325,7 @@ public class ReflectionCaching : ClassWithFishPatches = __result; } - public class GetFullName : FishPatch + public sealed class GetFullName : FishPatch { public override string? Description { get; } = "Normally averages at over 1000ns per call. Caching makes this about 30 times faster, including " @@ -354,7 +357,6 @@ public class ReflectionCaching : ClassWithFishPatches ref var centralCache = ref TypeFullNameCache.GetDirectly.GetOrAddReference(instance.TypeHandle); cache.Name = centralCache.Name; - // cache.Cached = centralCache.Cached; } return cache.Cached; @@ -374,14 +376,12 @@ public class ReflectionCaching : ClassWithFishPatches { ref var cache = ref TypeFullNameCache.GetExistingReference(__instance.TypeHandle); cache.Name = __result; - // cache.Cached = 1; lock (_lock) { ref var centralCache = ref TypeFullNameCache.GetDirectly.GetReference(__instance.TypeHandle); centralCache.Name = __result; - // centralCache.Cached = 1; } } @@ -390,6 +390,7 @@ public class ReflectionCaching : ClassWithFishPatches // probably never actually needed [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Globalization", "CA1308")] private static string ToLowerIfNeededForBindingFlags(string name, BindingFlags bindingAttr) => (bindingAttr & BindingFlags.IgnoreCase) != 0 ? name.ToLowerInvariant() : name; @@ -436,9 +437,9 @@ public class ReflectionCaching : ClassWithFishPatches } } - public class MonoCustomAttrs + public sealed class MonoCustomAttrs { - public class GetCustomAttributes : FishPatch + public sealed class GetCustomAttributes : FishPatch { public override string? Description { get; } = "Caches attributes for reflection lookups. Decent load time improvement."; @@ -544,18 +545,24 @@ public class ReflectionCaching : ClassWithFishPatches => _hashCode; } - public class ActivatorPatches + public sealed class ActivatorPatches { - public class CreateInstance_Type : FishPatch + public sealed class CreateInstance_Type : FishPatch { + public override List IncompatibleModIDs { get; } = [PackageIDs.MULTIPLAYER]; + public override string? Description { get; } = "Optimizes the parameterless Activator.CreateInstance method by invoking through specialized cached " + "delegates"; public override Delegate? TargetMethodGroup { get; } = (Func)Activator.CreateInstance; - public static bool Prefix(Type type, ref object __result) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Prefix(Type? type, ref object __result) { + if (type is null) + return true; + ref var cache = ref ActivatorCache.GetOrAddReference(type.TypeHandle); if (cache.Dirty @@ -570,7 +577,7 @@ public class ReflectionCaching : ClassWithFishPatches } } - // public class CreateInstance_Type_Args : FishPatch + // public sealed class CreateInstance_Type_Args : FishPatch // { // public override Delegate? TargetMethodGroup { get; } // = (Func)Activator.CreateInstance; @@ -669,9 +676,9 @@ public class ReflectionCaching : ClassWithFishPatches // } // } - public class FieldInfoPatches + public sealed class FieldInfoPatches { - public class GetValue_Patch : FishPatch + public sealed class GetValue_Patch : FishPatch { public override string Description { get; } = "Optimizes the GetValue method by invoking it through specialized cached delegates"; @@ -705,7 +712,7 @@ public class ReflectionCaching : ClassWithFishPatches } } - public class SetValue_Patch : FishPatch + public sealed class SetValue_Patch : FishPatch { public override string Description { get; } = "Optimizes the SetValue method by invoking it through specialized cached delegates"; @@ -718,29 +725,45 @@ public class ReflectionCaching : ClassWithFishPatches { ref var cache = ref FieldSetters.GetOrAddReference(__instance.FieldHandle); - if (cache.Dirty - && !FieldSetters.UpdateAsyncCache(ref cache, - __instance.FieldHandle, __instance)) - { + if (cache.Dirty && !TryUpdate(__instance, ref cache)) return true; - } cache.Result!(obj, value); return false; } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool TryUpdate(FieldInfo __instance, ref FieldInfoSetterCache cache) + => cache.Task != _fieldSetterFailureTask && __instance.IsConst() + ? ErrorForInvalidCallAttempt(__instance, ref cache) + : FieldSetters.UpdateAsyncCache(ref cache, + __instance.FieldHandle, __instance); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool ErrorForInvalidCallAttempt(FieldInfo info, ref FieldInfoSetterCache cache) + { + cache.Task = _fieldSetterFailureTask; + Log.Error($"FieldInfo.SetValue has been called on '{ + info.FullDescription()}'. This is a const that gets copied at compile time. An attempt at " + + $"setting a value will always fail."); + + return false; + } } + private static Task _fieldSetterFailureTask = new(static () => null!); + public static FieldGetter? MakeGetterDelegate(FieldInfo info) { try { - var dm = new DynamicMethod($"FieldGetter_{info.Name}", typeof(object), new[] { typeof(object) }, + var dm = new DynamicMethod($"FieldGetter_{info.Name}", typeof(object), [typeof(object)], GetOwnerType(info), true); var il = dm.GetILGenerator(); TryEmitFieldInstanceArgument(il, info); - il.Emit(info is { IsLiteral: true, IsInitOnly: false } + il.Emit(info.IsConst() ? FishTranspiler.Constant(info.GetRawConstantValue()) : FishTranspiler.Field(info)); @@ -755,7 +778,7 @@ public class ReflectionCaching : ClassWithFishPatches { Log.Error($"PerformanceFish failed to generate an optimized delegate for { info.FullDescription()}. Reverting to default behaviour instead.\n{ex}\n{ - new StackTrace() /*StackTraceUtility.ExtractStackTrace()*/}"); + Environment.StackTrace}"); return null; } } @@ -765,7 +788,7 @@ public class ReflectionCaching : ClassWithFishPatches try { var dm = new DynamicMethod($"FieldSetter_{info.Name}", typeof(void), - new[] { typeof(object), typeof(object) }, GetOwnerType(info), true); + [typeof(object), typeof(object)], GetOwnerType(info), true); var il = dm.GetILGenerator(); TryEmitFieldInstanceArgument(il, info); @@ -776,18 +799,17 @@ public class ReflectionCaching : ClassWithFishPatches { il.Emit(FishTranspiler.Call(typeof(ReflectionCaching), info.FieldType.IsNullable() ? nameof(UnboxNullableSafely) : nameof(UnboxSafely), - new[] { typeof(object) }, - new[] - { + [typeof(object)], + [ info.FieldType.IsNullable() // TODO: verify that this is actually correct ? Nullable.GetUnderlyingType(info.FieldType) : info.FieldType - })); + ])); } else { il.Emit(FishTranspiler.Call(typeof(ReflectionCaching), nameof(CastOrConvert), - generics: new[] { info.FieldType })); + generics: [info.FieldType])); } il.Emit(FishTranspiler.StoreField(info)); @@ -800,7 +822,7 @@ public class ReflectionCaching : ClassWithFishPatches { Log.Error($"PerformanceFish failed to generate an optimized delegate for { info.FullDescription()}. Reverting to default behaviour instead.\n{ex}\n{ - new StackTrace() /*StackTraceUtility.ExtractStackTrace()*/}"); + Environment.StackTrace}"); return null; } } @@ -839,9 +861,9 @@ public class ReflectionCaching : ClassWithFishPatches public delegate void FieldSetter(object? obj, object? value); } - public class MethodBasePatches + public sealed class MethodBasePatches { - public class Invoke_Patch : FishPatch + public sealed class Invoke_Patch : FishPatch { public override bool DefaultState => false; // TODO: fix @@ -885,6 +907,7 @@ public class ReflectionCaching : ClassWithFishPatches } // Similar to HarmonyLib.MethodInvoker, but correctly handles a couple of edge cases Harmony would throw on. + [SuppressMessage("Globalization", "CA1310")] public static MethodInvoker? MakeInvokeDelegate(MethodBase methodBase) { if (methodBase.DeclaringType is null && methodBase.Name.StartsWith("PadMethod")) @@ -902,7 +925,7 @@ public class ReflectionCaching : ClassWithFishPatches return null; var dm = new DynamicMethod($"MethodInvoker_{methodBase.Name}", typeof(object), - new[] { typeof(object), typeof(object[]) }, + [typeof(object), typeof(object[])], GetOwnerType(methodBase), true); var il = dm.GetILGenerator(); @@ -937,7 +960,7 @@ public class ReflectionCaching : ClassWithFishPatches { Log.Warning($"PerformanceFish failed to generate an optimized delegate for { Reflection.FullDescription(methodBase)}. Reverting to default behaviour instead.\n{ex}\n{ - new StackTrace() /*StackTraceUtility.ExtractStackTrace()*/}"); + Environment.StackTrace}"); return null; } } @@ -961,15 +984,14 @@ public class ReflectionCaching : ClassWithFishPatches il.Emit(parameterType.IsValueType ? FishTranspiler.Call(typeof(ReflectionCaching), parameterType.IsNullable() ? nameof(UnboxNullableSafely) : nameof(UnboxSafely), - new[] { typeof(object) }, - new[] - { + [typeof(object)], + [ parameterType.IsNullable() ? Nullable.GetUnderlyingType(parameterType) : parameterType - }) + ]) : FishTranspiler.Call(typeof(ReflectionCaching), nameof(CastOrConvert), - generics: new[] { parameterType })); + generics: [parameterType])); } private static void EmitInvokeByRefParameterLoad(ILGenerator il, Type parameterType) @@ -981,7 +1003,7 @@ public class ReflectionCaching : ClassWithFishPatches if (parameterType.IsNullable()) { il.Emit(FishTranspiler.Call(typeof(ReflectionCaching), nameof(VerifyNullableBoxObject), - generics: new[] { Nullable.GetUnderlyingType(parameterType) })); + generics: [Nullable.GetUnderlyingType(parameterType)])); il.Emit(FishTranspiler.UnboxAddress(parameterType)); } @@ -989,16 +1011,13 @@ public class ReflectionCaching : ClassWithFishPatches { il.Emit(parameterType.IsValueType ? FishTranspiler.Call(typeof(ReflectionCaching), nameof(UnboxRefSafely), - // new[] { typeof(object).MakeByRefType() }, - generics: new[] - { + generics: + [ /*parameterType.IsNullable() ? Nullable.GetUnderlyingType(parameterType) :*/ parameterType - }) - : FishTranspiler.Call(typeof(ReflectionCaching), nameof(CastRefSafely), - // new[] { typeof(object).MakeByRefType() }, - generics: new[] { parameterType })); + ]) + : FishTranspiler.Call(typeof(ReflectionCaching), nameof(CastRefSafely), generics: [parameterType])); } } @@ -1022,14 +1041,12 @@ public class ReflectionCaching : ClassWithFishPatches if (declaringType.IsValueType) { il.Emit(FishTranspiler.ArgumentAddress(0)); - il.Emit(FishTranspiler.Call(typeof(ReflectionCaching), nameof(UnboxRefSafely), - generics: new[] { declaringType })); + il.Emit(FishTranspiler.Call(typeof(ReflectionCaching), nameof(UnboxRefSafely), generics: [declaringType])); } else { il.Emit(FishTranspiler.This); - il.Emit(FishTranspiler.Call(typeof(ReflectionCaching), nameof(CastOrConvert), - generics: new[] { declaringType })); + il.Emit(FishTranspiler.Call(typeof(ReflectionCaching), nameof(CastOrConvert), generics: [declaringType])); } } diff --git a/Source/PerformanceFish/TextureLoadingPatches.cs b/Source/PerformanceFish/TextureLoadingPatches.cs index 22b1fa8..d6f0619 100644 --- a/Source/PerformanceFish/TextureLoadingPatches.cs +++ b/Source/PerformanceFish/TextureLoadingPatches.cs @@ -6,17 +6,17 @@ using System.IO; using Mono.Cecil; using Mono.Cecil.Cil; -using PerformanceFish.Pools; +using FisheryLib.Pools; using PerformanceFish.Prepatching; using RimWorld.IO; namespace PerformanceFish; -public class TextureLoadingPatches : ClassWithFishPrepatches +public sealed class TextureLoadingPatches : ClassWithFishPrepatches { public const string DDS_EXTENSION = ".dds"; - public class ModContentLoader_StaticConstructor : FishPrepatch + public sealed class ModContentLoader_StaticConstructor : FishPrepatch { public override string? Description { get; } = "Adds .dds to the accepted extensions for texture loading. Those files load faster than png, render at " @@ -30,7 +30,7 @@ public class TextureLoadingPatches : ClassWithFishPrepatches = ModContentLoader.AcceptableExtensionsTexture.Add(DDS_EXTENSION); } - public class ModContentLoader_LoadItem : FishPrepatch + public sealed class ModContentLoader_LoadItem : FishPrepatch { public override string? Description { get; } = "Adds .dds to the accepted extensions for texture loading. Those files load faster than png, render at " @@ -52,7 +52,7 @@ public class TextureLoadingPatches : ClassWithFishPrepatches } } - public class ModContentLoader_LoadTexture : FishPrepatch + public sealed class ModContentLoader_LoadTexture : FishPrepatch { public override string? Description { get; } = "Adds .dds to the accepted extensions for texture loading. Those files load faster than png, render at " @@ -146,7 +146,7 @@ public class TextureLoadingPatches : ClassWithFishPrepatches public static FileInfo? GetFileInfo(VirtualFile file) => file is FilesystemFile systemFile ? systemFile.fileInfo : null; - public class GetAllFilesForModPatch : FishPrepatch + public sealed class GetAllFilesForModPatch : FishPrepatch { public override string? Description { get; } = "Adds .dds to the accepted extensions for texture loading. Those files load faster than png, render at " @@ -162,7 +162,7 @@ public class TextureLoadingPatches : ClassWithFishPrepatches && method.DeclaringType.Name == "Dictionary`2"); ilProcessor.instructions[index].Operand = module.ImportReference(AccessTools.Constructor( - typeof(Dictionary), new[] { typeof(IEqualityComparer) })); + typeof(Dictionary), [typeof(IEqualityComparer)])); ilProcessor.InsertAt(index, OpCodes.Call, AccessTools.PropertyGetter(typeof(StringComparer), nameof(StringComparer.OrdinalIgnoreCase))); } diff --git a/Source/PerformanceFish/ThingCompCaching.cs b/Source/PerformanceFish/ThingCompCaching.cs index 1c4685b..2cb7e74 100644 --- a/Source/PerformanceFish/ThingCompCaching.cs +++ b/Source/PerformanceFish/ThingCompCaching.cs @@ -9,11 +9,15 @@ using IdeoForbidsCache namespace PerformanceFish; -public class ThingCompCaching : ClassWithFishPatches +public sealed class ThingCompCaching : ClassWithFishPatches { - public class CompAssignableToPawn_Bed_Patch : FishPatch + public sealed class CompAssignableToPawn_Bed_Patch : FishPatch { - public override string Description { get; } = "Caches results of CompAssignableToPawn_Bed.IdeoligionForbids"; + public override string Description { get; } + = "Caches results of CompAssignableToPawn_Bed.IdeoligionForbids" + .AppendWhen(!ModsConfig.IdeologyActive, ". Requires Ideology"); + + public override bool Enabled => base.Enabled && ModsConfig.IdeologyActive; public override MethodBase TargetMethodInfo { get; } = AccessTools.Method(typeof(CompAssignableToPawn_Bed), nameof(CompAssignableToPawn_Bed.IdeoligionForbids)); @@ -21,7 +25,7 @@ public class ThingCompCaching : ClassWithFishPatches [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(CompAssignableToPawn_Bed __instance, Pawn? pawn, ref bool __result, out bool __state) { - if (pawn is null) + if (pawn is null || __instance.Props.maxAssignedPawnsCount == 1) { __state = false; return true; @@ -54,16 +58,16 @@ public class ThingCompCaching : ClassWithFishPatches public record struct IdeoForbidsCacheValue { public bool Result; - private int _nextRefreshTick; - private int _assignedPawnsVersion; - private int _directRelationsVersion; - private List _assignedPawns; - private List _directRelations; + private int _nextRefreshTick = -2; + private int _assignedPawnsVersion = -2; + private int _directRelationsVersion = -2; + private List _assignedPawns = _nullPawns; + private List _directRelations = _nullRelations; public void Update(CompAssignableToPawn_Bed comp, Pawn pawn, bool result) { - _directRelations = pawn.relations.DirectRelations; - _assignedPawns = comp.assignedPawns; + _directRelations = pawn.relations?.DirectRelations ?? _nullRelations; + _assignedPawns = comp.assignedPawns ?? _nullPawns; _directRelationsVersion = _directRelations._version; Result = result; _assignedPawnsVersion = _assignedPawns._version; @@ -78,5 +82,12 @@ public class ThingCompCaching : ClassWithFishPatches || _assignedPawnsVersion != _assignedPawns._version || _directRelationsVersion != _directRelations._version; } + + private static readonly List _nullRelations = []; + private static readonly List _nullPawns = []; + + public IdeoForbidsCacheValue() + { + } } } \ No newline at end of file diff --git a/Source/PerformanceFish/ThingFilterPatches.cs b/Source/PerformanceFish/ThingFilterPatches.cs index 32dde8d..1ed9a5a 100644 --- a/Source/PerformanceFish/ThingFilterPatches.cs +++ b/Source/PerformanceFish/ThingFilterPatches.cs @@ -6,14 +6,15 @@ global using AllowedDefsListCache = PerformanceFish.Cache.ByReference; +using System.Linq; using PerformanceFish.Cache; namespace PerformanceFish; -public class ThingFilterPatches : ClassWithFishPatches +public sealed class ThingFilterPatches : ClassWithFishPatches { public override bool RequiresLoadedGameForPatching => true; - /*public class Allows_Patch : FishPatch + /*public sealed class Allows_Patch : FishPatch { public override Expression TargetMethod { get; } = static () => new ThingFilter().Allows(null as Thing); @@ -55,7 +56,7 @@ public class ThingFilterPatches : ClassWithFishPatches } }*/ - public class AllowedThingDefs_Patch : FishPatch + public sealed class AllowedThingDefs_Patch : FishPatch { public override string Description { get; } = "Small optimization. Returns a list instead of hashset for better performance when enumerating"; @@ -70,29 +71,31 @@ public class ThingFilterPatches : ClassWithFishPatches => Reflection.GetCodeInstructions(Replacement); } - public class AllStorableThingDefs_Patch : FishPatch + public sealed class AllStorableThingDefs_Patch : FishPatch { public override string Description { get; } = "Small optimization"; public override MethodBase TargetMethodInfo { get; } = AccessTools.PropertyGetter(typeof(ThingFilter), nameof(ThingFilter.AllStorableThingDefs)); - public static IEnumerable Replacement() - { - var allDefs = DefDatabase.defsList; - var count = allDefs.Count; - for (var i = 0; i < count; i++) - { - if (allDefs[i].EverStorable(true)) - yield return allDefs[i]; - } - } + public static ThingDef[]? AllStorableThingDefsCache; + + public static IEnumerable Replacement() => AllStorableThingDefsCache ?? Update(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static IEnumerable Update() + => Current.ProgramState != ProgramState.Playing + ? Original() + : AllStorableThingDefsCache = Original().ToArray(); + + public static IEnumerable Original() + => DefDatabase.AllDefs.Where(static def => def.EverStorable(true)); public static CodeInstructions Transpiler(CodeInstructions codes) => Reflection.MakeReplacementCall(Replacement); } - public class CopyAllowancesFrom_Patch : FirstPriorityFishPatch + public sealed class CopyAllowancesFrom_Patch : FirstPriorityFishPatch { public override string Description { get; } = "Used by the WorkGiver_DoBill patch to allow for faster refreshing of its cache"; @@ -109,7 +112,7 @@ public class ThingFilterPatches : ClassWithFishPatches } } - public class SetAllow_Patch : FishPatch + public sealed class SetAllow_Patch : FishPatch { public override string Description { get; } = "Used by the WorkGiver_DoBill patch to allow for faster refreshing of its cache"; @@ -149,7 +152,7 @@ public class ThingFilterPatches : ClassWithFishPatches private static int ComparerMethod(ThingDef a, ThingDef b) => a.BaseMarketValue.CompareTo(b.BaseMarketValue); } - public class SetDisallowAll_Patch : FishPatch + public sealed class SetDisallowAll_Patch : FishPatch { public override string Description { get; } = "Used by the WorkGiver_DoBill patch to allow for faster refreshing of its cache"; @@ -161,7 +164,7 @@ public class ThingFilterPatches : ClassWithFishPatches public static void Postfix(ThingFilter __instance) => ForceSynchronizeCache(__instance); } - public class SetAllowAll_Patch : FishPatch + public sealed class SetAllowAll_Patch : FishPatch { public override string Description { get; } = "Used by the WorkGiver_DoBill patch to allow for faster refreshing of its cache"; @@ -173,31 +176,22 @@ public class ThingFilterPatches : ClassWithFishPatches public static void Postfix(ThingFilter __instance) => ForceSynchronizeCache(__instance); } - public class BestThingRequest_Patch : FishPatch + public sealed class BestThingRequest_Patch : FishPatch { + public override List LinkedPatches { get; } = [typeof(Listers.Things.ThingsMatching_Patch)]; + public override string? Description { get; } = "RimWorld is normally only capable of creating ThingRequests either for predefined groups or for single " + "defs. This patch makes ThingFilters return requests for multiple defs, improving performance for " + "cases where the fallback group would have to be something like all haulables. Refueling benefits a " + "lot from this. Requires the patch on ListerThings.ThingsMatching to function."; - public override bool Enabled - { - set - { - if (base.Enabled == value) - return; - - Get().Enabled = base.Enabled = value; - } - } - public override MethodBase TargetMethodInfo { get; } = AccessTools.PropertyGetter(typeof(ThingFilter), nameof(ThingFilter.BestThingRequest)); public static void Postfix(ThingFilter __instance, ref ThingRequest __result) { - if (__result.group is not (ThingRequestGroup.HaulableEver or ThingRequestGroup.Everything) + if (__result.group is not ThingRequestGroup.HaulableEver and not ThingRequestGroup.Everything || __instance.allowedDefs.Count > 16) { return; @@ -213,7 +207,7 @@ public class ThingFilterPatches : ClassWithFishPatches public record struct AllowedDefsListCacheValue : ICacheable, ICacheable { - public List Defs = new(); + public List Defs = []; private int _nextRefreshTick = -2; public bool Dirty @@ -230,7 +224,8 @@ public class ThingFilterPatches : ClassWithFishPatches public void Update(ref ThingFilter key, int tickOffset) { var allowedDefs = key.allowedDefs; - allowedDefs.Remove(null); // apparently happens + while (allowedDefs.Remove(null)) + ; // apparently happens Defs.ReplaceContentsWith(allowedDefs); diff --git a/Source/PerformanceFish/ThingOwnerUtilityCaching.cs b/Source/PerformanceFish/ThingOwnerUtilityCaching.cs index 58d8cac..64a36f4 100644 --- a/Source/PerformanceFish/ThingOwnerUtilityCaching.cs +++ b/Source/PerformanceFish/ThingOwnerUtilityCaching.cs @@ -10,10 +10,18 @@ using PerformanceFish.Prepatching; namespace PerformanceFish; -public class ThingOwnerUtilityPreCaching : ClassWithFishPrepatches +public sealed class ThingOwnerUtilityPreCaching : ClassWithFishPrepatches { - public class GetAllThingsRecursively_Patch : FishPrepatch + public sealed class GetAllThingsRecursively_Patch : FishPrepatch { + // https://github.com/jptrrs/HumanResources/blob/master/Source/Harmony/ThingOwnerUtility_GetAllThingsRecursively.cs + // Harmony patches on generics like there strip out information from the generic T argument, due to harmony + // replacing the method with one made with Reflection.Emit + // noted down as known edge case on the harmony wiki here: + // https://harmony.pardeike.net/articles/patching-edgecases.html#generics + public override List IncompatibleModIDs { get; } + = [ModCompatibility.PackageIDs.HUMAN_RESOURCES]; + public override string Description { get; } = "Caching of information about mostly unspawned pawns and things. Relatively large performance impact"; @@ -82,9 +90,9 @@ public class ThingOwnerUtilityPreCaching : ClassWithFishPrepatches + (map.listerThings.listsByGroup[(int)ThingRequestGroup.ThingHolder]?.Count ?? 0); } -/*public class ThingOwnerUtilityCaching : ClassWithFishPatches +/*public sealed class ThingOwnerUtilityCaching : ClassWithFishPatches { - public class GetAllThingsRecursively_Patch : FishPatch + public sealed class GetAllThingsRecursively_Patch : FishPatch { public override string Description { get; } = "Caching of information about mostly unspawned pawns and things. Relatively large performance impact"; diff --git a/Source/PerformanceFish/ThingPatches.cs b/Source/PerformanceFish/ThingPatches.cs new file mode 100644 index 0000000..63e90b5 --- /dev/null +++ b/Source/PerformanceFish/ThingPatches.cs @@ -0,0 +1,75 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; + +namespace PerformanceFish; + +public sealed class ThingPatches : ClassWithFishPrepatches +{ + public sealed class ExposeDataFixStuffPatch : FishPrepatch + { + public override string? Description { get; } + = "This is an attempt at better logging and fixing errors coming from missing stuff, like wood or cloth, " + + "after mod removal. Only runs when loading a save and only when detecting things in this erroneous " + + "state. No performance impact, unless the errors were causing issues that ended up fixed."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(Thing), nameof(Thing.ExposeData)); + + public static void Postfix(Thing __instance) + { + if (Scribe.mode == LoadSaveMode.LoadingVars + && __instance is { def: { MadeFromStuff: true } def, Stuff: null }) + { + TryFixStuff(__instance, def); + } + } + + private static void TryFixStuff(Thing __instance, ThingDef def) + { + string label; + try + { + label = __instance.ToString(); + } + catch (Exception e) + { + label = __instance.thingIDNumber.ToStringCached(); + Log.Error($"Exception trying to fetch label for thing with def '{def}':\n{e}"); + } + + Log.Error($"Thing '{label}' of def '{def}' is madeFromStuff but stuff=null. Assigning default."); + + __instance.SetStuffDirect(GenStuff.DefaultStuffFor(def)); + } + } + + public sealed class SpawnedPatch : FishPrepatch + { + public override string? Description { get; } + = "Optimization for this property reducing its amount of instructions by 2/3 without changing results"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredPropertyGetter(typeof(Thing), nameof(Thing.Spawned)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(MiscExtensions.IsSpawned); + } + + public sealed class MapHeldPatch : FishPrepatch + { + public override string? Description { get; } + = "Optimization for this property reducing its amount of instructions by 2/3 without changing results"; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredPropertyGetter(typeof(Thing), nameof(Thing.MapHeld)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(MiscExtensions.TryGetMapHeld); + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/ThoughtWorker_Precept_AltarSharingOptimization.cs b/Source/PerformanceFish/ThoughtWorker_Precept_AltarSharingOptimization.cs index 561b92c..da8d806 100644 --- a/Source/PerformanceFish/ThoughtWorker_Precept_AltarSharingOptimization.cs +++ b/Source/PerformanceFish/ThoughtWorker_Precept_AltarSharingOptimization.cs @@ -9,9 +9,9 @@ using SharedAltarCache namespace PerformanceFish; -public class ThoughtWorker_Precept_AltarSharingOptimization : ClassWithFishPatches +public sealed class ThoughtWorker_Precept_AltarSharingOptimization : ClassWithFishPatches { - public class SharedAltar_Patch : FirstPriorityFishPatch + public sealed class SharedAltar_Patch : FirstPriorityFishPatch { public override string Description { get; } = "Caches results of ideology's shared altar thoughtworker and throttles it to wait a minimum of 128 ticks " @@ -23,7 +23,7 @@ public class ThoughtWorker_Precept_AltarSharingOptimization : ClassWithFishPatch public static bool Prefix(ThoughtWorker_Precept_AltarSharing __instance, Pawn pawn, ref Thing? __result, out bool __state) { - if (!pawn.Spawned) + if (!pawn.IsSpawned()) { __result = null; return __state = false; @@ -60,7 +60,7 @@ public class ThoughtWorker_Precept_AltarSharingOptimization : ClassWithFishPatch thing = result; _nextLateRefreshTick = TickHelper.Add(3072, pawn.thingIDNumber, 2048); _nextEarlyRefreshTick = TickHelper.Add(128, pawn.thingIDNumber, 128); - _lister = pawn.Map.listerThings; + _lister = pawn.GetMap().listerThings; _allStructuresListVersion = _lister.listsByGroup[(int)ThingRequestGroup.BuildingArtificial]._version; } diff --git a/Source/PerformanceFish/Utility/CellIndexExtensions.cs b/Source/PerformanceFish/Utility/CellIndexExtensions.cs new file mode 100644 index 0000000..73e3338 --- /dev/null +++ b/Source/PerformanceFish/Utility/CellIndexExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace PerformanceFish.Utility; + +public static class CellIndexExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CellToIndex(this in IntVec3 cell, Map map) => cell.CellToIndex(map.cellIndices); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CellToIndex(this in IntVec3 cell, CellIndices cellIndices) + => cell.CellToIndex(cellIndices.mapSizeX); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CellToIndex(this in IntVec3 cell, int mapSizeX) => (cell.z * mapSizeX) + cell.x; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CellToIndex(this IntVec2 cell, Map map) => cell.CellToIndex(map.cellIndices); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CellToIndex(this IntVec2 cell, CellIndices cellIndices) + => cell.CellToIndex(cellIndices.mapSizeX); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CellToIndex(this IntVec2 cell, int mapSizeX) => (cell.z * mapSizeX) + cell.x; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CellToIndex(int x, int z, Map map) => CellToIndex(x, z, map.cellIndices); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CellToIndex(int x, int z, CellIndices cellIndices) => CellToIndex(x, z, cellIndices.mapSizeX); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CellToIndex(int x, int z, int mapSizeX) => (z * mapSizeX) + x; +} \ No newline at end of file diff --git a/Source/PerformanceFish/Utility/CollectionExtensions.cs b/Source/PerformanceFish/Utility/CollectionExtensions.cs index 3117500..482ce82 100644 --- a/Source/PerformanceFish/Utility/CollectionExtensions.cs +++ b/Source/PerformanceFish/Utility/CollectionExtensions.cs @@ -4,6 +4,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. using System.Diagnostics.CodeAnalysis; +using System.Linq; using Mono.Collections.Generic; namespace PerformanceFish.Utility; @@ -295,6 +296,8 @@ public static class CollectionExtensions return ThrowForNoElements(); } + + public static List AsOrToList(this IEnumerable enumerable) => enumerable as List ?? enumerable.ToList(); [DoesNotReturn] private static T ThrowForNoElements() diff --git a/Source/PerformanceFish/Utility/Exposable.cs b/Source/PerformanceFish/Utility/Exposable.cs index d54c750..089e351 100644 --- a/Source/PerformanceFish/Utility/Exposable.cs +++ b/Source/PerformanceFish/Utility/Exposable.cs @@ -9,10 +9,9 @@ namespace PerformanceFish.Utility; /// Scribe_Deep normally creates a new instance when loading data. Scribe on this class prevents that. Also includes a /// try-catch block /// -public class Exposable : IExposable +public sealed class Exposable(IExposable @this) : IExposable { - public readonly IExposable Parent; - public Exposable(IExposable @this) => Parent = @this; + public readonly IExposable Parent = @this; public void ExposeData() { @@ -27,7 +26,7 @@ public class Exposable : IExposable } public override string ToString() => TrimName(Parent.ToString()); - public override bool Equals(object obj) => obj.Equals(Parent); + public override bool Equals(object? obj) => Parent.Equals(obj); public override int GetHashCode() => Parent.GetHashCode(); public static void Scribe(IExposable iExposable, string label, bool trimLabel = true) diff --git a/Source/PerformanceFish/Utility/GUIHelper.cs b/Source/PerformanceFish/Utility/GUIHelper.cs new file mode 100644 index 0000000..dbc6f60 --- /dev/null +++ b/Source/PerformanceFish/Utility/GUIHelper.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace PerformanceFish.Utility; + +public static class GUIHelper +{ + /// + /// Unity calls OnGUI with EventType.Layout at the beginning and EventType.Repaint at the end of every frame. + /// Anything drawn within the Layout call in turn gets covered every time, making it a waste of CPU cycles. + /// + public static bool CanSkip + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Event.current.type == EventType.Layout; + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Utility/MiscExtensions.cs b/Source/PerformanceFish/Utility/MiscExtensions.cs new file mode 100644 index 0000000..0310507 --- /dev/null +++ b/Source/PerformanceFish/Utility/MiscExtensions.cs @@ -0,0 +1,140 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Security; +using Verse.Sound; + +namespace PerformanceFish.Utility; + +public static class MiscExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SecuritySafeCritical] + public static bool AsBool(this int value) // ref on parameter instead of var would enforce stack allocation + { + var temp = (byte)value; // bool is 32 bits in .NET, but the jit compiler only eliminates when casting to byte first + return Unsafe.As(ref temp); // result is a direct return with no instructions whatsoever + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SecuritySafeCritical] + public static int AsInt(this bool value) + { + var temp = value; + return Unsafe.As(ref temp); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsItem(this Thing thing) => thing.def.category == ThingCategory.Item; + + public static ISlotGroupParent? GetSlotGroupParent(this in IntVec3 position, Map map) + => map.haulDestinationManager.SlotGroupAt(position)?.parent; + + public static ISlotGroupParent? StoringSlotGroupParent(this Thing thing) + => thing.Position.GetSlotGroupParent(thing.GetMap()); + + public static int SlotGroupCellCount(this Thing thing) => ((ISlotGroupParent)thing).AllSlotCellsList().Count; + + public static List GetThingListUnchecked(this in IntVec3 cell, Map map) + => map.thingGrid.ThingsListAtFast(cell.CellToIndex(map)); + + public static List GetThingListUnchecked(this IntVec2 cell, Map map) + => map.thingGrid.ThingsListAtFast(cell.CellToIndex(map)); + + public static Region? RegionAtUnchecked(this IntVec3 c, Map map, + RegionType allowedRegionTypes = RegionType.Set_Passable) + { + map.regionAndRoomUpdater.TryRebuildDirtyRegionsAndRooms(); + + return map.regionGrid.regionGrid[c.CellToIndex(map)] is { valid: true } region + && (region.type & allowedRegionTypes) != RegionType.None + ? region + : null; + } + + public static Region? GetValidRegionAtUnchecked(this RegionGrid grid, in IntVec3 c) + { + var map = grid.map; + + map.regionAndRoomUpdater.TryRebuildDirtyRegionsAndRooms(); + return grid.regionGrid[c.CellToIndex(map)] is { valid: true } region ? region : null; + } + + public static bool ContainsThingStackableWith(this List thingsOfSingleDef, Thing t) + { + var def = t.def; + + for (var i = thingsOfSingleDef.Count; i-- > 0;) + { + if (thingsOfSingleDef[i].stackCount < def.stackLimit && thingsOfSingleDef[i].CanStackWith(t)) + return true; + } + + return false; + } + + public static Map? TryGetMapHeld(this Thing thing) + { + var maps = Current.Game.Maps; + var mapIndex = (uint)thing.mapIndexOrState; + + return mapIndex < (uint)maps.Count + ? maps[(int)mapIndex] + : thing.ParentHolder is { } parentHolder + ? ThingOwnerUtility.GetRootMap(parentHolder) + : null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Map? TryGetMap(this Thing thing) + { + var maps = Current.Game.Maps; + var mapIndex = (uint)thing.mapIndexOrState; + + return mapIndex < (uint)maps.Count ? maps[(int)mapIndex] : null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Map GetMap(this Thing thing) => Current.Game.Maps[thing.mapIndexOrState]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Map GetMap(this Region region) => Current.Game.Maps[region.mapIndex]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSpawned(this Thing thing) => (uint)thing.mapIndexOrState < (uint)Current.Game.Maps.Count; + + public static void InvokeWhenCellIndicesReady(this Map map, Action action) + { + if (map.cellIndices != null) + action(map); + else + map.Events().ComponentsConstructed += action; + } + + public static void AddLetterSilently(this LetterStack letterStack, Letter letter, Color color, bool playSound = false, + string? debugInfo = null) + { + var letterDef = letter.def.MemberwiseClone(); + letterDef.index = 0; + letterDef.shortHash = 0; + letterDef.color = color; + letterDef.flashColor = color; + letter.def = letterDef; + letterStack.AddLetterSilently(letter, playSound, debugInfo); + } + + public static void AddLetterSilently(this LetterStack letterStack, Letter letter, bool playSound = false, + string? debugInfo = null) + { + if (playSound) + letter.def.arriveSound.PlayOneShotOnCamera(); + + letter.arrivalTime = Time.time; + letter.arrivalTick = Find.TickManager.TicksGame; + letter.debugInfo = debugInfo; + letterStack.letters.Add(letter); + letter.Received(); + } +} \ No newline at end of file diff --git a/Source/PerformanceFish/Utility/PipetteTool.cs b/Source/PerformanceFish/Utility/PipetteTool.cs new file mode 100644 index 0000000..ea5cd41 --- /dev/null +++ b/Source/PerformanceFish/Utility/PipetteTool.cs @@ -0,0 +1,80 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Linq; +using JetBrains.Annotations; +using UnityEngine.Rendering; + +namespace PerformanceFish.Utility; + +public static class PipetteTool +{ + [UsedImplicitly] + [DebugAction("Misc", "Pipette tool", actionType = DebugActionType.ToolMap, + allowedGameStates = AllowedGameStates.PlayingOnMap)] + public static void Action() + { + var things = UI.MouseCell().GetThingList(Find.CurrentMap).ToList(); + things.Sort(static (x, y) => ((int)x.def.altitudeLayer).CompareTo((int)y.def.altitudeLayer)); + + for (var i = things.Count; i-- > 0; ) + { + var thing = things[i]; + var material = thing.Graphic?.MatAt(thing.Rotation, thing); + if (material == null) + continue; + + var texture = material.mainTexture; + if (texture == null) + continue; + + var destinationTexture = RenderTexture.GetTemporary(texture.width, texture.height); + Graphics.Blit(texture, destinationTexture); + + AsyncGPUReadback.Request(destinationTexture, callback: result + => + { + try + { + using var pixelDataNative = result.GetData(); + + var pixelDataFiltered = pixelDataNative + .Where(static color => (color.r > 0 || color.g > 0 || color.b > 0) && color.a > 0) + .ToArray(); + + var averageColor = pixelDataFiltered.Length < 1 + ? (Color32)Color.black + : GetAverage(pixelDataFiltered); + + Log.Message($"Texture of thing '{thing}' has average color of {averageColor} { + "(■)".Colorize(averageColor)}."); + + Find.LetterStack.AddLetterSilently(LetterMaker.MakeLetter("Pipette result", + $"Texture of thing '{thing}' has average color of {averageColor} { + "(■)".Colorize(averageColor)}", + LetterDefOf.NeutralEvent), averageColor); + } + catch (Exception e) + { + Log.Error($"Exception in AsyncGPUReadback:\n{e}"); + } + finally + { + RenderTexture.ReleaseTemporary(destinationTexture); + } + }); + break; + } + } + + private static Color32 GetAverage(Color32[] colors) + => new(GetAverage(colors, static color => color.r), + GetAverage(colors, static color => color.g), + GetAverage(colors, static color => color.b), + GetAverage(colors, static color => color.a)); + + private static byte GetAverage(IEnumerable colors, Func selector) + => (byte)Mathf.RoundToInt((float)colors.Average(selector)); +} \ No newline at end of file diff --git a/Source/PerformanceFish/Utility/PooledArray.cs b/Source/PerformanceFish/Utility/PooledArray.cs new file mode 100644 index 0000000..93b6c18 --- /dev/null +++ b/Source/PerformanceFish/Utility/PooledArray.cs @@ -0,0 +1,105 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Diagnostics.CodeAnalysis; + +namespace PerformanceFish.Utility; + +public record struct PooledArray : IDisposable, IEnumerable +{ + private static T[]?[] _buffers = new T[]?[4]; + private static int _bufferCount; + + private T[] _array; + private int _length; + + public T[] BackingArray => _array; + public int Length => _length; + public int Capacity => _array.Length; + + public T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + Guard.IsLessThan(index, Length); + return _array[index]; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Guard.IsLessThan(index, Length); + _array[index] = value; + } + } + + public PooledArray() : this(0) + { + } + + public PooledArray(int length) + { + if (_bufferCount < 1) + { + NewBuffer(length); + } + else + { + ref var bucket = ref _buffers[--_bufferCount]; + _array = bucket!; + bucket = null; + if (_array.Length < length) + ResizeBuffer(length); + } + + _length = length; + } + + public PooledArray(ICollection collection) : this(collection.Count) => collection.CopyTo(_array, 0); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T GetReference(int index) + { + Guard.IsLessThan(index, Length); + return ref _array[index]; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ResizeBuffer(int length) => Array.Resize(ref _array, Mathf.NextPowerOfTwo(length)); + + [MemberNotNull(nameof(_array))] + [MethodImpl(MethodImplOptions.NoInlining)] + private void NewBuffer(int length) => _array = new T[Mathf.NextPowerOfTwo(length)]; + + public void Dispose() + { + if (++_bufferCount > _buffers.Length) + ExpandBuffers(_bufferCount); + + if (_length > 0) + Array.Clear(_array, 0, _length); + + _buffers[_bufferCount - 1] = _array; + } + + public void Sort(IComparer comparer) => _array.AsSpan()[.._length].Sort(comparer); + + private static void ExpandBuffers(int minLength) + { + var newBuffers = new T[]?[Mathf.NextPowerOfTwo(minLength)]; + + Array.Copy(_buffers, newBuffers, _buffers.Length); + + _buffers = newBuffers; + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < _length; i++) + yield return _array[i]; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file diff --git a/Source/PerformanceFish/Utility/ReflectionExtensions.cs b/Source/PerformanceFish/Utility/ReflectionExtensions.cs index b898c9b..3528539 100644 --- a/Source/PerformanceFish/Utility/ReflectionExtensions.cs +++ b/Source/PerformanceFish/Utility/ReflectionExtensions.cs @@ -3,6 +3,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +using System.Diagnostics; +using System.Reflection.Emit; using JetBrains.Annotations; namespace PerformanceFish.Utility; @@ -27,6 +29,8 @@ public static class ReflectionExtensions return null; } + + public static bool IsConst(this FieldInfo info) => info is { IsLiteral: true, IsInitOnly: false }; public static IEnumerable SubclassesWithNoMethodOverride(this Type type, params string[] names) { @@ -60,7 +64,7 @@ public static class ReflectionExtensions yield return subclass; } - public static IEnumerable SubclassesWithNoMethodOverride(this Type type, Type[] allowedDeclaringTypes, + public static IEnumerable SubclassesWithNoMethodOverride(this Type type, Type?[] allowedDeclaringTypes, params string[] names) { Guard.IsNotNull(type); @@ -75,8 +79,11 @@ public static class ReflectionExtensions var hasOverrides = false; for (var j = names.Length; j-- > 0;) { - if (allowedDeclaringTypes.Contains(AccessTools.Method(allSubclasses[i], names[j])!.DeclaringType!)) + if (AccessTools.Method(allSubclasses[i], names[j])!.DeclaringType is { } declaringType + && allowedDeclaringTypes.Contains(declaringType)) + { continue; + } hasOverrides = true; break; @@ -87,7 +94,7 @@ public static class ReflectionExtensions } } - public static IEnumerable SubclassesWithNoMethodOverrideAndSelf(this Type type, Type[] allowedDeclaringTypes, + public static IEnumerable SubclassesWithNoMethodOverrideAndSelf(this Type type, Type?[] allowedDeclaringTypes, params string[] names) { yield return type; @@ -95,4 +102,94 @@ public static class ReflectionExtensions foreach (var subclass in type.SubclassesWithNoMethodOverride(allowedDeclaringTypes, names)) yield return subclass; } + + public static Version GetReferencedAssemblyVersion(this Assembly assembly, Assembly referencedAssembly) + => assembly.GetReferencedAssemblyVersion(referencedAssembly.GetName().Name); + + public static Version GetReferencedAssemblyVersion(this Assembly assembly, string referencedAssemblyName) + => assembly.GetReferencedAssemblies().First(reference + => reference.FullName.StartsWith($"{referencedAssemblyName}, Version", StringComparison.Ordinal)).Version; + + public static Version GetLoadedVersion(this Assembly assembly) => assembly.GetName().Version; + + public static T MemberwiseClone(this T obj) => MemberwiseCloneCache.InstanceFunc(obj); + + private static class MemberwiseCloneCache + { + [ThreadStatic] + private static FishTable? _staticFuncs; + + public static FishTable StaticFuncs => _staticFuncs ??= Initialize(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static FishTable Initialize() + => new() + { + ValueInitializer = static type + => (Delegate)AccessTools.DeclaredField(typeof(MemberwiseCloneCache<>).MakeGenericType(type), + nameof(MemberwiseCloneCache.StaticFunc)).GetValue(null) + }; + } + + private static class MemberwiseCloneCache + { + internal static readonly Func StaticFunc, InstanceFunc; + + static MemberwiseCloneCache() + { + var type = typeof(T); + try + { + var dm = new DynamicMethod($"FisheryMemberwiseCloneFunc_{type.Name}", + type, [type], type, true); + + var il = dm.GetILGenerator(); + + if (type.IsValueType) + { + il.Emit(FishTranspiler.Argument(0)); + } + else + { + var copyVariable = FishTranspiler.NewLocalVariable(type, il); + + il.Emit(FishTranspiler.New(type, Type.EmptyTypes)); + il.Emit(copyVariable.Store()); + + foreach (var field in typeof(T).GetFields(BindingFlags.Instance + | BindingFlags.Public + | BindingFlags.NonPublic)) + { + var fieldInstruction = FishTranspiler.Field(field); + + il.Emit(copyVariable.Load()); + il.Emit(FishTranspiler.Argument(0)); + il.Emit(fieldInstruction.Load()); + il.Emit(fieldInstruction.Store()); + } + + il.Emit(copyVariable.Load()); + } + + il.Emit(FishTranspiler.Return); + + StaticFunc = (Func)dm.CreateDelegate(typeof(Func)); + + InstanceFunc = static obj => obj!.GetType() == typeof(T) + ? StaticFunc(obj) + : ((Func)MemberwiseCloneCache.StaticFuncs.GetOrAdd(obj.GetType()))(obj); + } + catch (Exception e) + { + Log.Error($"Failed creating a MemberwiseClone delegate for {type.FullDescription()}\n{ + e}\n{new StackTrace()}"); + + var defaultCloneFunc = (Func)AccessTools + .DeclaredMethod(typeof(object), nameof(MemberwiseClone)) + .CreateDelegate(typeof(Func)); + + InstanceFunc = StaticFunc = obj => (T)defaultCloneFunc(obj!); + } + } + } } \ No newline at end of file diff --git a/Source/PerformanceFish/Utility/StringExtensions.cs b/Source/PerformanceFish/Utility/StringExtensions.cs index a996e8c..430b6b4 100644 --- a/Source/PerformanceFish/Utility/StringExtensions.cs +++ b/Source/PerformanceFish/Utility/StringExtensions.cs @@ -8,5 +8,8 @@ namespace PerformanceFish.Utility; public static class StringExtensions { public static bool NameEqualsCaseInsensitive(this MethodInfo methodInfo, string name) - => methodInfo.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase); + => methodInfo.Name.Equals(name, StringComparison.OrdinalIgnoreCase); + + public static string AppendWhen(this string instance, bool predicate, string text) + => predicate ? instance + text : instance; } \ No newline at end of file diff --git a/Source/PerformanceFish/Utility/ThingHelper.cs b/Source/PerformanceFish/Utility/ThingHelper.cs new file mode 100644 index 0000000..118bffb --- /dev/null +++ b/Source/PerformanceFish/Utility/ThingHelper.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace PerformanceFish.Utility; + +public static class ThingHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetKey(this Thing thing) + => thing.thingIDNumber != -1 ? thing.thingIDNumber : RuntimeHelpers.GetHashCode(thing); +} \ No newline at end of file diff --git a/Source/PerformanceFish/TickHelper.cs b/Source/PerformanceFish/Utility/TickHelper.cs similarity index 77% rename from Source/PerformanceFish/TickHelper.cs rename to Source/PerformanceFish/Utility/TickHelper.cs index 2ee93b7..cc02ba9 100644 --- a/Source/PerformanceFish/TickHelper.cs +++ b/Source/PerformanceFish/Utility/TickHelper.cs @@ -3,7 +3,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -namespace PerformanceFish; +namespace PerformanceFish.Utility; public static class TickHelper { @@ -11,7 +11,11 @@ public static class TickHelper public static bool Past(int tick) => tick < TicksGame; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool MatchesModulo(int powerOfTwo) => (TicksGame & (powerOfTwo - 1)) == 0; + public static bool MatchesModulo(int powerOfTwo) => MatchesModulo(powerOfTwo, 0); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool MatchesModulo(int powerOfTwo, int offset) + => ((TicksGame + offset) & (powerOfTwo - 1)) == 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Add(int ticks) => ticks + TicksGame; diff --git a/Source/PerformanceFish/UtilityPatches.cs b/Source/PerformanceFish/UtilityPatches.cs index de8fe51..b3a8a32 100644 --- a/Source/PerformanceFish/UtilityPatches.cs +++ b/Source/PerformanceFish/UtilityPatches.cs @@ -5,12 +5,14 @@ namespace PerformanceFish; -public class UtilityPatches : ClassWithFishPatches +public sealed class UtilityPatches : ClassWithFishPatches { - public class FinalizeInit_Patch : FishPatch + public sealed class FinalizeInit_Patch : FishPatch { public override bool Enabled => true; + public override bool ShowSettings => false; + public override string Description { get; } = "Necessary for patches that require a loaded game to function"; @@ -31,13 +33,15 @@ public class UtilityPatches : ClassWithFishPatches } } - public class ClearAllMapsAndWorld_Patch : FishPatch + public sealed class ClearAllMapsAndWorld_Patch : FishPatch { public override bool Enabled => true; + public override bool ShowSettings => false; + public override string Description { get; } - = "A patch to make the game clear all Performance Fish caches whenever saves get unloaded, to avoid " - + "anything getting carried over to a new game by mistake"; + = "This clears all Performance Fish caches whenever saves get unloaded, to avoid anything getting carried " + + "over to a new game by mistake"; public override Delegate TargetMethodGroup => Verse.Profile.MemoryUtility.ClearAllMapsAndWorld; diff --git a/Source/PerformanceFish/UtilityPrepatches.cs b/Source/PerformanceFish/UtilityPrepatches.cs index 544ea2b..9f9935a 100644 --- a/Source/PerformanceFish/UtilityPrepatches.cs +++ b/Source/PerformanceFish/UtilityPrepatches.cs @@ -3,14 +3,13 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -using System.Diagnostics; using PerformanceFish.Prepatching; namespace PerformanceFish; -public class UtilityPrepatches : ClassWithFishPrepatches +public sealed class UtilityPrepatches : ClassWithFishPrepatches { - public class CreateModClassesPatch : FishPrepatch + public sealed class CreateModClassesPatch : FishPrepatch { public override string? Description { get; } = "Hook for very early loading of patches, right after the game finishes loading up game assemblies into " @@ -18,6 +17,8 @@ public class UtilityPrepatches : ClassWithFishPrepatches public override bool Enabled => true; + public override bool ShowSettings => false; + public override MethodBase TargetMethodBase { get; } = methodof(LoadedModManager.CreateModClasses); @@ -30,21 +31,8 @@ public class UtilityPrepatches : ClassWithFishPrepatches catch (Exception e) { Log.Error($"Exception caught within OnAssembliesLoaded:\n{e}\n{ - new StackTrace() /*StackTraceUtility.ExtractStackTrace()*/}"); + Environment.StackTrace /*StackTraceUtility.ExtractStackTrace()*/}"); } } } - - public class ThingDestroy : FishPrepatch - { - public override string? Description { get; } - = "Hook to invoke an event whenever things get destroyed. Certain functions require this."; - - public override bool Enabled => true; - - public override MethodBase TargetMethodBase { get; } - = AccessTools.DeclaredMethod(typeof(Thing), nameof(Thing.Destroy)); - - public static void Postfix(Thing __instance) => Cache.Utility.NotifyThingDestroyed(__instance); - } } \ No newline at end of file diff --git a/Source/PerformanceFish/WorkGiverDefFixes.cs b/Source/PerformanceFish/WorkGiverDefFixes.cs new file mode 100644 index 0000000..c7aa2bc --- /dev/null +++ b/Source/PerformanceFish/WorkGiverDefFixes.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; + +namespace PerformanceFish; + +public sealed class WorkGiverDefFixes : ClassWithFishPrepatches +{ + public sealed class WorkerPatch : FishPrepatch + { + public override string? Description { get; } + = "RimWorld has no checks to verify that workGiverDefs include valid classes with their actual " + + "implementation. This means whenever mods either don't provide one or it simply fails to load for any " + + "reason, entirely uninformative errors get spammed at the point pawns try starting a job at. This patch " + + "here adds a check, logging the erroneous def with mod source when possible and disabling the workGiver " + + "from running to prevent the error spam."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredPropertyGetter(typeof(WorkGiverDef), nameof(WorkGiverDef.Worker)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(Replacement); + + public static WorkGiver Replacement(WorkGiverDef instance) + { + if (instance.workerInt == null) + { + if (instance.giverClass is null) + TryFixAndLogError(instance); + + instance.workerInt = (WorkGiver)Activator.CreateInstance(instance.giverClass!); + instance.workerInt.def = instance; + } + + return instance.workerInt; + } + + public static void TryFixAndLogError(WorkGiverDef instance) + { + instance.giverClass = typeof(WorkGiver_AlwaysSkip); + Log.Error($"Detected missing giverClass in WorkGiverDef '{ + instance.defName ?? "null"}' from mod '{ + instance.modContentPack?.Name ?? "null"}' for WorkTypeDef '{ + instance.workType?.defName ?? "null"}'. Disabling now to avoid further issues."); + } + } + + public sealed class WorkGiver_AlwaysSkip : WorkGiver + { + public override bool ShouldSkip(Pawn pawn, bool forced = false) => true; + } +} \ No newline at end of file