diff --git a/.gitignore b/.gitignore index a34df41..b5ff8a8 100644 --- a/.gitignore +++ b/.gitignore @@ -481,3 +481,6 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp + +# Cache folders used in disk caching +Cache/* \ No newline at end of file diff --git a/1.5/Assemblies/PerformanceFish.dll b/1.5/Assemblies/PerformanceFish.dll index 24304e8..eacfdc0 100644 Binary files a/1.5/Assemblies/PerformanceFish.dll and b/1.5/Assemblies/PerformanceFish.dll differ diff --git a/1.5/Assemblies/PerformanceFish.pdb b/1.5/Assemblies/PerformanceFish.pdb index 312e985..4771ea9 100644 Binary files a/1.5/Assemblies/PerformanceFish.pdb and b/1.5/Assemblies/PerformanceFish.pdb differ diff --git a/1.6/Assemblies/PerformanceFish.dll b/1.6/Assemblies/PerformanceFish.dll new file mode 100644 index 0000000..1798725 Binary files /dev/null and b/1.6/Assemblies/PerformanceFish.dll differ diff --git a/1.6/Assemblies/PerformanceFish.pdb b/1.6/Assemblies/PerformanceFish.pdb new file mode 100644 index 0000000..bdc70d0 Binary files /dev/null and b/1.6/Assemblies/PerformanceFish.pdb differ diff --git a/About/About.xml b/About/About.xml index 4817d54..78aa9e3 100644 --- a/About/About.xml +++ b/About/About.xml @@ -5,6 +5,7 @@
  • 1.4
  • 1.5
  • +
  • 1.6
  • bradson PerformanceFish/ModIcon @@ -56,4 +57,4 @@
  • vanillaexpanded.storytellers.rick
  • download more tps - \ No newline at end of file + diff --git a/Source/PerformanceFish/1.5.csproj b/Source/PerformanceFish/1.5.csproj index baf5a13..7379b8e 100644 --- a/Source/PerformanceFish/1.5.csproj +++ b/Source/PerformanceFish/1.5.csproj @@ -3,6 +3,8 @@ ..\..\1.5\Assemblies\ Copyright (c) 2024 Bradson + $(MSBuildThisFileDirectory)..\..\..\Fishery + $(MSBuildThisFileDirectory)..\..\..\Fishery-clean DEBUG;V1_5 @@ -14,7 +16,7 @@ - ..\..\..\Fishery\1.5\Assemblies\1Fishery.dll + $(FisheryRepoRoot)\1.5\Assemblies\1Fishery.dll False diff --git a/Source/PerformanceFish/1.6.csproj b/Source/PerformanceFish/1.6.csproj new file mode 100644 index 0000000..1478547 --- /dev/null +++ b/Source/PerformanceFish/1.6.csproj @@ -0,0 +1,23 @@ + + + + ..\..\1.6\Assemblies\ + Copyright (c) 2026 Bradson + $(MSBuildThisFileDirectory)..\..\..\Fishery + $(MSBuildThisFileDirectory)..\..\..\Fishery-clean + + + DEBUG;V1_5;V1_6 + + + V1_5;V1_6 + + + + + + $(FisheryRepoRoot)\1.6\Assemblies\1Fishery.dll + False + + + diff --git a/Source/PerformanceFish/Cache/BitCellGrid.cs b/Source/PerformanceFish/Cache/BitCellGrid.cs index 9e99ea6..1027cc4 100644 --- a/Source/PerformanceFish/Cache/BitCellGrid.cs +++ b/Source/PerformanceFish/Cache/BitCellGrid.cs @@ -7,13 +7,15 @@ namespace PerformanceFish.Cache; public sealed class BitCellGrid { + private readonly Map _map; private long[] _innerArray; private CellIndices _cellIndices; public BitCellGrid(Map map) { - _innerArray = null!; - _cellIndices = null!; + _map = map; + _innerArray = Array.Empty(); + _cellIndices = default; map.InvokeWhenCellIndicesReady(Initialize); } @@ -23,12 +25,26 @@ public sealed class BitCellGrid _innerArray = new long[(Math.Max(0, _cellIndices.NumGridCells - 1) >> 6) + 1]; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureInitialized() + { + if (_cellIndices.NumGridCells != _map.cellIndices.NumGridCells) + Initialize(_map); + } + public bool this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => (_innerArray[index >> 6] & (1L << (index & 63))) != 0L; + get + { + EnsureInitialized(); + ValidateIndex(index); + return (_innerArray[index >> 6] & (1L << (index & 63))) != 0L; + } set { + EnsureInitialized(); + ValidateIndex(index); ref var bucket = ref _innerArray[index >> 6]; bucket ^= (-(long)value.AsInt() ^ bucket) & (1L << (index & 63)); } @@ -37,7 +53,24 @@ public sealed class BitCellGrid public bool this[in IntVec3 c] { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => this[c.CellToIndex(_cellIndices)]; - set => this[c.CellToIndex(_cellIndices)] = value; + get + { + EnsureInitialized(); + return this[c.CellToIndex(_cellIndices)]; + } + set + { + EnsureInitialized(); + this[c.CellToIndex(_cellIndices)] = value; + } } -} \ No newline at end of file + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ValidateIndex(int index) + { + if ((uint)index >= (uint)_cellIndices.NumGridCells) + { + throw new IndexOutOfRangeException($"BitCellGrid index {index} out of bounds for {_cellIndices.NumGridCells} cells on map size {_map.Size}."); + } + } +} diff --git a/Source/PerformanceFish/Cache/CellGrid.cs b/Source/PerformanceFish/Cache/CellGrid.cs index d83ad3f..9d964e5 100644 --- a/Source/PerformanceFish/Cache/CellGrid.cs +++ b/Source/PerformanceFish/Cache/CellGrid.cs @@ -9,38 +9,60 @@ namespace PerformanceFish.Cache; public sealed class CellGrid { + private readonly Map _map; private T[] _innerArray; private CellIndices _cellIndices; public CellGrid(Map map) { - _innerArray = null!; - _cellIndices = null!; + _map = map; + _innerArray = Array.Empty(); + _cellIndices = default; map.InvokeWhenCellIndicesReady(Initialize); } - [MemberNotNull(nameof(_innerArray), nameof(_cellIndices))] + [MemberNotNull(nameof(_innerArray))] private void Initialize(Map map) { _cellIndices = map.cellIndices; _innerArray = new T[_cellIndices.NumGridCells]; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureInitialized() + { + if (_cellIndices.NumGridCells != _map.cellIndices.NumGridCells) + Initialize(_map); + } + public ref T this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => ref _innerArray[index]; + get + { + EnsureInitialized(); + return ref _innerArray[index]; + } } public ref T this[in IntVec3 c] { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => ref _innerArray[c.CellToIndex(_cellIndices)]; + get + { + EnsureInitialized(); + var index = c.CellToIndex(_cellIndices); + return ref _innerArray[index]; + } } public ref T this[CellIndex cellIndex] { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => ref _innerArray[cellIndex.Value]; + get + { + EnsureInitialized(); + return ref _innerArray[cellIndex.Value]; + } } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/CellIndex.cs b/Source/PerformanceFish/CellIndex.cs index 731b6c4..e2f544f 100644 --- a/Source/PerformanceFish/CellIndex.cs +++ b/Source/PerformanceFish/CellIndex.cs @@ -10,20 +10,25 @@ public record struct CellIndex(int Value) public int Value = Value; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public CellIndex(IntVec3 cell, Map map) : this(cell, map.cellIndices.mapSizeX) {} + public CellIndex(IntVec3 cell, Map map) : this(cell, map.Size.x) {} [MethodImpl(MethodImplOptions.AggressiveInlining)] public CellIndex(IntVec3 cell, int mapSizeX) : this(cell.CellToIndex(mapSizeX)) {} - public IntVec3 ToCell(Map map) => ToCell(map.cellIndices.mapSizeX); + public IntVec3 ToCell(Map map) => ToCell(map.Size.x); public IntVec3 ToCell(int mapSizeX) => CellIndicesUtility.IndexToCell(Value, mapSizeX); public List GetThingList(Map map) => map.thingGrid.thingGrid[Value]; - public bool IsFogged(Map map) => map.fogGrid.fogGrid[Value]; + public bool IsFogged(Map map) +#if V1_6 + => map.fogGrid.fogGrid.IsSet(Value); +#else + => map.fogGrid.fogGrid[Value]; +#endif public float GetSnowDepth(Map map) => map.snowGrid.depthGrid[Value]; public Building? GetEdifice(Map map) => map.edificeGrid[Value]; -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/CompTickOptimization.cs b/Source/PerformanceFish/CompTickOptimization.cs new file mode 100644 index 0000000..5fceebc --- /dev/null +++ b/Source/PerformanceFish/CompTickOptimization.cs @@ -0,0 +1,353 @@ +// Copyright (c) 2026 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 CompTickOptimization : ClassWithFishPrepatches +{ + private static readonly MethodInfo _thingCompTickMethod + = AccessTools.DeclaredMethod(typeof(ThingComp), nameof(ThingComp.CompTick)); + + private static readonly MethodInfo _thingCompTickIntervalMethod + = AccessTools.DeclaredMethod(typeof(ThingComp), nameof(ThingComp.CompTickInterval), [typeof(int)]); + + private static readonly MethodInfo _thingCompTickRareMethod + = AccessTools.DeclaredMethod(typeof(ThingComp), nameof(ThingComp.CompTickRare)); + + private static readonly MethodInfo _thingCompTickLongMethod + = AccessTools.DeclaredMethod(typeof(ThingComp), nameof(ThingComp.CompTickLong)); + + private static readonly MethodInfo _hediffCompPostTickMethod + = AccessTools.DeclaredMethod(typeof(HediffComp), nameof(HediffComp.CompPostTick), [typeof(float).MakeByRefType()]); + + private static readonly MethodInfo _hediffCompPostTickIntervalMethod = AccessTools.DeclaredMethod( + typeof(HediffComp), + nameof(HediffComp.CompPostTickInterval), + [typeof(float).MakeByRefType(), typeof(int)]); + + private static readonly bool _thingCompTickBaseIsTrivial = IsTrivialVoidMethod(_thingCompTickMethod); + private static readonly bool _thingCompTickIntervalBaseIsTrivial = IsTrivialVoidMethod(_thingCompTickIntervalMethod); + private static readonly bool _thingCompTickRareBaseIsTrivial = IsTrivialVoidMethod(_thingCompTickRareMethod); + private static readonly bool _thingCompTickLongBaseIsTrivial = IsTrivialVoidMethod(_thingCompTickLongMethod); + private static readonly bool _hediffCompPostTickBaseIsTrivial = IsTrivialVoidMethod(_hediffCompPostTickMethod); + private static readonly bool _hediffCompPostTickIntervalBaseIsTrivial + = IsTrivialVoidMethod(_hediffCompPostTickIntervalMethod); + + private static readonly Lazy _canSkipInheritedBaseThingCompTick + = new(static () => _thingCompTickBaseIsTrivial && !HasHarmonyPatches(_thingCompTickMethod)); + + private static readonly Lazy _canSkipInheritedBaseThingCompTickInterval + = new(static () => _thingCompTickIntervalBaseIsTrivial && !HasHarmonyPatches(_thingCompTickIntervalMethod)); + + private static readonly Lazy _canSkipInheritedBaseThingCompTickRare + = new(static () => _thingCompTickRareBaseIsTrivial && !HasHarmonyPatches(_thingCompTickRareMethod)); + + private static readonly Lazy _canSkipInheritedBaseThingCompTickLong + = new(static () => _thingCompTickLongBaseIsTrivial && !HasHarmonyPatches(_thingCompTickLongMethod)); + + private static readonly Lazy _canSkipInheritedBaseHediffCompPostTick + = new(static () => _hediffCompPostTickBaseIsTrivial && !HasHarmonyPatches(_hediffCompPostTickMethod)); + + private static readonly Lazy _canSkipInheritedBaseHediffCompPostTickInterval = new( + static () => _hediffCompPostTickIntervalBaseIsTrivial && !HasHarmonyPatches(_hediffCompPostTickIntervalMethod)); + + private static readonly FishTable _thingCompTickTypes = new() + { + ValueInitializer = static type => InheritsThingCompTick(type) + }; + + private static readonly FishTable _thingCompTickIntervalTypes = new() + { + ValueInitializer = static type => InheritsThingCompTickInterval(type) + }; + + private static readonly FishTable _thingCompTickRareTypes = new() + { + ValueInitializer = static type => InheritsThingCompTickRare(type) + }; + + private static readonly FishTable _thingCompTickLongTypes = new() + { + ValueInitializer = static type => InheritsThingCompTickLong(type) + }; + + private static readonly FishTable _hediffCompPostTickTypes = new() + { + ValueInitializer = static type => InheritsHediffCompPostTick(type) + }; + + private static readonly FishTable _hediffCompPostTickIntervalTypes = new() + { + ValueInitializer = static type => InheritsHediffCompPostTickInterval(type) + }; + + public sealed class ThingTickPatch : FishPrepatch + { + public override bool DefaultState => false; + + public override string? Description { get; } + = "Skips inherited empty ThingComp.CompTick calls when the base method is still unpatched and trivial."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ThingWithComps), "Tick"); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(ThingWithComps instance) + { + var comps = instance.AllComps; + var count = comps.Count; + var canSkipInheritedBase = CanSkipInheritedBaseThingCompTick(); + for (var i = 0; i < count; i++) + { + var comp = comps[i]; + if (!canSkipInheritedBase || !_thingCompTickTypes.GetOrAdd(comp.GetType())) + comp.CompTick(); + } + } + } + + public sealed class ThingTickIntervalPatch : FishPrepatch + { + public override bool DefaultState => false; + + public override string? Description { get; } + = "Skips inherited empty ThingComp.CompTickInterval calls when the base method is still unpatched and trivial."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ThingWithComps), "TickInterval", [typeof(int)]); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(ThingWithComps instance, int delta) + { + var comps = instance.AllComps; + var count = comps.Count; + var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickInterval(); + for (var i = 0; i < count; i++) + { + var comp = comps[i]; + if (!canSkipInheritedBase || !_thingCompTickIntervalTypes.GetOrAdd(comp.GetType())) + comp.CompTickInterval(delta); + } + } + } + + public sealed class ThingTickRarePatch : FishPrepatch + { + public override bool DefaultState => false; + + public override string? Description { get; } + = "Skips inherited empty ThingComp.CompTickRare calls when the base method is still unpatched and trivial."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ThingWithComps), nameof(ThingWithComps.TickRare)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(ThingWithComps instance) + { + var comps = instance.AllComps; + var count = comps.Count; + var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickRare(); + for (var i = 0; i < count; i++) + { + var comp = comps[i]; + if (!canSkipInheritedBase || !_thingCompTickRareTypes.GetOrAdd(comp.GetType())) + comp.CompTickRare(); + } + } + } + + public sealed class ThingTickLongPatch : FishPrepatch + { + public override bool DefaultState => false; + + public override string? Description { get; } + = "Skips inherited empty ThingComp.CompTickLong calls when the base method is still unpatched and trivial."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ThingWithComps), nameof(ThingWithComps.TickLong)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(ThingWithComps instance) + { + var comps = instance.AllComps; + var count = comps.Count; + var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickLong(); + for (var i = 0; i < count; i++) + { + var comp = comps[i]; + if (!canSkipInheritedBase || !_thingCompTickLongTypes.GetOrAdd(comp.GetType())) + comp.CompTickLong(); + } + } + } + + public sealed class HediffPostTickPatch : FishPrepatch + { + public override bool DefaultState => false; + + public override string? Description { get; } + = "Skips inherited empty HediffComp.CompPostTick calls when the base method is still unpatched and trivial."; + + 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 void ReplacementBody(HediffWithComps instance) + { + var comps = instance.comps; + if (comps == null) + return; + + var count = comps.Count; + var severityAdjustment = 0f; + var canSkipInheritedBase = CanSkipInheritedBaseHediffCompPostTick(); + + for (var i = 0; i < count; i++) + { + var comp = comps[i]; + if (!canSkipInheritedBase || !_hediffCompPostTickTypes.GetOrAdd(comp.GetType())) + comp.CompPostTick(ref severityAdjustment); + } + + if (severityAdjustment != 0f) + instance.Severity += severityAdjustment; + } + } + + public sealed class HediffPostTickIntervalPatch : FishPrepatch + { + public override bool DefaultState => false; + + public override string? Description { get; } + = "Skips inherited empty HediffComp.CompPostTickInterval calls when the base method is still unpatched and trivial."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(HediffWithComps), nameof(HediffWithComps.PostTickInterval), [typeof(int)]); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + public static void ReplacementBody(HediffWithComps instance, int delta) + { + var comps = instance.comps; + if (comps == null) + return; + + var count = comps.Count; + var severityAdjustment = 0f; + var canSkipInheritedBase = CanSkipInheritedBaseHediffCompPostTickInterval(); + + for (var i = 0; i < count; i++) + { + var comp = comps[i]; + if (!canSkipInheritedBase || !_hediffCompPostTickIntervalTypes.GetOrAdd(comp.GetType())) + comp.CompPostTickInterval(ref severityAdjustment, delta); + } + + if (severityAdjustment != 0f) + instance.Severity += severityAdjustment; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanSkipInheritedBaseThingCompTick() + => _canSkipInheritedBaseThingCompTick.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanSkipInheritedBaseThingCompTickInterval() + => _canSkipInheritedBaseThingCompTickInterval.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanSkipInheritedBaseThingCompTickRare() + => _canSkipInheritedBaseThingCompTickRare.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanSkipInheritedBaseThingCompTickLong() + => _canSkipInheritedBaseThingCompTickLong.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanSkipInheritedBaseHediffCompPostTick() + => _canSkipInheritedBaseHediffCompPostTick.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanSkipInheritedBaseHediffCompPostTickInterval() + => _canSkipInheritedBaseHediffCompPostTickInterval.Value; + + private static bool InheritsThingCompTick(Type type) + => AccessTools.Method(type, nameof(ThingComp.CompTick))?.DeclaringType == typeof(ThingComp); + + private static bool InheritsThingCompTickInterval(Type type) + => AccessTools.Method(type, nameof(ThingComp.CompTickInterval), [typeof(int)])?.DeclaringType == typeof(ThingComp); + + private static bool InheritsThingCompTickRare(Type type) + => AccessTools.Method(type, nameof(ThingComp.CompTickRare))?.DeclaringType == typeof(ThingComp); + + private static bool InheritsThingCompTickLong(Type type) + => AccessTools.Method(type, nameof(ThingComp.CompTickLong))?.DeclaringType == typeof(ThingComp); + + private static bool InheritsHediffCompPostTick(Type type) + => AccessTools.Method(type, nameof(HediffComp.CompPostTick), [typeof(float).MakeByRefType()])?.DeclaringType + == typeof(HediffComp); + + private static bool InheritsHediffCompPostTickInterval(Type type) + => AccessTools.Method(type, nameof(HediffComp.CompPostTickInterval), + [typeof(float).MakeByRefType(), typeof(int)])?.DeclaringType + == typeof(HediffComp); + + private static bool IsTrivialVoidMethod(MethodInfo method) + { + if (method.ReturnType != typeof(void) + || method.GetMethodBody() is not { ExceptionHandlingClauses.Count: 0, LocalVariables.Count: 0 } body) + { + return false; + } + + var il = body.GetILAsByteArray(); + if (il == null || il.Length == 0) + return false; + + var sawRet = false; + + for (var i = 0; i < il.Length; i++) + { + switch (il[i]) + { + case 0x00: + continue; + case 0x2A when i == il.Length - 1 && !sawRet: + sawRet = true; + continue; + default: + return false; + } + } + + return sawRet; + } + + private static bool HasHarmonyPatches(MethodBase method) + { + var patches = Harmony.GetPatchInfo(method); + return patches is not null + && (patches.Prefixes.Count != 0 + || patches.Postfixes.Count != 0 + || patches.Transpilers.Count != 0 + || patches.Finalizers.Count != 0); + } +} diff --git a/Source/PerformanceFish/Defs/RecipeDefCaching.cs b/Source/PerformanceFish/Defs/RecipeDefCaching.cs index 868047e..7720c4a 100644 --- a/Source/PerformanceFish/Defs/RecipeDefCaching.cs +++ b/Source/PerformanceFish/Defs/RecipeDefCaching.cs @@ -56,8 +56,8 @@ public sealed class RecipeDefCaching : ClassWithFishPatches { private int _defDatabaseVersion = -2; private int _defRecipeUsersVersion = -2; - private RecipeDef _def; - public List RecipeUsers; + private RecipeDef? _def; + public List RecipeUsers = []; public void Update(RecipeDef instance, IEnumerable result) { @@ -81,7 +81,7 @@ public sealed class RecipeDefCaching : ClassWithFishPatches [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _defDatabaseVersion != DefDatabase.defsList._version - || _defRecipeUsersVersion != (_def.recipeUsers?._version ?? 0); + || _defRecipeUsersVersion != (_def?.recipeUsers?._version ?? 0); } } } @@ -141,4 +141,4 @@ public sealed class RecipeDefCaching : ClassWithFishPatches => throw new NotImplementedException(); } }*/ -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/Defs/ThingDefPatches.cs b/Source/PerformanceFish/Defs/ThingDefPatches.cs index ffc0c67..ac98f4d 100644 --- a/Source/PerformanceFish/Defs/ThingDefPatches.cs +++ b/Source/PerformanceFish/Defs/ThingDefPatches.cs @@ -21,13 +21,25 @@ public sealed class ThingDefPatches : ClassWithFishPrepatches public sealed class GetHashCodePatch : FishClassPrepatch { public override string? Description { get; } +#if V1_6 + = "Disabled on 1.6 because Verse.Def now already caches defNameHash and overrides GetHashCode itself."; +#else = "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."; +#endif - public override Type Type { get; } = typeof(BuildableDef); + public override Type Type { get; } = +#if V1_6 + typeof(Def); +#else + typeof(BuildableDef); +#endif public override void FreePatch(TypeDefinition typeDefinition) { +#if V1_6 + return; +#else var module = typeDefinition.Module; var fishHashField = new FieldDefinition(FISH_HASH_FIELD_NAME, FieldAttributes.Private, @@ -74,6 +86,7 @@ public sealed class ThingDefPatches : ClassWithFishPrepatches getHashCodeBody.MaxStackSize = 2; getHashCodeBody.Method.ImplAttributes |= MethodImplAttributes.AggressiveInlining; getHashCodeBody.OptimizeMacros(); +#endif } // [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -295,4 +308,4 @@ public sealed class ThingDefPatches : ClassWithFishPrepatches Flammability, MaxHitPoints } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/Directory.Build.props b/Source/PerformanceFish/Directory.Build.props index cc90b52..84db8c9 100644 --- a/Source/PerformanceFish/Directory.Build.props +++ b/Source/PerformanceFish/Directory.Build.props @@ -29,12 +29,13 @@ - + + @@ -93,4 +94,4 @@ - \ No newline at end of file + diff --git a/Source/PerformanceFish/GasGridOptimization.cs b/Source/PerformanceFish/GasGridOptimization.cs index 3b5e3bf..a69bd8c 100644 --- a/Source/PerformanceFish/GasGridOptimization.cs +++ b/Source/PerformanceFish/GasGridOptimization.cs @@ -41,7 +41,8 @@ public sealed class GasGridOptimization : ClassWithFishPrepatches = "Required by the gas grid optimization"; public override MethodBase TargetMethodBase { get; } - = AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.SetDirect)); + = AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.SetDirect), + [typeof(int), typeof(byte), typeof(byte), typeof(byte), typeof(byte)])!; public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) => ilProcessor.ReplaceBodyWith(ReplacementBody); @@ -415,7 +416,7 @@ public sealed class GasGridOptimization : ClassWithFishPrepatches } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetMapSizeX(Map map) => map.cellIndices.mapSizeX; + public static int GetMapSizeX(Map map) => map.Size.x; } public sealed class ExposeDataPatch : FishPrepatch @@ -1075,7 +1076,7 @@ public sealed class GasGridOptimization : ClassWithFishPrepatches [MethodImpl(MethodImplOptions.AggressiveInlining)] private IntVec3 IndexToCell(int index) - => CellIndicesUtility.IndexToCell(index, Map.cellIndices.mapSizeX); + => CellIndicesUtility.IndexToCell(index, Map.Size.x); /// /// For modders. Performance Fish doesn't automatically scribe added custom defs. @@ -1136,4 +1137,4 @@ public sealed class GasGridOptimization : ClassWithFishPrepatches } // ReSharper restore UnusedMember.Local } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/GenLocalDateCaching.cs b/Source/PerformanceFish/GenLocalDateCaching.cs index a8ed3bc..baa3dc6 100644 --- a/Source/PerformanceFish/GenLocalDateCaching.cs +++ b/Source/PerformanceFish/GenLocalDateCaching.cs @@ -19,45 +19,17 @@ public sealed class GenLocalDateCaching : ClassWithFishPatches + "optimization, but yields accurate results instead of a placeholder value to avoid issues"; public override Delegate TargetMethodGroup { get; } = (Func)GenLocalDate.DayTick; - public override int TranspilerMethodPriority => Priority.First; + public override int PrefixMethodPriority => Priority.First; - public static CodeInstructions Transpiler(CodeInstructions codes, MethodBase method, ILGenerator generator) - //=> Reflection.GetCodeInstructions(Replacement); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Prefix(Thing thing, ref int __result) { - var labelToStartOfMethod = generator.DefineLabel(); - codes.First().labels.Add(labelToStartOfMethod); + if (thing.TryGetMap() is not { } map) + return true; - yield return FishTranspiler.Argument(method, "thing"); - yield return FishTranspiler.Field(typeof(Thing), nameof(Thing.mapIndexOrState)); - yield return FishTranspiler.Constant(0); - yield return FishTranspiler.IfLessThan(labelToStartOfMethod); - - yield return FishTranspiler.Field(typeof(Current), nameof(Current.gameInt)); - yield return FishTranspiler.Field(typeof(Game), nameof(Game.maps)); - yield return FishTranspiler.Argument(method, "thing"); - yield return FishTranspiler.Field(typeof(Thing), nameof(Thing.mapIndexOrState)); - yield return FishTranspiler.Call(typeof(List), "get_Item"); - yield return FishTranspiler.Call>(GenLocalDate.DayTick); - yield return FishTranspiler.Return; - - foreach (var code in codes) - yield return code; + __result = GenLocalDate.DayTick(map); + return false; } - - //[MethodImpl(MethodImplOptions.AggressiveInlining)] - //public static int Replacement(Thing thing) - // => thing.mapIndexOrState < 0 ? GenDate.DayTick(GenTicks.TicksAbs, GenLocalDate.LongitudeForDate(thing)) - // : GenLocalDate.DayTick(Current.gameInt.maps[thing.mapIndexOrState]); - - //[MethodImpl(MethodImplOptions.AggressiveInlining)] - //public static bool Prefix(Thing thing, ref int __result) - //{ - // if (thing.mapIndexOrState < 0) - // return true; - - // __result = GenLocalDate.DayTick(Current.gameInt.maps[thing.mapIndexOrState]); - // return false; - //} } public sealed class DayTickByMap_Patch : FishPatch @@ -79,7 +51,7 @@ public sealed class GenLocalDateCaching : ClassWithFishPatches return __state = true; } - if (SavedTick != TickHelper.TicksGame) + if (SavedDayTickTick != TickHelper.TicksGame) return __state = true; __result = DayTick; @@ -92,12 +64,88 @@ public sealed class GenLocalDateCaching : ClassWithFishPatches if (!__state || SavedMap != map) return; - SavedTick = TickHelper.TicksGame; + SavedDayTickTick = TickHelper.TicksGame; DayTick = __result; } } + public sealed class HourIntegerByMap_Patch : FishPatch + { + public override string Description { get; } + = "Caches results of GenLocalDate.HourInteger for the first map."; + + public override Delegate TargetMethodGroup { get; } = (Func)GenLocalDate.HourInteger; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Prefix(Map map, ref int __result, out bool __state) + { + if (map != SavedMap) + { + if (SavedMap is null && Find.Maps is { Count: > 0 } maps && maps[0] == map) + SavedMap = map; + + return __state = true; + } + + if (SavedHourIntegerTick != TickHelper.TicksGame) + return __state = true; + + __result = HourInteger; + return __state = false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Postfix(Map map, int __result, bool __state) + { + if (!__state || SavedMap != map) + return; + + SavedHourIntegerTick = TickHelper.TicksGame; + HourInteger = __result; + } + } + + public sealed class SeasonByMap_Patch : FishPatch + { + public override string Description { get; } + = "Caches results of GenLocalDate.Season for the first map."; + + public override Delegate TargetMethodGroup { get; } = (Func)GenLocalDate.Season; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Prefix(Map map, ref Season __result, out bool __state) + { + if (map != SavedMap) + { + if (SavedMap is null && Find.Maps is { Count: > 0 } maps && maps[0] == map) + SavedMap = map; + + return __state = true; + } + + if (SavedSeasonTick != TickHelper.TicksGame) + return __state = true; + + __result = Season; + return __state = false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Postfix(Map map, Season __result, bool __state) + { + if (!__state || SavedMap != map) + return; + + SavedSeasonTick = TickHelper.TicksGame; + Season = __result; + } + } + public static Map? SavedMap; - public static int SavedTick = -2; + public static int SavedDayTickTick = -2; + public static int SavedHourIntegerTick = -2; + public static int SavedSeasonTick = -2; public static int DayTick = -2; -} \ No newline at end of file + public static int HourInteger = -2; + public static Season Season = Season.Undefined; +} diff --git a/Source/PerformanceFish/GenTypesPatches.cs b/Source/PerformanceFish/GenTypesPatches.cs index 105cc95..e4f48f2 100644 --- a/Source/PerformanceFish/GenTypesPatches.cs +++ b/Source/PerformanceFish/GenTypesPatches.cs @@ -43,14 +43,15 @@ public sealed class GenTypesPatches : ClassWithFishPrepatches public static List ReplacementBody() where TAttr : Attribute { + var attributeType = typeof(TAttr); lock (Lock) { - var typesWithAttribute = GenTypes.cachedTypesWithAttribute.TryGetValue(typeof(TAttr)); + var typesWithAttribute = GenTypes.cachedTypesWithAttribute.TryGetValue(attributeType); if (typesWithAttribute is null) { - GenTypes.cachedTypesWithAttribute.Add(typeof(TAttr), - typesWithAttribute = GenTypes.AllTypes.AsParallel() + GenTypes.cachedTypesWithAttribute.Add(attributeType, + typesWithAttribute = GenTypes.AllTypes .Where(Predicate()).ToList()); } @@ -82,23 +83,14 @@ public sealed class GenTypesPatches : ClassWithFishPrepatches if (subclasses is null) { - ((PredicateClass)Predicate.Target).BaseType = baseType; GenTypes.cachedSubclasses.Add(baseType, - subclasses = GenTypes.AllTypes.AsParallel() - .Where(Predicate).ToList()); + subclasses = GenTypes.AllTypes + .Where(type => type.IsSubclassOf(baseType)).ToList()); } return subclasses; } } - - public static Func Predicate = new PredicateClass().Invoke; - - public sealed class PredicateClass - { - public Type? BaseType; - public bool Invoke(Type type) => type.IsSubclassOf(BaseType!); - } } public sealed class AllSubclassesNonAbstract : FishPrepatch @@ -123,7 +115,7 @@ public sealed class GenTypesPatches : ClassWithFishPrepatches if (subclassesNonAbstract is null) { GenTypes.cachedSubclassesNonAbstract.Add(baseType, - subclassesNonAbstract = baseType.AllSubclasses().AsParallel() + subclassesNonAbstract = baseType.AllSubclasses() .Where(Predicate).ToList()); } @@ -133,4 +125,4 @@ public sealed class GenTypesPatches : ClassWithFishPrepatches public static Func Predicate = static subClass => !subClass.IsAbstract; } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/GeneTrackerOptimization.cs b/Source/PerformanceFish/GeneTrackerOptimization.cs index 242358d..88ca881 100644 --- a/Source/PerformanceFish/GeneTrackerOptimization.cs +++ b/Source/PerformanceFish/GeneTrackerOptimization.cs @@ -14,6 +14,16 @@ public sealed class GeneTrackerOptimization : ClassWithFishPrepatches { public sealed class GeneTrackerTickPatch : FishPrepatch { + private static readonly FishTable _skippableTypes; + + static GeneTrackerTickPatch() + { + _skippableTypes = new() + { + ValueInitializer = static type => ComputeIsSkippableType(type) + }; + } + public override string? Description { get; } = "Every pawn has a gene tracker, which is responsible for ticking each of their genes. Normally it ticks " + "all of them equally, including those don't change or affect anything through ticking, like skin colors " @@ -74,7 +84,7 @@ public sealed class GeneTrackerOptimization : ClassWithFishPrepatches for (var i = 0; i < count; i++) { var gene = genes[i]; - if (SkippableTypes.Contains(gene.GetType()) + if (IsSkippableType(gene.GetType()) && (gene.def.mentalBreakMtbDays <= 0f || gene.def.mentalBreakDef == null)) { continue; @@ -83,8 +93,64 @@ public sealed class GeneTrackerOptimization : ClassWithFishPrepatches genesToTick.Add(gene); } } - - public static HashSet SkippableTypes - = typeof(Gene).SubclassesWithNoMethodOverrideAndSelf(nameof(Gene.Tick)).ToHashSet(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSkippableType(Type type) => _skippableTypes.GetOrAdd(type); + + private static bool ComputeIsSkippableType(Type type) + { + if (type == typeof(Gene)) + return true; + + var tickMethod = type.GetMethod(nameof(Gene.Tick), global::System.Reflection.BindingFlags.Instance + | global::System.Reflection.BindingFlags.Public + | global::System.Reflection.BindingFlags.NonPublic, null, Type.EmptyTypes, null); + var tickIntervalMethod = type.GetMethod(nameof(Gene.TickInterval), + global::System.Reflection.BindingFlags.Instance + | global::System.Reflection.BindingFlags.Public + | global::System.Reflection.BindingFlags.NonPublic, null, [typeof(int)], null); + + return tickMethod?.DeclaringType == typeof(Gene) + && tickIntervalMethod?.DeclaringType == typeof(Gene); + } } -} \ No newline at end of file + + public sealed class GeneTrackerTickIntervalPatch : FishPrepatch + { + // i am iffy about this + public override string? Description { get; } + = "Every pawn has a gene tracker, which is responsible for ticking each of their genes. Normally it ticks " + + "all of them equally, including those don't change or affect anything through ticking, like skin colors " + + "or basic stat modifiers. This patch improves the gene tracker to determine genes that need ticking in " + + "advance, cache the list of them, and only tick those, skipping all the others."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.Method(typeof(Pawn_GeneTracker), nameof(Pawn_GeneTracker.GeneTrackerTickInterval)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(GeneTrackerTickInterval); + + public static void GeneTrackerTickInterval(Pawn_GeneTracker instance, int delta) + { + if (!ModLister.BiotechInstalled) + return; + + var genesToTick = instance.GenesToTick(); + if (GeneTrackerTickPatch.Dirty(instance, genesToTick)) + GeneTrackerTickPatch.Update(instance, genesToTick); + + for (var i = genesToTick.Count - 1; i >= 0; i--) + { + if (genesToTick[i].Active) + genesToTick[i].TickInterval(delta); + } + + if (instance.pawn.IsSpawned() + && instance.Xenotype != XenotypeDefOf.Baseliner + && instance.pawn.IsHashIntervalTick(300, delta)) + { + LessonAutoActivator.TeachOpportunity(ConceptDefOf.GenesAndXenotypes, OpportunityType.Important); + } + } + } +} diff --git a/Source/PerformanceFish/GlobalUsings.cs b/Source/PerformanceFish/GlobalUsings.cs new file mode 100644 index 0000000..ee3aa06 --- /dev/null +++ b/Source/PerformanceFish/GlobalUsings.cs @@ -0,0 +1 @@ +global using HashCode = FisheryLib.HashCode; diff --git a/Source/PerformanceFish/Hauling/LwmCompCacheValue.cs b/Source/PerformanceFish/Hauling/LwmCompCacheValue.cs index a0ef995..9565317 100644 --- a/Source/PerformanceFish/Hauling/LwmCompCacheValue.cs +++ b/Source/PerformanceFish/Hauling/LwmCompCacheValue.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/. -global using LwmCompCache = PerformanceFish.Cache.ByReference; +global using LwmCompCache = PerformanceFish.Cache.ByReference; namespace PerformanceFish.Hauling; @@ -11,9 +11,9 @@ public record struct LwmCompCacheValue { public CompProperties? CompProperties; - public static CompProperties? TryGetLwmProps(Thing slotGroupParent) + public static CompProperties? TryGetLwmProps(ThingDef thingDef) => ModCompatibility.Types.LWM.CompProperties != null - ? slotGroupParent.def.comps?.Find(static props + ? thingDef.comps?.Find(static props => props.GetType() == ModCompatibility.Types.LWM.CompProperties) : null; @@ -35,4 +35,4 @@ public record struct LwmCompCacheValue => 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/RimfactoryExtensionCacheValue.cs b/Source/PerformanceFish/Hauling/RimfactoryExtensionCacheValue.cs index fce3331..2c3e5e3 100644 --- a/Source/PerformanceFish/Hauling/RimfactoryExtensionCacheValue.cs +++ b/Source/PerformanceFish/Hauling/RimfactoryExtensionCacheValue.cs @@ -4,7 +4,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. global using RimfactoryExtensionCache - = PerformanceFish.Cache.ByReference; + = PerformanceFish.Cache.ByReference; using RimfactoryTypes = PerformanceFish.ModCompatibility.Types.Rimfactory; namespace PerformanceFish.Hauling; @@ -13,17 +13,17 @@ public record struct RimfactoryExtensionCacheValue { public DefModExtension? ModExtension; - public static DefModExtension? TryGetExtension(Thing slotGroupParent) + public static DefModExtension? TryGetExtension(ThingDef thingDef) => RimfactoryTypes.ModExtension != null - ? slotGroupParent.def.modExtensions?.Find(_extensionPredicate) + ? thingDef.modExtensions?.Find(_extensionPredicate) : null; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool HasExtension(Thing slotGroupParent) - => RimfactoryTypes.ModExtension != null && TestExtension(slotGroupParent); + public static bool HasExtension(Thing thing) + => RimfactoryTypes.ModExtension != null && TestExtension(thing.def); [MethodImpl(MethodImplOptions.NoInlining)] - private static bool TestExtension(Thing slotGroupParent) => slotGroupParent.TryGetRimfactoryExtension() != null; + private static bool TestExtension(ThingDef thingDef) => TryGetExtension(thingDef) != null; static RimfactoryExtensionCacheValue() => EnsureInitialized(); @@ -44,4 +44,4 @@ public record struct RimfactoryExtensionCacheValue => RimfactoryTypes.ModExtension is { } extensionType ? AccessTools.FieldRefAccess(AccessTools.Field(extensionType, name)) : null; -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/Hauling/StorageExtensions.cs b/Source/PerformanceFish/Hauling/StorageExtensions.cs index e176ac0..5a85c04 100644 --- a/Source/PerformanceFish/Hauling/StorageExtensions.cs +++ b/Source/PerformanceFish/Hauling/StorageExtensions.cs @@ -24,11 +24,11 @@ public static class StorageExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static CompProperties? TryGetLwmCompProperties(this Thing thing) - => LwmCompCache.GetOrAdd(thing).CompProperties; + => LwmCompCache.GetOrAdd(thing.def).CompProperties; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DefModExtension? TryGetRimfactoryExtension(this Thing thing) - => RimfactoryExtensionCache.GetOrAdd(thing).ModExtension; + => RimfactoryExtensionCache.GetOrAdd(thing.def).ModExtension; public static int GetTotalSlots(this SlotGroup slotGroup) => slotGroup.GetTotalSlots(slotGroup.CellsList.Count); @@ -57,4 +57,4 @@ public static class StorageExtensions LwmCompCacheValue.EnsureInitialized(); RimfactoryExtensionCacheValue.EnsureInitialized(); } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/JobSystem/JobGiver_HaulOptimization.cs b/Source/PerformanceFish/JobSystem/JobGiver_HaulOptimization.cs index 6cf45ab..84c6a93 100644 --- a/Source/PerformanceFish/JobSystem/JobGiver_HaulOptimization.cs +++ b/Source/PerformanceFish/JobSystem/JobGiver_HaulOptimization.cs @@ -3,6 +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/. +using System.Linq; using PerformanceFish.ModCompatibility; namespace PerformanceFish.JobSystem; @@ -41,13 +42,21 @@ public sealed class JobGiver_HaulOptimization : ClassWithFishPatches } } - public static List SortedThingsPotentiallyNeedingHauling(List things, Pawn pawn) + public static ICollection SortedThingsPotentiallyNeedingHauling(ICollection things, Pawn pawn) { ((ThingPositionComparer)_comparer.Target).rootCell = pawn.Position; - things.Sort(_comparer); - return things; + + if (things is List list) + { + list.Sort(_comparer); + return list; + } + + var sortedThings = things.ToList(); + sortedThings.Sort(_comparer); + return sortedThings; } private static Comparison _comparer = new ThingPositionComparer().Compare; } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/Listers/Buildings.cs b/Source/PerformanceFish/Listers/Buildings.cs index 32cae14..3ad0c09 100644 --- a/Source/PerformanceFish/Listers/Buildings.cs +++ b/Source/PerformanceFish/Listers/Buildings.cs @@ -6,6 +6,7 @@ using Mono.Cecil; using Mono.Cecil.Cil; using PerformanceFish.Prepatching; +using PerformanceFish.Utility; namespace PerformanceFish.Listers; @@ -40,7 +41,21 @@ public sealed class Buildings : ClassWithFishPrepatches => ilProcessor.ReplaceBodyWith(ReplacementBody); public static IEnumerable ReplacementBody(ListerBuildings __instance) - => (IEnumerable)__instance.Cache().ColonistBuildingsByType.GetOrAdd(typeof(T)); + => typeof(Building).IsAssignableFrom(typeof(T)) + ? (IEnumerable)__instance.Cache().ColonistBuildingsByType.GetOrAdd(typeof(T)) + : FallbackLoop(__instance); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static IEnumerable FallbackLoop(ListerBuildings __instance) + { + var allBuildingsColonist = __instance.allBuildingsColonist; + + for (var i = 0; i < allBuildingsColonist.Count; i++) + { + if (allBuildingsColonist[i] is T val) + yield return val; + } + } } #endif @@ -237,11 +252,17 @@ public sealed class Buildings : ClassWithFishPrepatches if (def.building is { isNaturalRock: true }) return; + UpdateCacheOnAdd(__instance, b, def); + } + + private static void UpdateCacheOnAdd(ListerBuildings __instance, Building b, ThingDef def) + { var cache = __instance.Cache(); - + if (b.Faction == Faction.OfPlayer) { - cache.ColonistBuildingsByDef.GetOrAdd(def).Add(b); + if (!cache.ColonistBuildingsByDef.GetOrAdd(def).TryAdd(b)) + return; if (b is Building_ResearchBench researchBench) cache.ColonistResearchBenches.Add(researchBench); @@ -250,18 +271,18 @@ public sealed class Buildings : ClassWithFishPrepatches var buildingType = b.GetType(); - do + while (buildingType != null && typeof(Building).IsAssignableFrom(buildingType)) { - if (buildingType == null) + cache.ColonistBuildingsByType.GetOrAdd(buildingType).Add(b); + if (buildingType == typeof(Building)) break; - cache.ColonistBuildingsByType.GetOrAdd(buildingType).Add(b); + buildingType = buildingType.BaseType; } - while ((buildingType = buildingType.BaseType) != typeof(Building)); } else { - cache.NonColonistBuildingsByDef.GetOrAdd(def).Add(b); + cache.NonColonistBuildingsByDef.GetOrAdd(def).TryAdd(b); } } } @@ -290,6 +311,11 @@ public sealed class Buildings : ClassWithFishPrepatches if (def.building is { isNaturalRock: true }) return; + UpdateCacheOnRemove(__instance, b, def); + } + + private static void UpdateCacheOnRemove(ListerBuildings __instance, Building b, ThingDef def) + { var cache = __instance.Cache(); if (b.Faction == Faction.OfPlayer) @@ -302,14 +328,14 @@ public sealed class Buildings : ClassWithFishPrepatches var buildingType = b.GetType(); - do + while (buildingType != null && typeof(Building).IsAssignableFrom(buildingType)) { - if (buildingType == null) + cache.ColonistBuildingsByType.GetOrAdd(buildingType).Remove(b); + if (buildingType == typeof(Building)) break; - cache.ColonistBuildingsByType.GetOrAdd(buildingType).Remove(b); + buildingType = buildingType.BaseType; } - while ((buildingType = buildingType.BaseType) != typeof(Building)); } else { @@ -321,17 +347,43 @@ public sealed class Buildings : ClassWithFishPrepatches public readonly record struct Cache() { public readonly FishTable> - ColonistBuildingsByDef = [], - NonColonistBuildingsByDef = []; + ColonistBuildingsByDef = new() { ValueInitializer = static _ => InitializeBuildingsList() }, + NonColonistBuildingsByDef = new() { ValueInitializer = static _ => InitializeBuildingsList() }; - public readonly IndexedFishSet ColonistResearchBenches = []; + public readonly IndexedFishSet ColonistResearchBenches = InitializeBuildingsList(); - public readonly IndexedFishSet ColonistDeepScanners = []; + public readonly IndexedFishSet ColonistDeepScanners = InitializeBuildingsList(); public readonly FishTable ColonistBuildingsByType = new() { ValueInitializer = static type - => (IList)Activator.CreateInstance(typeof(IndexedFishSet<>).MakeGenericType(type)) + => (IList)_buildingsListInitializerDefinition.MakeGenericMethod(type).Invoke(null, null) }; + + private static readonly MethodInfo _buildingsListInitializerDefinition + = methodof(InitializeBuildingsList).GetGenericMethodDefinition(); + + public static unsafe IndexedFishSet InitializeBuildingsList() where T : Building +#pragma warning disable CS8622 + => new(static () => new(0, + keyHashCodeByRefGetter: &GetKeyByRef, + keyHashCodeGetter: &ThingHelper.GetKey, + keyEqualityByRefComparer: &EqualsByKeyByRef, + keyEqualityComparer: &EqualsByKey)); +#pragma warning restore CS8622 + + private static int GetKeyByRef(ref T thing) where T : Building => thing.GetKey(); + + private static bool EqualsByKey(T? left, T? right) where T : Building + => ReferenceEquals(left, right) + || left is not null + && right is not null + && left.GetKey() == right.GetKey(); + + private static bool EqualsByKeyByRef(ref T? left, ref T? right) where T : Building + => ReferenceEquals(left, right) + || left is not null + && right is not null + && left.GetKey() == right.GetKey(); } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/Listers/Haulables.cs b/Source/PerformanceFish/Listers/Haulables.cs index 6fd8c4f..384c511 100644 --- a/Source/PerformanceFish/Listers/Haulables.cs +++ b/Source/PerformanceFish/Listers/Haulables.cs @@ -183,22 +183,22 @@ public sealed class Haulables : ClassWithFishPrepatches public readonly HashSet ThingsQueuedToRemove = []; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void CheckUpdate(List haulables) + public void CheckUpdate(ICollection haulables) { if (IsDirty(haulables)) Update(haulables); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsDirty(List haulables) => haulables.Count != ContainedThings.Count; + public bool IsDirty(ICollection haulables) => haulables.Count != ContainedThings.Count; [MethodImpl(MethodImplOptions.NoInlining)] - public void Update(List haulables) + public void Update(ICollection haulables) { ContainedThings.Clear(); - for (var i = haulables.Count; i-- > 0;) - ContainedThings.Add(haulables[i].GetKey()); + foreach (var haulable in haulables) + ContainedThings.Add(haulable.GetKey()); } } } @@ -232,4 +232,4 @@ public static class ListerHaulablesExtensions if (cache.ContainedThings.Remove(t.GetKey())) haulables.Remove(t); } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/Listers/Things.cs b/Source/PerformanceFish/Listers/Things.cs index 60fc1bb..000fec3 100644 --- a/Source/PerformanceFish/Listers/Things.cs +++ b/Source/PerformanceFish/Listers/Things.cs @@ -12,6 +12,9 @@ namespace PerformanceFish.Listers; public sealed class ThingsPrepatches : ClassWithFishPrepatches { + private static readonly AccessTools.FieldRef> _haulSources + = AccessTools.FieldRefAccess>("haulSources"); + public sealed class AddPatch : FishPrepatch { public override string? Description { get; } @@ -31,6 +34,8 @@ public sealed class ThingsPrepatches : ClassWithFishPrepatches lock (__instance.LockObject()) { AddToDefList(__instance, t); + if (t is IHaulSource haulSource) + _haulSources(__instance).Add(haulSource); var allGroups = ThingListGroupHelper.AllGroups; foreach (var thingRequestGroup in allGroups) @@ -74,6 +79,8 @@ public sealed class ThingsPrepatches : ClassWithFishPrepatches lock (__instance.LockObject()) { RemoveFromDefList(__instance, t); + if (t is IHaulSource haulSource) + _haulSources(__instance).Remove(haulSource); var allGroups = ThingListGroupHelper.AllGroups; for (var i = 0; i < allGroups.Length; i++) @@ -268,6 +275,7 @@ public sealed class ThingsPrepatches : ClassWithFishPrepatches __instance.IndexMapByDef().Clear(); __instance.IndexMapByGroup().Clear(); + _haulSources(__instance).Clear(); ClearTypeLists(__instance); } @@ -568,9 +576,25 @@ public sealed class Things : ClassWithFishPatches public static unsafe IndexedFishSet InitializeThingsList() where T : Thing #pragma warning disable CS8622 - => new(static () => new(0, keyHashCodeByRefGetter: &GetKeyByRef, keyHashCodeGetter: &ThingHelper.GetKey)); + => new(static () => new(0, + keyHashCodeByRefGetter: &GetKeyByRef, + keyHashCodeGetter: &ThingHelper.GetKey, + keyEqualityByRefComparer: &EqualsByKeyByRef, + keyEqualityComparer: &EqualsByKey)); #pragma warning restore CS8622 private static int GetKeyByRef(ref T thing) where T : Thing => thing.GetKey(); + + private static bool EqualsByKey(T? left, T? right) where T : Thing + => ReferenceEquals(left, right) + || left is not null + && right is not null + && left.GetKey() == right.GetKey(); + + private static bool EqualsByKeyByRef(ref T? left, ref T? right) where T : Thing + => ReferenceEquals(left, right) + || left is not null + && right is not null + && left.GetKey() == right.GetKey(); } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/LogPatches.cs b/Source/PerformanceFish/LogPatches.cs index d2ac7f7..e49238d 100644 --- a/Source/PerformanceFish/LogPatches.cs +++ b/Source/PerformanceFish/LogPatches.cs @@ -59,15 +59,16 @@ public sealed class LogPatches : ClassWithFishPrepatches public static void InsertEarlyMessages() { var unhandledMessages = Log.UnhandledMessages; - if (unhandledMessages is null) - Debug.LogError("Message queue is null"); - + Log.Ready = true; - + Verse.Log.messageQueue.Enqueue(new(LogMessageType.Message, PERFORMANCE_FISH_WELCOME_MESSAGE, PERFORMANCE_FISH_WELCOME_STACKTRACE)); - while (unhandledMessages!.TryDequeue(out var message)) + if (unhandledMessages is null) + return; + + while (unhandledMessages.TryDequeue(out var message)) { try { diff --git a/Source/PerformanceFish/MiscOptimizations.cs b/Source/PerformanceFish/MiscOptimizations.cs index bde9c27..2b7287e 100644 --- a/Source/PerformanceFish/MiscOptimizations.cs +++ b/Source/PerformanceFish/MiscOptimizations.cs @@ -114,17 +114,30 @@ public sealed class MiscOptimizations : ClassWithFishPatches = "Throttles rotting on misconfigured defs to only recalculate every 256 ticks, instead of constantly."; public override MethodBase TargetMethodInfo { get; } +#if V1_6 + = AccessTools.DeclaredMethod(typeof(RimWorld.CompRottable), nameof(RimWorld.CompRottable.CompTickInterval), + [typeof(int)])!; +#else = AccessTools.Method(typeof(RimWorld.CompRottable), nameof(RimWorld.CompRottable.CompTick)); +#endif public static CodeInstructions Transpiler(CodeInstructions codes, ILGenerator generator) => Reflection.GetCodeInstructions(Replacement, generator); [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if V1_6 + public static void Replacement(RimWorld.CompRottable instance, int delta) + { + if ((TickHelper.TicksGame & 255) == (instance.parent.thingIDNumber & 255)) + instance.TickInterval(256); + } +#else public static void Replacement(RimWorld.CompRottable instance) { if ((TickHelper.TicksGame & 255) == (instance.parent.thingIDNumber & 255)) instance.Tick(256); } +#endif } public sealed class RitualObligationTrigger_Date : FirstPriorityFishPatch @@ -344,4 +357,4 @@ public sealed class MiscOptimizations : ClassWithFishPatches } #endif } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/ModsConfigPatches.cs b/Source/PerformanceFish/ModsConfigPatches.cs index b6c3ad8..f6d1432 100644 --- a/Source/PerformanceFish/ModsConfigPatches.cs +++ b/Source/PerformanceFish/ModsConfigPatches.cs @@ -17,7 +17,8 @@ public sealed class ModsConfigPatches : ClassWithFishPrepatches = "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 MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ModsConfig), nameof(ModsConfig.AreAllActive), [typeof(string)])!; public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) => IsActiveFix(ilProcessor, module); @@ -29,7 +30,9 @@ public sealed class ModsConfigPatches : ClassWithFishPrepatches = "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 MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(ModsConfig), nameof(ModsConfig.IsAnyActiveOrEmpty), + [typeof(IEnumerable), typeof(bool)])!; public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) => IsActiveFix(ilProcessor, module); @@ -59,4 +62,4 @@ public sealed class ModsConfigPatches : ClassWithFishPrepatches } public static bool IsActiveReplacement(string id) => ModLister.GetActiveModWithIdentifier(id, true) != null; -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/NeedsTrackerCaching.cs b/Source/PerformanceFish/NeedsTrackerCaching.cs new file mode 100644 index 0000000..df4f8ff --- /dev/null +++ b/Source/PerformanceFish/NeedsTrackerCaching.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 System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using PerformanceFish.Prepatching; + +namespace PerformanceFish; + +// TODO: benchmark + +public sealed class NeedsTrackerCaching : ClassWithFishPrepatches +{ + public sealed class TryGetNeedPatch : FishPrepatch + { + public override string? Description { get; } + = "Caches generic need lookups behind the needs list version."; + + public override MethodBase TargetMethodBase { get; } = GetTargetMethod(outParameter: false); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(TryGetNeed); + + public static T? TryGetNeed(Pawn_NeedsTracker instance) where T : Need + => TryGetNeedInternal(instance, out T? need) ? need : null; + } + + public sealed class TryGetNeedOutPatch : FishPrepatch + { + public override string? Description { get; } + = "Caches generic need lookups behind the needs list version."; + + public override MethodBase TargetMethodBase { get; } = GetTargetMethod(outParameter: true); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(TryGetNeed); + + public static bool TryGetNeed(Pawn_NeedsTracker instance, out T? need) where T : Need + => TryGetNeedInternal(instance, out need); + } + + private static bool TryGetNeedInternal(Pawn_NeedsTracker instance, out T? need) where T : Need + { + ref var cache + = ref Cache.ByReference.GetOrAddReference(instance, + typeof(T).TypeHandle); + + var needs = instance.needs; + if (!cache.IsDirty(needs)) + { + need = (T?)cache.Need; + return cache.Found; + } + + return UpdateCache(instance, typeof(T), out need, ref cache); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool UpdateCache(Pawn_NeedsTracker instance, Type needType, out T? need, ref CacheValue cache) + where T : Need + { + var needs = instance.needs; + for (var i = 0; i < needs.Count; i++) + { + var candidate = needs[i]; + if (candidate.GetType() != needType) + continue; + + need = (T)candidate; + cache.Update(needs, need, found: true); + return true; + } + + need = null; + cache.Update(needs, null, found: false); + return false; + } + + private static MethodInfo GetTargetMethod(bool outParameter) + { + foreach (var method in typeof(Pawn_NeedsTracker).GetMethods(AccessTools.all)) + { + if (method.Name != nameof(Pawn_NeedsTracker.TryGetNeed) + || !method.IsGenericMethodDefinition + || method.GetParameters().Length != (outParameter ? 1 : 0)) + { + continue; + } + + return method; + } + + return ThrowMissingMethodException(outParameter); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static MethodInfo ThrowMissingMethodException(bool outParameter) + => throw new MissingMethodException( + $"Could not find Pawn_NeedsTracker.TryGetNeed generic overload with {(outParameter ? "one" : "zero")} parameter(s)."); + + public record struct CacheValue() + { + private List? _listReference; + private int _listVersion = -2; + public Need? Need; + public bool Found; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDirty(List needs) => !ReferenceEquals(needs, _listReference) || needs._version != _listVersion; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(List needs, Need? need, bool found) + { + _listReference = needs; + _listVersion = needs._version; + Need = need; + Found = found; + } + } +} diff --git a/Source/PerformanceFish/OnAssembliesLoaded.cs b/Source/PerformanceFish/OnAssembliesLoaded.cs index 7690c00..48d0b40 100644 --- a/Source/PerformanceFish/OnAssembliesLoaded.cs +++ b/Source/PerformanceFish/OnAssembliesLoaded.cs @@ -50,7 +50,9 @@ public static class OnAssembliesLoaded try { if (!patchClass.RequiresLoadedGameForPatching) + { patchClass.Patches.PatchAll(); + } } catch (Exception ex) { @@ -122,4 +124,4 @@ public static class OnAssembliesLoaded action.Method.FullDescription()}:\n{ex}"); } } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/Patching/FishPatch.cs b/Source/PerformanceFish/Patching/FishPatch.cs index 4bd0b6a..84d3ae5 100644 --- a/Source/PerformanceFish/Patching/FishPatch.cs +++ b/Source/PerformanceFish/Patching/FishPatch.cs @@ -193,8 +193,6 @@ public abstract class FishPatch : SingletonFactory, IExposable, IHasD Patched = true; foreach (var method in TargetMethodInfos) { - DebugLog.Message($"Performance Fish is applying {GetType().Name} on {method.FullDescription()}"); - try { if (IsBenchmarking) @@ -232,6 +230,7 @@ public abstract class FishPatch : SingletonFactory, IExposable, IHasD finalizer: TryMakeHarmonyMethod(FinalizerMethodInfo, FinalizerMethodPriority))); } } + } catch (Exception e) { @@ -291,6 +290,9 @@ public abstract class FishPatch : SingletonFactory, IExposable, IHasD private MethodInfo? TryGetMethod(string nameCaseInsensitive, Predicate? predicate = null) => GetType().TryGetMethod(nameCaseInsensitive, predicate); + private static string GetTargetMethodName(MethodBase method) + => $"{method.DeclaringType?.FullName ?? ""}.{method.Name}"; + private static int TryGetPriority(MethodInfo? info) => info?.TryGetAttribute()?.info.priority ?? Priority.Normal; @@ -378,4 +380,4 @@ internal static class Benchmarking Method = method; } } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/Patching/FishPatchHolder.cs b/Source/PerformanceFish/Patching/FishPatchHolder.cs index 77c9e2b..e96dcba 100644 --- a/Source/PerformanceFish/Patching/FishPatchHolder.cs +++ b/Source/PerformanceFish/Patching/FishPatchHolder.cs @@ -59,7 +59,9 @@ public class FishPatchHolder : IExposable, IEnumerable private void AddPatchesRecursively(Type type) { - if (typeof(FishPatch).IsAssignableFrom(type) && !All.ContainsKey(type)) + if (typeof(FishPatch).IsAssignableFrom(type) + && !type.IsAbstract + && !All.ContainsKey(type)) { if (PerformanceFishMod.AllPatchClasses is { } allPatches) RemoveDupes(allPatches, type); @@ -92,4 +94,4 @@ public class FishPatchHolder : IExposable, IEnumerable IEnumerator IEnumerable.GetEnumerator() => All.Values.GetEnumerator(); private Type _type; -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/PerformanceFish.sln b/Source/PerformanceFish/PerformanceFish.sln index d417805..d68e95d 100644 --- a/Source/PerformanceFish/PerformanceFish.sln +++ b/Source/PerformanceFish/PerformanceFish.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.1.31911.260 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "1.5", "1.5.csproj", "{E8A3D63E-5850-4866-AB06-DBD44B11F67B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "1.6", "1.6.csproj", "{8B299603-8E75-4C35-AFC8-89748599E689}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "1.4", "1.4.csproj", "{0257ABCD-7009-4C7D-9BD8-599FC47595DB}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6948F7E6-7ED7-4716-87EF-CFF1F465F561}" @@ -24,6 +26,10 @@ Global {E8A3D63E-5850-4866-AB06-DBD44B11F67B}.Debug|Any CPU.Build.0 = Debug|Any CPU {E8A3D63E-5850-4866-AB06-DBD44B11F67B}.Release|Any CPU.ActiveCfg = Release|Any CPU {E8A3D63E-5850-4866-AB06-DBD44B11F67B}.Release|Any CPU.Build.0 = Release|Any CPU + {8B299603-8E75-4C35-AFC8-89748599E689}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B299603-8E75-4C35-AFC8-89748599E689}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B299603-8E75-4C35-AFC8-89748599E689}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B299603-8E75-4C35-AFC8-89748599E689}.Release|Any CPU.Build.0 = Release|Any CPU {0257ABCD-7009-4C7D-9BD8-599FC47595DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0257ABCD-7009-4C7D-9BD8-599FC47595DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {0257ABCD-7009-4C7D-9BD8-599FC47595DB}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs b/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs index 906f3d8..eefb747 100644 --- a/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs +++ b/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs @@ -15,6 +15,10 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches { public sealed class WorldObjectsHolderTickPatch : FishPrepatch { + private static FishTable _skippableCompTypes = null!; + private static FishTable _skippableWorldObjectTypes = null!; + private static readonly List _tmpThingHolders = []; + public override string? Description { get; } = "The world objects holder is responsible for ticking every world object. This includes settlements, " + "caravans, outposts and any other object placed in the world, instead of maps. Normally it ticks " @@ -35,7 +39,11 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches var worldObjects = WorldObjectsHolder.tmpWorldObjects; for (var i = worldObjects.Count; i-- > 0;) +#if V1_6 + worldObjects[i].DoTick(); +#else worldObjects[i].Tick(); +#endif } public static void UpdateCache(WorldObjectsHolder instance) @@ -50,7 +58,7 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches var worldObject = instanceWorldObjects[i]; if (worldObject is not MapParent { HasMap: true } - && SkippableWorldObjects.Contains(worldObject.GetType()) + && CanSkipWorldObjectTick(worldObject) && (worldObject is not Settlement settlement || settlement.trader?.stock is null) && CanSkipCompTick(worldObject)) { @@ -64,9 +72,46 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches CachedMapsVersion = Current.gameInt.maps._version; } + private static bool CanSkipWorldObjectTick(WorldObject worldObject) + { + return IsSkippableWorldObjectType(worldObject.GetType()) && !HasTickableContents(worldObject); + } + + private static bool HasTickableContents(WorldObject worldObject) + { + if (worldObject is not IThingHolder holder) + return false; + + lock (_tmpThingHolders) + { + _tmpThingHolders.Clear(); + _tmpThingHolders.Add(holder); + holder.GetChildHolders(_tmpThingHolders); + + for (var i = 0; i < _tmpThingHolders.Count; i++) + { + var thingHolder = _tmpThingHolders[i]; + if (thingHolder is IThingHolderTickable tickable && !tickable.ShouldTickContents) + continue; + + var directlyHeldThings = thingHolder.GetDirectlyHeldThings(); + if (directlyHeldThings?.Owner is not null and not Map and not Caravan) + { + _tmpThingHolders.Clear(); + return true; + } + } + + _tmpThingHolders.Clear(); + return false; + } + } + private static bool CanSkipCompTick(WorldObject worldObject) { var comps = worldObject.comps; + if (comps.Count == 0) + return true; for (var i = comps.Count; i-- > 0;) { @@ -74,10 +119,8 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches if (comp is EnterCooldownComp { Active: true }) return false; - if (!SkippableComps.Contains(comp.GetType())) + if (!IsSkippableCompType(comp.GetType())) return false; - - comp.CompTick(); } return true; @@ -90,10 +133,7 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches private static Type?[] _whitelistedTickingCompTypes = [ - typeof(WorldObjectComp), - typeof(FormCaravanComp), - typeof(TimedDetectionRaids), - typeof(EnterCooldownComp) + typeof(WorldObjectComp) ], _whitelistedWorldObjectTypes = [ @@ -106,28 +146,69 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches public static void AddCompToWhiteList(Type compType) { _whitelistedTickingCompTypes = _whitelistedTickingCompTypes.Add(compType); - SkippableComps = InitializeSkippableComps(); + ResetSkippableCompTypes(); } public static void AddWorldObjectToWhiteList(Type worldObjectType) { _whitelistedWorldObjectTypes = _whitelistedWorldObjectTypes.Add(worldObjectType); - SkippableWorldObjects = InitializeSkippableWorldObjects(); + ResetSkippableWorldObjectTypes(); } - public static HashSet - SkippableComps = InitializeSkippableComps(), - SkippableWorldObjects = InitializeSkippableWorldObjects(); + private static void ResetSkippableCompTypes() + { + _skippableCompTypes = new() + { + ValueInitializer = static type => ComputeIsSkippableCompType(type) + }; + } - private static HashSet InitializeSkippableComps() - => MakeSubclassHashSet(typeof(WorldObjectComp), nameof(WorldObjectComp.CompTick), - _whitelistedTickingCompTypes); + private static void ResetSkippableWorldObjectTypes() + { + _skippableWorldObjectTypes = new() + { + ValueInitializer = static type => ComputeIsSkippableWorldObjectType(type) + }; + } - private static HashSet InitializeSkippableWorldObjects() - => MakeSubclassHashSet(typeof(WorldObject), nameof(WorldObject.Tick), _whitelistedWorldObjectTypes); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSkippableCompType(Type type) => _skippableCompTypes.GetOrAdd(type); - private static HashSet MakeSubclassHashSet(Type type, string name, Type?[] allowedDeclaringTypes) - => type.SubclassesWithNoMethodOverrideAndSelf(allowedDeclaringTypes, name).ToHashSet(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSkippableWorldObjectType(Type type) => _skippableWorldObjectTypes.GetOrAdd(type); + + private static bool ComputeIsSkippableCompType(Type type) + { + if (AccessTools.Method(type, nameof(WorldObjectComp.CompTick))?.DeclaringType is not { } declaringType + || !_whitelistedTickingCompTypes.Contains(declaringType)) + { + return false; + } + +#if V1_6 + return AccessTools.Method(type, nameof(WorldObjectComp.CompTickInterval), [typeof(int)])?.DeclaringType + is { } intervalDeclaringType + && _whitelistedTickingCompTypes.Contains(intervalDeclaringType); +#else + return true; +#endif + } + + private static bool ComputeIsSkippableWorldObjectType(Type type) + { + if (AccessTools.Method(type, "Tick")?.DeclaringType is not { } declaringType + || !_whitelistedWorldObjectTypes.Contains(declaringType)) + { + return false; + } + +#if V1_6 + return AccessTools.Method(type, "TickInterval", [typeof(int)])?.DeclaringType is { } intervalDeclaringType + && _whitelistedWorldObjectTypes.Contains(intervalDeclaringType); +#else + return true; +#endif + } public static bool CacheDirty(WorldObjectsHolder instance) => CachedWorldObjectsVersion != instance.worldObjects._version @@ -135,7 +216,12 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches public static void SetDirty() => CachedWorldObjectsVersion = CachedMapsVersion = -2; - static WorldObjectsHolderTickPatch() => Cache.Utility.Cleared += SetDirty; + static WorldObjectsHolderTickPatch() + { + ResetSkippableCompTypes(); + ResetSkippableWorldObjectTypes(); + Cache.Utility.Cleared += SetDirty; + } } public sealed class ExpandingIconCaching : FishPrepatch @@ -245,4 +331,4 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches } } } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/PrepatcherFields.cs b/Source/PerformanceFish/PrepatcherFields.cs index b00ffd4..dd19b40 100644 --- a/Source/PerformanceFish/PrepatcherFields.cs +++ b/Source/PerformanceFish/PrepatcherFields.cs @@ -9,6 +9,7 @@ using PerformanceFish.Hauling; using PerformanceFish.Planet; using Prepatcher; using RimWorld.Planet; +using PFMapEvents = PerformanceFish.Events.MapEvents; namespace PerformanceFish; @@ -22,7 +23,7 @@ public static class PrepatcherFields [PrepatcherField] [ValueInitializer(nameof(CreateMapEvents))] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static extern MapEvents.Instanced Events(this Map map); + public static extern PFMapEvents.Instanced Events(this Map map); [PrepatcherField] [ValueInitializer(nameof(CreateThingEvents))] @@ -175,7 +176,7 @@ public static class PrepatcherFields 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 PFMapEvents.Instanced CreateMapEvents() => new(); public static ThingEvents.Instanced CreateThingEvents() => new(); public static StorageSettingsPatches.StorageSettingsCache CreateStorageSettingsCache() => new(); public static StorageDistrict[] GetDefaultDistrictArray() => StorageDistrict.GetDefaultArray(); @@ -290,4 +291,4 @@ public static class PrepatcherFields } // public static List CreateHediffCompList() => new(); -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/Prepatching/FishPrepatch.cs b/Source/PerformanceFish/Prepatching/FishPrepatch.cs index be25dba..edef402 100644 --- a/Source/PerformanceFish/Prepatching/FishPrepatch.cs +++ b/Source/PerformanceFish/Prepatching/FishPrepatch.cs @@ -4,6 +4,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. using System.Diagnostics; +using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; @@ -104,7 +105,7 @@ public abstract class FishPrepatch : FishPrepatchBase Log.Error($"Exception while applying postfix for {GetType().FullName}\n{e}\n{new StackTrace(true)}"); } } - + targetMethodBody.SimplifyMacros(); targetMethodBody.OptimizeMacros(); } @@ -192,14 +193,26 @@ public abstract class FishPrepatch : FishPrepatchBase 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 hasReturnValue = targetMethod.ReturnType.FullName != module.TypeSystem.Void.FullName; + var resultVariable = hasReturnValue + ? patchVariables.Result ??= ilProcessor.DeclareLocal(targetMethod.ReturnType) + : GetResultVariable(ilProcessor, ref patchVariables, resultParameter, targetMethod); var stateVariable = GetStateVariable(ilProcessor, ref patchVariables, stateParameter); + var retInstructions = instructions.Where(static instruction => instruction.OpCode == OpCodes.Ret).ToArray(); + if (retInstructions.Length == 0) + return; var postfixInstructions = new Collection(); - if (resultVariable != null) - postfixInstructions.Add((OpCodes.Stloc, resultVariable)); + if (hasReturnValue) + { + postfixInstructions.Add((OpCodes.Stloc, resultVariable!)); + postfixInstructions.Add(OpCodes.Nop); + } + else + { + postfixInstructions.Add(OpCodes.Nop); + } foreach (var parameter in postfixParameters) { @@ -217,36 +230,87 @@ public abstract class FishPrepatch : FishPrepatchBase postfixInstructions.Add((OpCodes.Call, postfixMethodInfo)); } - if (postfixMethodInfo.ReturnType == typeof(void) && resultVariable != null) - postfixInstructions.Add((OpCodes.Ldloc, resultVariable)); + if (postfixMethodInfo.ReturnType != typeof(void)) + postfixInstructions.Add((OpCodes.Stloc, 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; - } + if (hasReturnValue) + postfixInstructions.Add((OpCodes.Ldloc, resultVariable!)); - var lastRetInstructionIndex = retInstructions[retInstructionCount - 1]; + var lastRetInstruction = retInstructions[^1]; + var lastRetInstructionIndex = instructions.IndexOf(lastRetInstruction); ilProcessor.InsertRange(lastRetInstructionIndex, postfixInstructions); + var captureStart = instructions[lastRetInstructionIndex]; + var postfixStart = instructions[lastRetInstructionIndex + (hasReturnValue ? 1 : 0)]; - for (var i = 0; i < instructions.Count; i++) + RetargetExceptionHandlerEnds(targetMethod.Body, lastRetInstruction, captureStart); + var retInstructionSet = retInstructions.ToHashSet(); + foreach (var instruction in instructions) { - var instruction = instructions[i]; - if (instruction.Operand is Instruction branchTarget && branchTarget.OpCode == OpCodes.Ret) - instruction.Operand = instructions[lastRetInstructionIndex]; + if (instruction.Operand is Instruction branchTarget && retInstructionSet.Contains(branchTarget)) + instruction.Operand = captureStart; } - for (var i = 0; i < retInstructionCount - 1; i++) + for (var i = 0; i < retInstructions.Length - 1; i++) { - var instruction = instructions[retInstructions[i]]; - instruction.OpCode = OpCodes.Br; - instruction.Operand = instructions[lastRetInstructionIndex]; + var instruction = retInstructions[i]; + if (hasReturnValue) + ilProcessor.InsertBefore(instruction, Instruction.Create(OpCodes.Stloc, resultVariable!)); + + instruction.OpCode = IsInProtectedRegion(targetMethod.Body, instruction) ? OpCodes.Leave : OpCodes.Br; + instruction.Operand = postfixStart; } } + private static void RetargetExceptionHandlerEnds(Mono.Cecil.Cil.MethodBody methodBody, Instruction oldBoundary, + Instruction newBoundary) + { + foreach (var exceptionHandler in methodBody.ExceptionHandlers) + { + if (exceptionHandler.TryEnd == oldBoundary) + exceptionHandler.TryEnd = newBoundary; + if (exceptionHandler.HandlerEnd == oldBoundary) + exceptionHandler.HandlerEnd = newBoundary; + } + } + + private static bool IsInProtectedRegion(Mono.Cecil.Cil.MethodBody methodBody, Instruction instruction) + { + var instructions = methodBody.Instructions; + var instructionIndex = instructions.IndexOf(instruction); + + foreach (var exceptionHandler in methodBody.ExceptionHandlers) + { + if (IsWithinRange(instructions, instructionIndex, exceptionHandler.TryStart, exceptionHandler.TryEnd) + || IsWithinRange(instructions, instructionIndex, exceptionHandler.HandlerStart, + exceptionHandler.HandlerEnd) + || exceptionHandler.FilterStart != null + && IsWithinRange(instructions, instructionIndex, exceptionHandler.FilterStart, + exceptionHandler.HandlerStart)) + { + return true; + } + } + + return false; + } + + private static bool IsWithinRange(Mono.Collections.Generic.Collection instructions, + int instructionIndex, Instruction? rangeStart, Instruction? rangeEnd) + { + if (rangeStart == null) + return false; + + var rangeStartIndex = instructions.IndexOf(rangeStart); + if (rangeStartIndex < 0 || instructionIndex < rangeStartIndex) + return false; + + if (rangeEnd == null) + return true; + + var rangeEndIndex = instructions.IndexOf(rangeEnd); + return rangeEndIndex < 0 || instructionIndex < rangeEndIndex; + } + private static VariableDefinition? GetStateVariable(ILProcessor ilProcessor, ref PatchVariables patchVariables, ParameterInfo? stateParameter) => stateParameter != null @@ -352,4 +416,4 @@ public abstract class FishPrepatch : FishPrepatchBase { public VariableDefinition? Result, State; } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/Prepatching/FishPrepatchExtensions.cs b/Source/PerformanceFish/Prepatching/FishPrepatchExtensions.cs index 50a2373..a1bb787 100644 --- a/Source/PerformanceFish/Prepatching/FishPrepatchExtensions.cs +++ b/Source/PerformanceFish/Prepatching/FishPrepatchExtensions.cs @@ -13,8 +13,9 @@ public static class FishPrepatchExtensions { var prepatches = ToSortedList(allPrepatchClasses, true); - foreach (var prepatch in prepatches) + for (var i = 0; i < prepatches.Count; i++) { + var prepatch = prepatches[i]; if (!prepatch.Enabled) continue; @@ -40,8 +41,9 @@ public static class FishPrepatchExtensions { var prepatches = ToSortedList(allPrepatchClasses, false); - foreach (var prepatch in prepatches) + for (var i = 0; i < prepatches.Count; i++) { + var prepatch = prepatches[i]; if (!prepatch.IsActive) continue; @@ -70,4 +72,5 @@ public static class FishPrepatchExtensions 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 b436f83..1ebf83e 100644 --- a/Source/PerformanceFish/Prepatching/FishPrepatchHolder.cs +++ b/Source/PerformanceFish/Prepatching/FishPrepatchHolder.cs @@ -37,7 +37,9 @@ public class FishPrepatchHolder : IExposable, IEnumerable private void AddPatchesRecursively(Type type) { - if (typeof(FishPrepatchBase).IsAssignableFrom(type) && !All.ContainsKey(type)) + if (typeof(FishPrepatchBase).IsAssignableFrom(type) + && !type.IsAbstract + && !All.ContainsKey(type)) { if (PerformanceFishMod.AllPrepatchClasses is { } allPatches) RemoveDupes(allPatches, type); @@ -69,4 +71,4 @@ public class FishPrepatchHolder : IExposable, IEnumerable IEnumerator IEnumerable.GetEnumerator() => All.Values.GetEnumerator(); private Type _type; -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/Rendering/DynamicDrawManagerPatches.cs b/Source/PerformanceFish/Rendering/DynamicDrawManagerPatches.cs index 934292d..269817c 100644 --- a/Source/PerformanceFish/Rendering/DynamicDrawManagerPatches.cs +++ b/Source/PerformanceFish/Rendering/DynamicDrawManagerPatches.cs @@ -68,40 +68,79 @@ public sealed class DynamicDrawManagerPatches : ClassWithFishPrepatches public static void CullAndInitializeThings(DynamicDrawManager instance) { var map = instance.map; - var mapSizeX = map.SizeX(); + var cellIndices = map.cellIndices; + var mapSizeX = GetMapSizeX(cellIndices); var fogGrid = map.fogGrid.fogGrid; var snowGrid = map.snowGrid.depthGrid; var checkShadows = MatBases.SunShadow.shader.isSupported; - var currentViewRect = Find.CameraDriver.CurrentViewRect.ClipInsideMap(map).ExpandedBy(1); + var currentViewRect = Find.CameraDriver.CurrentViewRect.ExpandedBy(1); +#if V1_6 + if (WorldComponent_GravshipController.GravshipRenderInProgess) + currentViewRect = currentViewRect.Encapsulate(WorldComponent_GravshipController.GravshipRenderBounds); +#endif + currentViewRect = currentViewRect.ClipInsideMap(map); var shadowViewRect = SectionLayer_SunShadows.GetSunShadowsViewRect(map, currentViewRect); instance.drawThings.UnwrapArray(out var drawThings, out var drawThingsCount); for (var i = 0; i < drawThingsCount; i++) { var drawThing = drawThings.UnsafeLoad(i); - var position = drawThing.Position; + var position = GetCullPosition(drawThing); var drawThingDef = drawThing.def; var cellIndex = position.CellToIndex(mapSizeX); - if ((!fogGrid[cellIndex] || drawThingDef.seeThroughFog) - && (drawThingDef.hideAtSnowDepth >= 1f - || drawThingDef.hideAtSnowDepth >= snowGrid[cellIndex])) + if (!CellIndicesContain(cellIndices, cellIndex) + || (!drawThingDef.seeThroughFog +#if V1_6 + && fogGrid.IsSet(cellIndex)) +#else + && fogGrid[cellIndex]) +#endif +#if V1_6 + || (drawThingDef.hideAtSnowOrSandDepth < 1f + && snowGrid[cellIndex] > drawThingDef.hideAtSnowOrSandDepth)) +#else + || (drawThingDef.hideAtSnowDepth < 1f + && snowGrid[cellIndex] > drawThingDef.hideAtSnowDepth)) +#endif { - if (currentViewRect.Contains(position) - || currentViewRect.Overlaps(drawThing.OccupiedDrawRect())) - { - ThingsToDraw.Add(drawThing); - - if (!DebugViewSettings.singleThreadedDrawing) - EnsureInitialized(drawThing); - } - else if (drawThing is Pawn pawn && checkShadows && shadowViewRect.Contains(drawThing.Position)) - { - PawnShadowsToDraw.Add(pawn); - } + continue; + } + + if (currentViewRect.Contains(position) + || currentViewRect.Overlaps(drawThing.OccupiedDrawRect())) + { + ThingsToDraw.Add(drawThing); + + if (!DebugViewSettings.singleThreadedDrawing) + EnsureInitialized(drawThing); + } + else if (drawThing is Pawn pawn && checkShadows && shadowViewRect.Contains(position)) + { + PawnShadowsToDraw.Add(pawn); } } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static IntVec3 GetCullPosition(Thing drawThing) + => drawThing is Pawn ? drawThing.DrawPos.ToIntVec3() : drawThing.Position; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CellIndicesContain(CellIndices cellIndices, int index) +#if V1_6 + => cellIndices.Contains(index); +#else + => (uint)index < (uint)(cellIndices.mapSizeX * cellIndices.mapSizeZ); +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetMapSizeX(CellIndices cellIndices) +#if V1_6 + => cellIndices.SizeX; +#else + => cellIndices.mapSizeX; +#endif + public static void EnsureInitialized(Thing drawThing) { try @@ -228,4 +267,4 @@ public sealed class DynamicDrawManagerPatches : ClassWithFishPrepatches } } } -#endif \ No newline at end of file +#endif diff --git a/Source/PerformanceFish/Rendering/PawnRenderingOptimizations.cs b/Source/PerformanceFish/Rendering/PawnRenderingOptimizations.cs new file mode 100644 index 0000000..7637478 --- /dev/null +++ b/Source/PerformanceFish/Rendering/PawnRenderingOptimizations.cs @@ -0,0 +1,134 @@ +// Copyright (c) 2026 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.Rendering; + +public sealed class PawnRenderingOptimizations : ClassWithFishPrepatches +{ + private const int DAMAGE_FLASH_TICKS_TOTAL = 16; + + public sealed class IsHiddenFromPlayerPatch : FishPrepatch + { + // not 100% on this even benefitting, bionics, scars, etc likely worsen performance here because of the extra check + // probably better to just track if a hediff adds invisibility + public override string? Description { get; } + = "Fast-paths InvisibilityUtility.IsHiddenFromPlayer for the case where a pawn has no hediffs."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(InvisibilityUtility), + nameof(InvisibilityUtility.IsHiddenFromPlayer), [typeof(Pawn)]); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ReplacementBody(Pawn pawn) + { + if (DebugSettings.showHiddenPawns) + return false; + + if (pawn.Faction == Faction.OfPlayer) + return false; + + var hediffs = pawn.health.hediffSet.hediffs; + if (hediffs.Count == 0) + return false; + + for (var i = 0; i < hediffs.Count; i++) + { + var comp = hediffs[i].TryGetComp(); + if (comp != null && !comp.Props.visibleToPlayer && !comp.PsychologicallyVisible) + return true; + } + + return false; + } + } + + public sealed class PawnRenderTreeComputeMatrixPatch : FishPrepatch + { + public override string? Description { get; } + = "Avoids Matrix4x4 inverse work when undoing a pivot translation in PawnRenderTree.ComputeMatrix."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(PawnRenderTree), "ComputeMatrix"); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReplacementBody(PawnRenderTree instance, ref Matrix4x4 matrix, in Vector3 offset, + in Vector3 pivot, in Quaternion rotation, in Vector3 scale, bool canRotate) + { + if (offset != Vector3.zero) + matrix *= Matrix4x4.Translate(offset); + + if (pivot != Vector3.zero) + matrix *= Matrix4x4.Translate(pivot); + + if (canRotate && rotation != Quaternion.identity) + matrix *= Matrix4x4.Rotate(rotation); + + if (scale != Vector3.one) + matrix *= Matrix4x4.Scale(scale); + + if (pivot != Vector3.zero) + matrix *= Matrix4x4.Translate(-pivot); + } + } + + public sealed class DamageFlasherCurColorPatch : FishPrepatch + { + private static readonly AccessTools.FieldRef _lastDamageTick + = AccessTools.FieldRefAccess("lastDamageTick"); + + public override string? Description { get; } + = "Fast-paths DamageFlasher.CurColor when no damage flash is active."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredPropertyGetter(typeof(DamageFlasher), nameof(DamageFlasher.CurColor)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Color ReplacementBody(DamageFlasher instance) + { + var ticksLeft = _lastDamageTick(instance) + DAMAGE_FLASH_TICKS_TOTAL - Find.TickManager.TicksGame; + return ticksLeft <= 0 + ? Color.white + : Color.Lerp(Color.white, DamagedMatPool.DamagedMatStartingColor, + (float)ticksLeft / DAMAGE_FLASH_TICKS_TOTAL); + } + } + + public sealed class DamageFlasherGetDamagedMatPatch : FishPrepatch + { + private static readonly AccessTools.FieldRef _lastDamageTick + = AccessTools.FieldRefAccess("lastDamageTick"); + + public override string? Description { get; } + = "Fast-paths DamageFlasher.GetDamagedMat when no damage flash is active."; + + public override MethodBase TargetMethodBase { get; } + = AccessTools.DeclaredMethod(typeof(DamageFlasher), nameof(DamageFlasher.GetDamagedMat)); + + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Material ReplacementBody(DamageFlasher instance, Material baseMat) + { + var ticksLeft = _lastDamageTick(instance) + DAMAGE_FLASH_TICKS_TOTAL - Find.TickManager.TicksGame; + return ticksLeft <= 0 + ? baseMat + : DamagedMatPool.GetDamageFlashMat(baseMat, (float)ticksLeft / DAMAGE_FLASH_TICKS_TOTAL); + } + } +} diff --git a/Source/PerformanceFish/Rendering/TextureAtlasPatches.cs b/Source/PerformanceFish/Rendering/TextureAtlasPatches.cs new file mode 100644 index 0000000..39c1b32 --- /dev/null +++ b/Source/PerformanceFish/Rendering/TextureAtlasPatches.cs @@ -0,0 +1,52 @@ +// 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.Rendering; + +public sealed class TextureAtlasPatches : ClassWithFishPatches +{ + public sealed class StaticTextureAtlas_ApplyTextureCompression : FishPatch + { + public override bool DefaultState => false; + + public override string? Description { get; } + = "Routes texture atlas compression through the GPU path when compute shaders are available, " + + "regardless of the VRAM threshold check in UnityData.ComputeShadersSupported. Prevents native " + + "crashes in debug Mono builds."; + + public override MethodBase TargetMethodInfo + => AccessTools.DeclaredMethod(typeof(StaticTextureAtlas), "ApplyTextureCompression"); + + public static bool Prefix(StaticTextureAtlas __instance, bool noGpuCompressionSupport) + { + if (!noGpuCompressionSupport || !SystemInfo.supportsComputeShaders) + return true; + + if (__instance.ColorTexture != null) + { + var name = __instance.ColorTexture.name; + var compressed = StaticTextureAtlas.FastCompressDXT(__instance.ColorTexture, deleteOriginal: true); + compressed.name = name; + _colorTextureField.SetValue(__instance, compressed); + } + + if (__instance.MaskTexture != null) + { + var name = __instance.MaskTexture.name; + var compressed = StaticTextureAtlas.FastCompressDXT(__instance.MaskTexture, deleteOriginal: true); + compressed.name = name; + _maskTextureField.SetValue(__instance, compressed); + } + + return false; + } + + private static readonly FieldInfo _colorTextureField + = AccessTools.DeclaredField(typeof(StaticTextureAtlas), "colorTexture"); + + private static readonly FieldInfo _maskTextureField + = AccessTools.DeclaredField(typeof(StaticTextureAtlas), "maskTexture"); + } +} diff --git a/Source/PerformanceFish/Rendering/TextureLoadingPatches.cs b/Source/PerformanceFish/Rendering/TextureLoadingPatches.cs index a261112..0a8c90c 100644 --- a/Source/PerformanceFish/Rendering/TextureLoadingPatches.cs +++ b/Source/PerformanceFish/Rendering/TextureLoadingPatches.cs @@ -93,9 +93,9 @@ public sealed class TextureLoadingPatches : ClassWithFishPrepatches var data = file.ReadAllBytes(); texture2D = new(2, 2, TextureFormat.Alpha8, true); texture2D.LoadImage(data); - FixMipMapsIfNeeded(texture2D, data, file); + FixMipMapsIfNeeded(ref texture2D, data, file); } - + if (!isDds && Prefs.TextureCompression) texture2D.Compress(true); @@ -120,11 +120,11 @@ public sealed class TextureLoadingPatches : ClassWithFishPrepatches var texture2D = new Texture2D(2, 2, TextureFormat.Alpha8, true); texture2D.LoadImage(data); - FixMipMapsIfNeeded(texture2D, data, file); + FixMipMapsIfNeeded(ref texture2D, data, file); return texture2D; } - public static void FixMipMapsIfNeeded(Texture2D texture2D, byte[] data, VirtualFile file) + public static void FixMipMapsIfNeeded(ref Texture2D texture2D, byte[] data, VirtualFile file) { if (((texture2D.width & 3) == 0) & ((texture2D.height & 3) == 0)) return; diff --git a/Source/PerformanceFish/RoomOptimizations.cs b/Source/PerformanceFish/RoomOptimizations.cs index 8246413..17b6037 100644 --- a/Source/PerformanceFish/RoomOptimizations.cs +++ b/Source/PerformanceFish/RoomOptimizations.cs @@ -349,8 +349,7 @@ public sealed class RoomOptimizations : ClassWithFishPatches, IHasDescription [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Room? RoomAt(IntVec3 c, Map map, RegionType allowedRegionTypes = RegionType.Set_All) { - var cellIndices = map.cellIndices; - if (((uint)c.x >= (uint)cellIndices.mapSizeX) | ((uint)c.z >= (uint)cellIndices.mapSizeZ)) + if (!c.InBounds(map)) return null; var regionAndRoomUpdater = map.regionAndRoomUpdater; @@ -360,8 +359,8 @@ public sealed class RoomOptimizations : ClassWithFishPatches, IHasDescription else if (regionAndRoomUpdater.AnythingToRebuild) LogIncorrectResultWarning(c); - var region = map.regionGrid.regionGrid[(c.z * cellIndices.mapSizeX) + c.x]; - return region is null || !region.valid || (region.type & allowedRegionTypes) == 0 + var region = map.regionGrid.GetValidRegionAt_NoRebuild(c); + return region is null || (region.type & allowedRegionTypes) == 0 ? null : region.District?.Room; } @@ -400,4 +399,4 @@ public sealed class RoomPrepatches : ClassWithFishPrepatches return tmpRegions; } } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/System/ReflectionCaching.cs b/Source/PerformanceFish/System/ReflectionCaching.cs index fe3665d..a725945 100644 --- a/Source/PerformanceFish/System/ReflectionCaching.cs +++ b/Source/PerformanceFish/System/ReflectionCaching.cs @@ -382,6 +382,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches public sealed class GetFullName : FishPatch { + public override bool DefaultState => true; + public override string? Description { get; } = "Normally averages at over 1000ns per call. Caching makes this about 30 times faster, including " + "Harmony overhead. Type.Name is similarly slow too, but unfortunately can't be patched as it's an " @@ -392,54 +394,83 @@ public sealed class ReflectionCaching : ClassWithFishPatches = AccessTools.DeclaredPropertyGetter(typeof(RuntimeType), nameof(RuntimeType.FullName)); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Prefix(RuntimeType __instance, ref string? __result, out bool __state) + public static bool Prefix(RuntimeType __instance, ref string? __result, out FullNameState __state) { - ref var cache = ref TypeFullNameCache.GetOrAddReference(__instance.TypeHandle); + __state = default; + if (_fullNameRecursionDepth != 0) + return true; - if (!cache.Cached && !TryGetFromCentralCache(__instance, ref cache)) - return __state = true; + __state.ReleaseGuard = true; + _fullNameRecursionDepth++; + + var cache = TypeFullNameCache.GetOrAddReference(__instance.TypeHandle); + var fetchedFromCentralCache = false; + if (!cache.Cached) + { + fetchedFromCentralCache = TryGetFromCentralCache(__instance, out cache); + if (!fetchedFromCentralCache) + { + __state.ShouldStore = true; + return true; + } + } + + if (fetchedFromCentralCache) + TypeFullNameCache.GetExistingReference(__instance.TypeHandle) = cache; __result = cache.Name; - return __state = false; + return false; } [MethodImpl(MethodImplOptions.NoInlining)] private static bool TryGetFromCentralCache(RuntimeType instance, - ref TypeFullNameCacheValue cache) + out TypeFullNameCacheValue cache) { lock (_lock) { - ref var centralCache - = ref TypeFullNameCache.GetDirectly.GetOrAddReference(instance.TypeHandle); - cache.Name = centralCache.Name; + cache = TypeFullNameCache.GetDirectly.GetOrAddReference(instance.TypeHandle); + if (!cache.Cached) + return false; } - return cache.Cached; + return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Postfix(RuntimeType __instance, string? __result, bool __state) + public static void Postfix(RuntimeType __instance, string? __result, FullNameState __state) { - if (!__state) + if (!__state.ShouldStore) return; UpdateCache(__instance, __result); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Exception? Finalizer(Exception? __exception, ref FullNameState __state) + { + if (__state.ReleaseGuard) + _fullNameRecursionDepth--; + + return __exception; + } + [MethodImpl(MethodImplOptions.NoInlining)] private static void UpdateCache(RuntimeType __instance, string? __result) { - ref var cache = ref TypeFullNameCache.GetExistingReference(__instance.TypeHandle); - cache.Name = __result; + var cache = new TypeFullNameCacheValue + { + Cached = true, + Name = __result + }; + TypeFullNameCache.GetExistingReference(__instance.TypeHandle) = cache; lock (_lock) - { - ref var centralCache - = ref TypeFullNameCache.GetDirectly.GetReference(__instance.TypeHandle); - centralCache.Name = __result; - } + TypeFullNameCache.GetDirectly.GetOrAddReference(__instance.TypeHandle) = cache; } + [ThreadStatic] + private static int _fullNameRecursionDepth; + private static object _lock = new(); } @@ -474,24 +505,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches public PropertyInfo? Info; } - public record struct TypeFullNameCacheValue() + public record struct FullNameState { - private object? _value = typeof(void); + public bool ReleaseGuard; + public bool ShouldStore; + } - public string? Name - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Unsafe.As(_value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => _value = value; - } - - public bool Cached - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _value is not MemberInfo; - } + public record struct TypeFullNameCacheValue + { + public bool Cached; + public string? Name; } } @@ -499,6 +522,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches { public sealed class GetCustomAttributes : FishPatch { + public override bool DefaultState => true; + public override string? Description { get; } = "Caches attributes for reflection lookups. Decent load time improvement."; @@ -510,58 +535,193 @@ public sealed class ReflectionCaching : ClassWithFishPatches public static bool Prefix(ICustomAttributeProvider obj, Type attributeType, bool inherit, out object[]? __result, out CustomAttributeState __state) { - var key = new CustomAttributeCache(obj, (__state.AttributeType = attributeType).TypeHandle, - __state.Inherit = inherit); + var key = new CustomAttributeCache(obj, attributeType.TypeHandle, inherit); + __state = new() { Key = key }; - ref var cache = ref CustomAttributeCache.GetOrAddReference(in key); - if (cache.Attributes is null) - TryGetFromCentralCache(ref key, ref cache); + var cacheLookup = TryGetCachedAttributes(ref key, out __result); - return __state.State = (__result = cache.Attributes) is null; + if (cacheLookup == CustomAttributeCacheLookup.Hit) + return false; + + if (cacheLookup == CustomAttributeCacheLookup.InProgress) + { + __result = null; + return true; + } + + MarkInProgress(ref key); + __state.ShouldStore = true; + __result = null; + return true; } [MethodImpl(MethodImplOptions.NoInlining)] - private static void TryGetFromCentralCache(ref CustomAttributeCache key, - ref CustomAttributeCacheValue cache) + private static CustomAttributeCacheLookup TryGetCachedAttributes(ref CustomAttributeCache key, + out object[]? result) { + var localLookup = TryUseCache(CustomAttributeCache.Get, ref key, out result); + if (localLookup == CustomAttributeCacheLookup.Hit) + { + result = CopyCachedAttributes(result!); + return CustomAttributeCacheLookup.Hit; + } + + if (localLookup == CustomAttributeCacheLookup.InProgress) + return localLookup; + lock (_lock) - cache.Attributes = CustomAttributeCache.GetDirectly.GetOrAdd(ref key).Attributes; + { + var centralLookup = TryUseCache(CustomAttributeCache.GetDirectly, ref key, out var centralResult); + if (centralLookup == CustomAttributeCacheLookup.Hit) + { + result = CopyCachedAttributes(centralResult!); + CustomAttributeCache.Get[key] = new() + { + Attributes = centralResult, + Status = CustomAttributeCacheStatus.Completed + }; + return CustomAttributeCacheLookup.Hit; + } + + if (centralLookup == CustomAttributeCacheLookup.InProgress) + return CustomAttributeCacheLookup.InProgress; + } + + result = null; + return CustomAttributeCacheLookup.Miss; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Postfix(ICustomAttributeProvider obj, object[]? __result, - in CustomAttributeState __state) + ref CustomAttributeState __state) { - if (!__state.State || __result is null) + if (!__state.ShouldStore || __result is null) return; - UpdateCache(obj, __state, __result); + StoreCompleted(ref __state.Key, __result); } [MethodImpl(MethodImplOptions.NoInlining)] - private static void UpdateCache(ICustomAttributeProvider obj, in CustomAttributeState __state, - object[]? __result) + public static Exception? Finalizer(Exception? __exception, ref CustomAttributeState __state) { - var key = new CustomAttributeCache(obj, __state.AttributeType.TypeHandle, __state.Inherit); - - CustomAttributeCache.GetExistingReference(in key).Attributes = __result; + if (__state.ShouldStore && __exception != null) + ClearInProgress(ref __state.Key); + + return __exception; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static CustomAttributeCacheLookup TryUseCache( + Dictionary cache, + ref CustomAttributeCache key, out object[]? result) + { + if (!cache.TryGetValue(key, out var value)) + { + result = null; + return CustomAttributeCacheLookup.Miss; + } + + switch (value.Status) + { + case CustomAttributeCacheStatus.Completed: + result = value.Attributes; + return CustomAttributeCacheLookup.Hit; + case CustomAttributeCacheStatus.InProgress: + LogInProgress(ref key); + result = null; + return CustomAttributeCacheLookup.InProgress; + default: + result = null; + return CustomAttributeCacheLookup.Miss; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void MarkInProgress(ref CustomAttributeCache key) + { + var value = new CustomAttributeCacheValue + { + Status = CustomAttributeCacheStatus.InProgress + }; lock (_lock) - CustomAttributeCache.GetDirectly.GetReference(ref key).Attributes = __result; + { + CustomAttributeCache.Get[key] = value; + CustomAttributeCache.GetDirectly[key] = value; + } } + [MethodImpl(MethodImplOptions.NoInlining)] + private static void StoreCompleted(ref CustomAttributeCache key, object[] result) + { + var value = new CustomAttributeCacheValue + { + Attributes = CopyCachedAttributes(result), + Status = CustomAttributeCacheStatus.Completed + }; + + lock (_lock) + { + CustomAttributeCache.Get[key] = value; + CustomAttributeCache.GetDirectly[key] = value; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ClearInProgress(ref CustomAttributeCache key) + { + lock (_lock) + { + if (CustomAttributeCache.Get.TryGetValue(key, out var localValue) + && localValue.Status == CustomAttributeCacheStatus.InProgress) + { + CustomAttributeCache.Get.Remove(key); + } + + if (CustomAttributeCache.GetDirectly.TryGetValue(key, out var centralValue) + && centralValue.Status == CustomAttributeCacheStatus.InProgress) + { + CustomAttributeCache.GetDirectly.Remove(key); + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void LogInProgress(ref CustomAttributeCache _) + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object[] CopyCachedAttributes(object[] attributes) + => attributes.Length != 0 ? (object[])attributes.Clone() : Array.Empty(); + private static object _lock = new(); } public record struct CustomAttributeCacheValue { public object[]? Attributes; + public CustomAttributeCacheStatus Status; } public record struct CustomAttributeState { - public bool State, Inherit; - public Type AttributeType; + public bool ShouldStore; + public CustomAttributeCache Key; + } + + public enum CustomAttributeCacheStatus : byte + { + Uninitialized, + InProgress, + Completed + } + + private enum CustomAttributeCacheLookup : byte + { + Miss, + Hit, + InProgress } } @@ -577,8 +737,12 @@ public sealed class ReflectionCaching : ClassWithFishPatches Guard.IsNotNull(array); Guard.IsLessThanOrEqualTo(array.Length, byte.MaxValue); Array = array; - var length = array.Length; - _hashCode = HashCode.Combine(length, length > 0 && Array[0] is { } item ? item.GetHashCode() : 0); + var hashCode = array.Length; + + for (var i = 0; i < array.Length; i++) + hashCode = global::System.HashCode.Combine(hashCode, array[i]); + + _hashCode = hashCode; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -587,13 +751,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches if (Array is null) return other.Array is null; + if (_hashCode != other._hashCode) + return false; + if (other.Array is null || Array.Length != other.Array.Length) return false; var length = Array.Length; for (var i = 0; i < length; i++) { - if (!Array[i].Equals(other.Array![i])) + if (!EqualityComparer.Default.Equals(Array[i], other.Array[i])) return false; } @@ -608,6 +775,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches { public sealed class CreateInstance_Type : FishPatch { + public override bool DefaultState => true; + public override List IncompatibleModIDs { get; } = [PackageIDs.MULTIPLAYER]; public override string? Description { get; } @@ -619,21 +788,94 @@ public sealed class ReflectionCaching : ClassWithFishPatches [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(Type? type, ref object __result) { - if (type is null) + if (type is not RuntimeType runtimeType) return true; - - ref var cache = ref ActivatorCache.GetOrAddReference(type.TypeHandle); + + ref var cache = ref ActivatorCache.GetOrAddReference(runtimeType.TypeHandle); + + if (ReferenceEquals(cache.Result, _unsupportedActivatorSentinel)) + return true; + + if (cache.Result is not null) + return TryInvoke(ref __result, cache.Result); + + if (!CanUseFastPath(runtimeType)) + return MarkUnsupported(runtimeType, ref cache); if (cache.Dirty - && !ActivatorCache.UpdateAsyncCache>(ref cache, - type.TypeHandle, type)) + && !ActivatorCache.UpdateAsyncCache>(ref cache, + runtimeType.TypeHandle, runtimeType)) { return true; } - __result = cache.Result!(); - return false; + try + { + __result = cache.Result!(); + return false; + } + catch (OverflowException) + { + throw; + } + catch (Exception ex) + { + throw new TargetInvocationException(ex); + } } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool MarkUnsupported(RuntimeType type, ref ActivatorCacheValue cache) + { + cache.Result = _unsupportedActivatorSentinel; + + lock (ActivatorCache.SyncLock) + ActivatorCache.GetDirectly.GetOrAddReference(type.TypeHandle).Result = _unsupportedActivatorSentinel; + + return true; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool TryInvoke(ref object __result, Func activator) + { + try + { + __result = activator(); + return false; + } + catch (OverflowException) + { + throw; + } + catch (Exception ex) + { + throw new TargetInvocationException(ex); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanUseFastPath(RuntimeType type) + { + if (type.HasElementType + || type.ContainsGenericParameters + || type.IsInterface + || type.IsAbstract + || type.IsCOMObject + || type == typeof(void) + || type == typeof(TypedReference) + || type == typeof(ArgIterator) + || type == typeof(RuntimeArgumentHandle)) + { + return false; + } + + return type.IsValueType + || type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null) + is not null; + } + + private static readonly Func _unsupportedActivatorSentinel + = static () => throw new InvalidOperationException("Unsupported activator sentinel"); } // public sealed class CreateInstance_Type_Args : FishPatch @@ -642,14 +884,34 @@ public sealed class ReflectionCaching : ClassWithFishPatches // = (Func)Activator.CreateInstance; // } - public record struct ActivatorCacheValue : IAsyncCacheable> + public record struct ActivatorCacheValue : IAsyncCacheable> { public Func? Result { get; set; } public Task>? Task { get; set; } public bool Dirty => Result == null; - public ValueTask?> MakeResultAsync(RuntimeTypeHandle key, Type type) - => new(Reflection.CreateConstructorDelegate>(type)); + public ValueTask?> MakeResultAsync(RuntimeTypeHandle key, RuntimeType type) + => new(MakeActivatorDelegate(type)); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static Func? MakeActivatorDelegate(RuntimeType type) + { + if (type.IsValueType) + return Reflection.CreateConstructorDelegate>(type); + + var constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, + Type.EmptyTypes, null); + + if (constructor is null) + return null; + + var dynamicMethod = new DynamicMethod($"Activator_{type.Name}", typeof(object), Type.EmptyTypes, + type, true); + var il = dynamicMethod.GetILGenerator(); + il.Emit(FishTranspiler.New(constructor)); + il.Emit(FishTranspiler.Return); + return (Func)dynamicMethod.CreateDelegate(typeof(Func)); + } } // public record struct ActivatorWithArgumentsCacheValue : IAsyncCacheable true; + public override string Description { get; } = "Optimizes the GetValue method by invoking it through specialized cached delegates"; @@ -751,12 +1015,17 @@ public sealed class ReflectionCaching : ClassWithFishPatches // Log.Message(types.ToStringSafeEnumerable()); //} - public override MethodBase TargetMethodInfo { get; } - = AccessTools.DeclaredMethod(typeof(MonoField), nameof(MonoField.GetValue))!; + public override MethodBase? TargetMethodInfo { get; } + = AccessTools.DeclaredMethod( + AccessTools.TypeByName("System.Reflection.RuntimeFieldInfo") + ?? AccessTools.TypeByName("System.Reflection.MonoField"), + nameof(FieldInfo.GetValue), [typeof(object)]); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(FieldInfo __instance, object? obj, ref object? __result) { + ValidateGetValueCall(__instance, obj); + ref var cache = ref FieldGetters.GetOrAddReference(__instance.FieldHandle); if (cache.Dirty @@ -769,10 +1038,36 @@ public sealed class ReflectionCaching : ClassWithFishPatches __result = cache.Result!(obj); return false; } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ValidateGetValueCall(FieldInfo info, object? obj) + { + if (!info.IsStatic) + { + if (obj is null) + throw new TargetException("Non-static field requires a target"); + + var declaringType = info.DeclaringType!; + if (!declaringType.IsAssignableFrom(obj.GetType())) + { + throw new ArgumentException( + $"Field {info.Name} defined on type {declaringType} is not a field on the target object which is of type {obj.GetType()}.", + nameof(obj)); + } + } + + if (!info.IsLiteral && info.DeclaringType!.ContainsGenericParameters) + { + throw new InvalidOperationException( + "Late bound operations cannot be performed on fields with types for which Type.ContainsGenericParameters is true."); + } + } } public sealed class SetValue_Patch : FishPatch { + public override bool DefaultState => false; + public override string Description { get; } = "Optimizes the SetValue method by invoking it through specialized cached delegates"; @@ -794,14 +1089,20 @@ public sealed class ReflectionCaching : ClassWithFishPatches [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, + { + if (__instance.IsConst()) + return ErrorForInvalidCallAttempt(__instance, ref cache); + + return FieldSetters.UpdateAsyncCache(ref cache, __instance.FieldHandle, __instance); + } [MethodImpl(MethodImplOptions.NoInlining)] private static bool ErrorForInvalidCallAttempt(FieldInfo info, ref FieldInfoSetterCache cache) { + if (cache.Task == _fieldSetterFailureTask) + return false; + 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 " @@ -811,7 +1112,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches } } - private static Task _fieldSetterFailureTask = new(static () => null!); + private static Task _fieldSetterFailureTask = new(static () => null!); // sentinel only public static FieldGetter? MakeGetterDelegate(FieldInfo info) { @@ -983,14 +1284,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches if (methodInfo is null && constructorInfo is null) return null; - + + var parameters = methodBase.GetParameters(); + if (!CanCacheInvoke(methodBase, methodInfo, constructorInfo, parameters)) + return null; + var dm = new DynamicMethod($"MethodInvoker_{methodBase.Name}", typeof(object), [typeof(object), typeof(object[])], GetOwnerType(methodBase), true); var il = dm.GetILGenerator(); - var parameters = methodBase.GetParameters(); - if (methodInfo != null && !methodBase.IsStatic) EmitInstanceArgument(il, methodBase); @@ -1025,6 +1328,49 @@ public sealed class ReflectionCaching : ClassWithFishPatches } } + private static bool CanCacheInvoke(MethodBase methodBase, MethodInfo? methodInfo, + ConstructorInfo? constructorInfo, ParameterInfo[] parameters) + { + if (methodBase.ContainsGenericParameters + || (methodBase.CallingConvention & CallingConventions.VarArgs) != 0) + { + return false; + } + + if (methodInfo != null && !IsSupportedInvokeType(methodInfo.ReturnType)) + return false; + + if (constructorInfo?.DeclaringType is { } declaringType + && !IsSupportedInvokeType(declaringType)) + { + return false; + } + + for (var i = 0; i < parameters.Length; i++) + { + if (!IsSupportedInvokeParameter(parameters[i].ParameterType)) + return false; + } + + return true; + } + + private static bool IsSupportedInvokeParameter(Type parameterType) + { + // object[] stores nullable value arguments as boxed T or null, not boxed Nullable, so the + // current byref fast path cannot safely hand out a Nullable&. + if (parameterType.IsByRef && parameterType.GetElementType()!.IsNullable()) + return false; + + if (parameterType.IsByRef) + parameterType = parameterType.GetElementType()!; + + return IsSupportedInvokeType(parameterType); + } + + private static bool IsSupportedInvokeType(Type type) + => !type.IsPointer && !type.ContainsGenericParameters; + private static void EmitInvokeMethodCall(MethodBase methodBase, ILGenerator il, MethodInfo methodInfo) { il.Emit(FishTranspiler.Call(methodBase)); @@ -1119,7 +1465,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches [MethodImpl(MethodImplOptions.AggressiveInlining)] [SecuritySafeCritical] - public static ref TTo CastRefSafely(ref object? from) + public static ref TTo CastRefSafely(ref object? from) where TTo : class? => ref from is TTo or null ? ref Unsafe.As(ref from) : ref ThrowInvalidCastException(); @@ -1131,7 +1477,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches ? ref Unsafe.Unbox(from) : ref Unsafe.Unbox(from = from is null - ? default + ? default(TTo) : FisheryLib.Convert.Type(from)); [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1181,4 +1527,4 @@ public sealed class ReflectionCaching : ClassWithFishPatches // populated with dynamic methods by MethodBasePatches.MakeInvokeDelegate, MakeGetterDelegate and // MakeSetterDelegate } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/Utility/CellIndexExtensions.cs b/Source/PerformanceFish/Utility/CellIndexExtensions.cs index 517b656..185ebe5 100644 --- a/Source/PerformanceFish/Utility/CellIndexExtensions.cs +++ b/Source/PerformanceFish/Utility/CellIndexExtensions.cs @@ -12,7 +12,7 @@ public static class CellIndexExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CellToIndex(this in IntVec3 cell, CellIndices cellIndices) - => cell.CellToIndex(cellIndices.mapSizeX); + => cell.CellToIndex(GetMapSizeX(cellIndices)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CellToIndex(this in IntVec3 cell, int mapSizeX) => (cell.z * mapSizeX) + cell.x; @@ -22,7 +22,7 @@ public static class CellIndexExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CellToIndex(this IntVec2 cell, CellIndices cellIndices) - => cell.CellToIndex(cellIndices.mapSizeX); + => cell.CellToIndex(GetMapSizeX(cellIndices)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CellToIndex(this IntVec2 cell, int mapSizeX) => (cell.z * mapSizeX) + cell.x; @@ -31,7 +31,7 @@ public static class CellIndexExtensions 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); + public static int CellToIndex(int x, int z, CellIndices cellIndices) => CellToIndex(x, z, GetMapSizeX(cellIndices)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CellToIndex(int x, int z, int mapSizeX) => (z * mapSizeX) + x; @@ -41,11 +41,24 @@ public static class CellIndexExtensions => thingGrid.thingGrid[cellIndex.Value]; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsFogged(this FogGrid fogGrid, CellIndex cellIndex) => fogGrid.fogGrid[cellIndex.Value]; + public static bool IsFogged(this FogGrid fogGrid, CellIndex cellIndex) +#if V1_6 + => fogGrid.fogGrid.IsSet(cellIndex.Value); +#else + => fogGrid.fogGrid[cellIndex.Value]; +#endif [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float GetDepth(this SnowGrid snowGrid, CellIndex cellIndex) => snowGrid.depthGrid[cellIndex.Value]; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float GetDepth(this SnowGrid snowGrid, int cellIndex) => snowGrid.depthGrid[cellIndex]; -} \ No newline at end of file + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetMapSizeX(CellIndices cellIndices) +#if V1_6 + => cellIndices.SizeX; +#else + => cellIndices.mapSizeX; +#endif +} diff --git a/Source/PerformanceFish/Utility/MiscExtensions.cs b/Source/PerformanceFish/Utility/MiscExtensions.cs index 1a8add6..b2d6e13 100644 --- a/Source/PerformanceFish/Utility/MiscExtensions.cs +++ b/Source/PerformanceFish/Utility/MiscExtensions.cs @@ -97,11 +97,14 @@ public static class MiscExtensions } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int SizeX(this Map map) => map.cellIndices.mapSizeX; + public static int SizeX(this Map map) => map.Size.x; public static Map? TryGetMapHeld(this Thing thing) { - var maps = Current.Game.Maps; + var maps = Find.Maps; + if (maps == null) + return thing.ParentHolder is { } rootHolder ? ThingOwnerUtility.GetRootMap(rootHolder) : null; + var mapIndex = (uint)thing.mapIndexOrState; return mapIndex < (uint)maps.Count @@ -114,7 +117,10 @@ public static class MiscExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Map? TryGetMap(this Thing thing) { - var maps = Current.Game.Maps; + var maps = Find.Maps; + if (maps == null) + return null; + var mapIndex = (uint)thing.mapIndexOrState; return mapIndex < (uint)maps.Count ? maps[(int)mapIndex] : null; @@ -127,17 +133,36 @@ public static class MiscExtensions 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 bool IsSpawned(this Thing thing) + { + var maps = Find.Maps; + if (thing.mapIndexOrState < 0 || maps == null) + return false; + + if (thing.mapIndexOrState < maps.Count) + return true; + + Verse.Log.ErrorOnce($"Thing {thing.ThingID} is associated with invalid map index {thing.mapIndexOrState}", 64664487); + return false; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetLoadID(this Bill bill) => bill.loadID; public static void InvokeWhenCellIndicesReady(this Map map, Action action) { +#if V1_6 + if (map.cellIndices.NumGridCells > 0) +#else if (map.cellIndices != null) +#endif + { action(map); + } else + { map.Events().ComponentsConstructed += action; + } } public static void AddLetterSilently(this LetterStack letterStack, Letter letter, Color color, bool playSound = false, @@ -164,4 +189,4 @@ public static class MiscExtensions letterStack.letters.Add(letter); letter.Received(); } -} \ No newline at end of file +} diff --git a/Source/PerformanceFish/UtilityPrepatches.cs b/Source/PerformanceFish/UtilityPrepatches.cs index 7387a39..c31bdb4 100644 --- a/Source/PerformanceFish/UtilityPrepatches.cs +++ b/Source/PerformanceFish/UtilityPrepatches.cs @@ -26,6 +26,9 @@ public sealed class UtilityPrepatches : ClassWithFishPrepatches public override MethodBase TargetMethodBase { get; } = methodof(LoadedModManager.CreateModClasses); + public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) + => ilProcessor.ReplaceBodyWith(ReplacementBody); + public static void Prefix() { try @@ -37,6 +40,59 @@ public sealed class UtilityPrepatches : ClassWithFishPrepatches Log.Error($"Exception caught within OnAssembliesLoaded:\n{e}\n{new StackTrace(true)}"); } } + + public static void ReplacementBody() + { + var runningModClasses = RunningModClasses; + var assemblyToModContentPack = BuildAssemblyToModContentPackMap(); + + foreach (var type in typeof(Mod).InstantiableDescendantsAndSelf()) + { + if (type is null) + continue; + + DeepProfiler.Start("Loading " + type?.ToString() + " mod class"); + try + { + if (!runningModClasses.ContainsKey(type)) + { + assemblyToModContentPack.TryGetValue(type.Assembly, out var modContentPack); + runningModClasses[type] = (Mod)Activator.CreateInstance(type, modContentPack); + } + } + catch (Exception ex) + { + Log.Error("Error while instantiating a mod of type " + type?.ToString() + ": " + ex); + } + finally + { + DeepProfiler.End(); + } + } + } + + private static Dictionary BuildAssemblyToModContentPackMap() + { + var runningMods = LoadedModManager.RunningModsListForReading; + var assemblyToModContentPack = new Dictionary(); + + for (var i = 0; i < runningMods.Count; i++) + { + var modContentPack = runningMods[i]; + var loadedAssemblies = modContentPack.assemblies.loadedAssemblies; + + for (var j = 0; j < loadedAssemblies.Count; j++) + assemblyToModContentPack.TryAdd(loadedAssemblies[j], modContentPack); + } + + return assemblyToModContentPack; + } + + private static Dictionary RunningModClasses + => (Dictionary)(_runningModClassesField ??= + AccessTools.Field(typeof(LoadedModManager), "runningModClasses")).GetValue(null)!; + + private static FieldInfo? _runningModClassesField; } public sealed class NoSteamLogWarning : FishPrepatch @@ -93,4 +149,4 @@ public sealed class UtilityPrepatches : ClassWithFishPrepatches instruction.Operand = null; } } -} \ No newline at end of file +}