Compare commits

...

4 Commits

17 changed files with 336 additions and 81 deletions
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -7,9 +7,9 @@
<li>1.5</li>
<li>1.6</li>
</supportedVersions>
<author>bradson</author>
<author>bradson, localavaster, Lionel Colaso, Riggs0</author>
<modIconPath IgnoreIfNoMatchingField="True">PerformanceFish/ModIcon</modIconPath>
<url>https://github.com/bbradson/performance-fish</url>
<url>hhttps://git.hudsonriggs.systems/HRiggs/Performance-Fish</url>
<loadBefore>
<li>Dubwise.DubsPerformanceAnalyzer</li>
<li>Dubwise.DubsPerformanceAnalyzerButchered</li>
@@ -46,7 +46,7 @@
<packageId>bs.fishery</packageId>
<displayName>Fishery</displayName>
<steamWorkshopUrl>n/a</steamWorkshopUrl>
<downloadUrl>https://github.com/bbradson/Fishery</downloadUrl>
<downloadUrl>https://git.hudsonriggs.systems/HRiggs/Fishery</downloadUrl>
</li>
</modDependencies>
<incompatibleWith>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
+7 -1
View File
@@ -3,6 +3,7 @@
<PropertyGroup>
<OutDir>..\..\1.6\Assemblies\</OutDir>
<Copyright>Copyright (c) 2026 Bradson</Copyright>
<PolySharpUseEmbeddedAttributeForGeneratedTypes>false</PolySharpUseEmbeddedAttributeForGeneratedTypes>
<FisheryRepoRoot Condition="Exists('$(MSBuildThisFileDirectory)..\..\..\Fishery\1.6\Assemblies\1Fishery.dll')">$(MSBuildThisFileDirectory)..\..\..\Fishery</FisheryRepoRoot>
<FisheryRepoRoot Condition="'$(FisheryRepoRoot)' == '' and Exists('$(MSBuildThisFileDirectory)..\..\..\Fishery-clean\1.6\Assemblies\1Fishery.dll')">$(MSBuildThisFileDirectory)..\..\..\Fishery-clean</FisheryRepoRoot>
</PropertyGroup>
@@ -13,8 +14,13 @@
<DefineConstants>V1_5;V1_6</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Krafs.Rimworld.Ref" Version="1.6.*" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net48" Version="1.0.3" PrivateAssets="all" />
<PackageReference Include="Krafs.Rimworld.Ref" Version="1.6.4871" />
<PackageReference Include="Lib.Harmony" Version="2.3.3" ExcludeAssets="runtime" />
<PackageReference Update="Zetrith.Prepatcher" Version="1.2.0" />
<PackageReference Update="PolySharp" Version="1.16.0" />
<PackageReference Update="Krafs.Publicizer" Version="2.3.2" />
<PackageReference Update="JetBrains.Annotations" Version="2023.3.0" />
<Reference Include="1Fishery">
<HintPath>$(FisheryRepoRoot)\1.6\Assemblies\1Fishery.dll</HintPath>
<Private>False</Private>
@@ -0,0 +1,39 @@
// Copyright (c) 2026
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
#if V1_6
using Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
namespace PerformanceFish;
public sealed class AbilityTrackerTickOptimization : ClassWithFishPrepatches
{
public sealed class AbilitiesTickPatch : FishPrepatch
{
public override string? Description { get; }
= "Reads the pawn ability list once per ability tick instead of twice, while still observing list changes between abilities.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(Pawn_AbilityTracker), nameof(Pawn_AbilityTracker.AbilitiesTick));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(AbilitiesTick);
public static void AbilitiesTick(Pawn_AbilityTracker instance)
{
for (var i = 0;; i++)
{
var abilities = instance.AllAbilitiesForReading;
if (i >= abilities.Count)
return;
abilities[i].AbilityTick();
}
}
}
}
#endif
+12 -6
View File
@@ -59,29 +59,35 @@ public static class Utility
return;
_gcInProgress = true;
try
{
if (_cachesToProcessForGC is null || _currentGCProgress >= _cachesToProcessForGC.Length)
{
_cachesToProcessForGC = All.ToArray();
_gcRate = GenDate.TicksPerTwelfth / _cachesToProcessForGC.CountNonEmpty();
var nonEmptyCount = _cachesToProcessForGC.CountNonEmpty();
_gcRate = nonEmptyCount > 0 ? GenDate.TicksPerTwelfth / nonEmptyCount : 1000;
_currentGCProgress = 0;
_destroyedThingCounter = 0;
if (nonEmptyCount == 0)
return;
}
while (_cachesToProcessForGC[_currentGCProgress].Count == 0
&& _cachesToProcessForGC.Length - _currentGCProgress > 1)
while (_currentGCProgress < _cachesToProcessForGC.Length
&& _cachesToProcessForGC[_currentGCProgress].Count == 0)
{
_currentGCProgress++;
}
if (_currentGCProgress < _cachesToProcessForGC.Length)
_cachesToProcessForGC[_currentGCProgress++].Clear();
}
finally
{
_lastGCTick = TickHelper.TicksGame;
_gcInProgress = false;
}
}
}
private static int CountNonEmpty(this ICollection[] collection)
{
+81 -2
View File
@@ -11,6 +11,15 @@ namespace PerformanceFish;
public sealed class CompTickOptimization : ClassWithFishPrepatches
{
#if V1_6
public struct ThingTickCache
{
public List<ThingComp>? Source;
public int Version;
public bool AllTrivial;
}
#endif
private static readonly MethodInfo _thingCompTickMethod
= AccessTools.DeclaredMethod(typeof(ThingComp), nameof(ThingComp.CompTick));
@@ -39,8 +48,12 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
private static readonly bool _hediffCompPostTickIntervalBaseIsTrivial
= IsTrivialVoidMethod(_hediffCompPostTickIntervalMethod);
#if V1_6
private static int _canSkipInheritedBaseThingCompTickState;
#else
private static readonly Lazy<bool> _canSkipInheritedBaseThingCompTick
= new(static () => _thingCompTickBaseIsTrivial && !HasHarmonyPatches(_thingCompTickMethod));
#endif
private static readonly Lazy<bool> _canSkipInheritedBaseThingCompTickInterval
= new(static () => _thingCompTickIntervalBaseIsTrivial && !HasHarmonyPatches(_thingCompTickIntervalMethod));
@@ -89,10 +102,19 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
public sealed class ThingTickPatch : FishPrepatch
{
public override bool DefaultState => false;
public override bool DefaultState =>
#if V1_6
true;
#else
false;
#endif
public override string? Description { get; }
#if V1_6
= "Skips the component tick loop when every component inherits the unpatched, empty base CompTick method.";
#else
= "Skips inherited empty ThingComp.CompTick calls when the base method is still unpatched and trivial.";
#endif
public override MethodBase TargetMethodBase { get; }
= AccessTools.DeclaredMethod(typeof(ThingWithComps), "Tick");
@@ -102,16 +124,61 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
public static void ReplacementBody(ThingWithComps instance)
{
#if V1_6
var comps = instance.comps;
if (comps is null)
return;
#else
var comps = instance.AllComps;
#endif
var count = comps.Count;
#if V1_6
// Small lists cost less to dispatch normally than to validate a cache.
if (count >= 4 && CanSkipInheritedBaseThingCompTick())
{
ref var cache = ref instance.ThingTickCache();
if (!ReferenceEquals(cache.Source, comps) || cache.Version != comps._version)
UpdateThingTickCache(comps, ref cache);
if (cache.AllTrivial)
return;
}
#else
var canSkipInheritedBase = CanSkipInheritedBaseThingCompTick();
#endif
for (var i = 0; i < count; i++)
{
#if V1_6
instance.comps![i].CompTick();
#else
var comp = comps[i];
if (!canSkipInheritedBase || !_thingCompTickTypes.GetOrAdd(comp.GetType()))
comp.CompTick();
#endif
}
}
#if V1_6
[MethodImpl(MethodImplOptions.NoInlining)]
private static void UpdateThingTickCache(List<ThingComp> comps, ref ThingTickCache cache)
{
var allTrivial = true;
for (var i = 0; i < comps.Count; i++)
{
var comp = comps[i];
if (comp is null || !_thingCompTickTypes.GetOrAdd(comp.GetType()))
{
allTrivial = false;
break;
}
}
cache.Source = comps;
cache.Version = comps._version;
cache.AllTrivial = allTrivial;
}
#endif
}
public sealed class ThingTickIntervalPatch : FishPrepatch
@@ -267,7 +334,19 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanSkipInheritedBaseThingCompTick()
=> _canSkipInheritedBaseThingCompTick.Value;
{
#if V1_6
var state = _canSkipInheritedBaseThingCompTickState;
if (state == 0)
{
state = _thingCompTickBaseIsTrivial && !HasHarmonyPatches(_thingCompTickMethod) ? 2 : 1;
_canSkipInheritedBaseThingCompTickState = state;
}
return state == 2;
#else
return _canSkipInheritedBaseThingCompTick.Value;
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanSkipInheritedBaseThingCompTickInterval()
@@ -61,7 +61,15 @@ public sealed class GeneTrackerOptimization : ClassWithFishPrepatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Dirty(Pawn_GeneTracker instance, List<Gene> genesToTick)
#if V1_6
{
ref var versions = ref instance.GeneListVersions();
return !versions.Initialized || versions.Xenogenes != instance.xenogenes._version
|| versions.Endogenes != instance.endogenes._version;
}
#else
=> GetListVersion(instance) != genesToTick._version;
#endif
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Update(Pawn_GeneTracker instance, List<Gene> genesToTick)
@@ -71,12 +79,21 @@ public sealed class GeneTrackerOptimization : ClassWithFishPrepatches
AddTickableGenes(genesToTick, instance.xenogenes);
AddTickableGenes(genesToTick, instance.endogenes);
#if V1_6
ref var versions = ref instance.GeneListVersions();
versions.Xenogenes = instance.xenogenes._version;
versions.Endogenes = instance.endogenes._version;
versions.Initialized = true;
#else
genesToTick._version = GetListVersion(instance);
#endif
}
#if !V1_6
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetListVersion(Pawn_GeneTracker instance)
=> (instance.xenogenes._version << 16) | (instance.endogenes._version & 0b1111_1111_1111_1111);
#endif
private static void AddTickableGenes(List<Gene> genesToTick, List<Gene> genes)
{
@@ -115,6 +132,15 @@ public sealed class GeneTrackerOptimization : ClassWithFishPrepatches
}
}
#if V1_6
public struct GeneListVersionState
{
public int Xenogenes;
public int Endogenes;
public bool Initialized;
}
#endif
public sealed class GeneTrackerTickIntervalPatch : FishPrepatch
{
// i am iffy about this
+36 -9
View File
@@ -53,11 +53,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : ThingComp
{
private List<ThingComp>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<ThingComp> comps) => comps._version != _listVersion;
public bool IsDirty(List<ThingComp> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(ThingWithComps thing, T? comp)
@@ -65,6 +67,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (thing.thingIDNumber < 0)
return;
_listReference = thing.comps;
_listVersion = thing.comps._version;
Comp = comp;
}
@@ -113,11 +116,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : CompProperties
{
private List<CompProperties>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<CompProperties> comps) => comps._version != _listVersion;
public bool IsDirty(List<CompProperties> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(ThingDef thingDef, T? comp)
@@ -125,6 +130,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (thingDef.shortHash == default)
return;
_listReference = thingDef.comps;
_listVersion = thingDef.comps._version;
Comp = comp;
}
@@ -174,11 +180,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue()
{
private List<CompProperties>? _listReference;
private int _listVersion = -2;
public bool HasComp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<CompProperties> comps) => comps._version != _listVersion;
public bool IsDirty(List<CompProperties> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(ThingDef thingDef, bool hasComp)
@@ -186,6 +194,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (thingDef.shortHash == default)
return;
_listReference = thingDef.comps;
_listVersion = thingDef.comps._version;
HasComp = hasComp;
}
@@ -236,11 +245,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : ThingComp
{
private List<CompProperties>? _listReference;
private int _listVersion = -2;
public bool HasComp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<CompProperties> comps) => comps._version != _listVersion;
public bool IsDirty(List<CompProperties> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(ThingDef thingDef, bool hasComp)
@@ -248,6 +259,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (thingDef.shortHash == default)
return;
_listReference = thingDef.comps;
_listVersion = thingDef.comps._version;
HasComp = hasComp;
}
@@ -296,11 +308,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : HediffComp
{
private List<HediffComp>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<HediffComp> comps) => comps._version != _listVersion;
public bool IsDirty(List<HediffComp> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(HediffWithComps hediff, T? comp)
@@ -308,6 +322,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (hediff.loadID < 0)
return;
_listReference = hediff.comps;
_listVersion = hediff.comps._version;
Comp = comp;
}
@@ -355,11 +370,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : HediffCompProperties
{
private List<HediffCompProperties>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<HediffCompProperties> comps) => comps._version != _listVersion;
public bool IsDirty(List<HediffCompProperties> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(HediffDef hediffDef, T? comp)
@@ -367,6 +384,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (hediffDef.shortHash == 0)
return;
_listReference = hediffDef.comps;
_listVersion = hediffDef.comps._version;
Comp = comp;
}
@@ -414,11 +432,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : AbilityComp
{
private List<AbilityComp>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<AbilityComp> comps) => comps._version != _listVersion;
public bool IsDirty(List<AbilityComp> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(Ability ability, T? comp)
@@ -426,6 +446,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (ability.Id < 0)
return;
_listReference = ability.comps;
_listVersion = ability.comps._version;
Comp = comp;
}
@@ -468,11 +489,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : WorldObjectComp
{
private List<WorldObjectComp>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<WorldObjectComp> comps) => comps._version != _listVersion;
public bool IsDirty(List<WorldObjectComp> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(WorldObject worldObject, T? comp)
@@ -480,6 +503,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (worldObject.ID < 0)
return;
_listReference = worldObject.comps;
_listVersion = worldObject.comps._version;
Comp = comp;
}
@@ -522,11 +546,13 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
public record struct CacheValue<T>() where T : MapComponent
{
private List<MapComponent>? _listReference;
private int _listVersion = -2;
public T? Comp;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<MapComponent> comps) => comps._version != _listVersion;
public bool IsDirty(List<MapComponent> comps)
=> !ReferenceEquals(comps, _listReference) || comps._version != _listVersion;
[MethodImpl(MethodImplOptions.NoInlining)]
public void Update(Map map, T? comp)
@@ -534,6 +560,7 @@ public sealed class GetCompCaching : ClassWithFishPrepatches
if (map.uniqueID < 0)
return;
_listReference = map.components;
_listVersion = map.components._version;
Comp = comp;
}
@@ -16,6 +16,8 @@ public sealed class NeedsTrackerCaching : ClassWithFishPrepatches
{
public sealed class TryGetNeedPatch : FishPrepatch
{
public override bool DefaultState => true;
public override string? Description { get; }
= "Caches generic need lookups behind the needs list version.";
@@ -30,6 +32,8 @@ public sealed class NeedsTrackerCaching : ClassWithFishPrepatches
public sealed class TryGetNeedOutPatch : FishPrepatch
{
public override bool DefaultState => true;
public override string? Description { get; }
= "Caches generic need lookups behind the needs list version.";
@@ -55,18 +59,18 @@ public sealed class NeedsTrackerCaching : ClassWithFishPrepatches
return cache.Found;
}
return UpdateCache(instance, typeof(T), out need, ref cache);
return UpdateCache(instance, out need, ref cache);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool UpdateCache<T>(Pawn_NeedsTracker instance, Type needType, out T? need, ref CacheValue cache)
private static bool UpdateCache<T>(Pawn_NeedsTracker instance, 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)
if (candidate.GetType() != typeof(T))
continue;
need = (T)candidate;
+2 -5
View File
@@ -39,7 +39,7 @@ public static class OnAssembliesLoaded
PerformanceFishMod.AllPatchClasses = PerformanceFishMod.InitializeAllPatchClasses<IHasFishPatch>();
PerformanceFishMod.AllPrepatchClasses ??= PerformanceFishMod.InitializeAllPatchClasses<ClassWithFishPrepatches>();
TryInitialize(AnalyzerFixes.Patch, true);
TryInitialize(AnalyzerFixes.Patch);
_ = FishSettings.Instance;
@@ -109,13 +109,10 @@ public static class OnAssembliesLoaded
}
}
private static async void TryInitialize(Action action, bool async = false)
private static void TryInitialize(Action action)
{
try
{
if (async)
await Task.Run(action).ConfigureAwait(false);
else
action();
}
catch (Exception ex)
+10 -5
View File
@@ -90,6 +90,7 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
public List<MethodInfo> BenchmarkHarmonyMethodInfos { get; } = [];
private bool Patched { get; set; }
private bool ShouldBePatched { get; set; }
private readonly HashSet<MethodBase> _patchedTargets = [];
public virtual string? Name => null;
@@ -190,15 +191,16 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
protected virtual void ApplyPatch()
{
Patched = true;
foreach (var method in TargetMethodInfos)
var targets = TargetMethodInfos.ToArray();
foreach (var method in targets)
{
if (_patchedTargets.Contains(method))
continue;
try
{
if (IsBenchmarking)
{
Patched = false;
if (AccessTools.EnumeratorMoveNext(method) is { } moveNext)
{
BenchmarkHarmonyMethodInfos.Add(PerformanceFishMod.Harmony!.Patch(moveNext,
@@ -229,6 +231,7 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
transpiler: TryMakeHarmonyMethod(TranspilerMethodInfo, TranspilerMethodPriority),
finalizer: TryMakeHarmonyMethod(FinalizerMethodInfo, FinalizerMethodPriority)));
}
_patchedTargets.Add(method);
}
}
@@ -240,6 +243,7 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
FinalizerMethodInfo.FullDescription()}:\n{e}");
}
}
Patched = !IsBenchmarking && targets.All(_patchedTargets.Contains);
}
private static HarmonyMethod? TryMakeHarmonyMethod(MethodInfo? method, int priority = -1, string[]? before = null,
@@ -249,7 +253,7 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
public virtual void TryUnpatch()
{
ShouldBePatched = false;
if ( /*!Enabled &&*/ Patched)
if (Patched || _patchedTargets.Count != 0)
ApplyUnpatching();
}
@@ -269,6 +273,7 @@ public abstract class FishPatch : SingletonFactory<FishPatch>, IExposable, IHasD
});
}
}
_patchedTargets.Clear();
}
private static void TryUnpatchMethod(MethodBase targetMethod, MethodInfo? patchMethod)
@@ -15,16 +15,21 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
{
public sealed class WorldObjectsHolderTickPatch : FishPrepatch
{
public override bool DefaultState => true;
private static FishTable<Type, bool> _skippableCompTypes = null!;
private static FishTable<Type, bool> _skippableWorldObjectTypes = null!;
private static readonly List<IThingHolder> _tmpThingHolders = [];
private static readonly List<SkippedWorldObject> _skippedWorldObjects = [];
private static List<WorldObject>? _cachedWorldObjectsReference;
private static List<Map>? _cachedMapsReference;
private readonly record struct SkippedWorldObject(WorldObject WorldObject,
List<WorldObjectComp> Comps, int CompsVersion, bool HasEnterCooldownComp);
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 "
+ "everything equally, including many static objects that cannot possibly affect anything from a tick. "
+ "This patch improves the world objects holder to determine objects that need ticking in advance, cache "
+ "the list of them, and only tick those, skipping all the others.";
= "Caches the world objects that need ticking and skips objects with no active tick behavior. "
+ "Objects with changing contents or components are rechecked before each tick.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.Method(typeof(WorldObjectsHolder), nameof(WorldObjectsHolder.WorldObjectsHolderTick));
@@ -38,7 +43,7 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
UpdateCache(instance);
var worldObjects = WorldObjectsHolder.tmpWorldObjects;
for (var i = worldObjects.Count; i-- > 0;)
for (var i = 0; i < worldObjects.Count; i++)
#if V1_6
worldObjects[i].DoTick();
#else
@@ -50,18 +55,23 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
{
var staticWorldObjects = WorldObjectsHolder.tmpWorldObjects;
staticWorldObjects.Clear();
_skippedWorldObjects.Clear();
var instanceWorldObjects = instance.worldObjects;
for (var i = instanceWorldObjects.Count; i-- > 0;)
for (var i = 0; i < instanceWorldObjects.Count; i++)
{
var worldObject = instanceWorldObjects[i];
if (worldObject is not MapParent { HasMap: true }
&& CanSkipWorldObjectTick(worldObject)
&& (worldObject is not Settlement settlement || settlement.trader?.stock is null)
&& CanSkipCompTick(worldObject))
{
var comps = worldObject.comps;
var hasEnterCooldownComp = false;
for (var j = 0; j < comps.Count; j++)
hasEnterCooldownComp |= comps[j] is EnterCooldownComp;
_skippedWorldObjects.Add(new(worldObject, comps, comps._version, hasEnterCooldownComp));
continue;
}
@@ -70,6 +80,8 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
CachedWorldObjectsVersion = instanceWorldObjects._version;
CachedMapsVersion = Current.gameInt.maps._version;
_cachedWorldObjectsReference = instanceWorldObjects;
_cachedMapsReference = Current.gameInt.maps;
}
private static bool CanSkipWorldObjectTick(WorldObject worldObject)
@@ -79,32 +91,31 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
private static bool HasTickableContents(WorldObject worldObject)
{
if (worldObject is not IThingHolder holder)
if (worldObject is not IThingHolder holder
|| worldObject is IThingHolderTickable { ShouldTickContents: false })
return false;
lock (_tmpThingHolders)
{
_tmpThingHolders.Clear();
try
{
_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();
var heldThings = _tmpThingHolders[i].GetDirectlyHeldThings();
if (heldThings is not null && heldThings.Owner is not Map and not Caravan)
return true;
}
}
_tmpThingHolders.Clear();
return false;
}
finally
{
_tmpThingHolders.Clear();
}
}
}
private static bool CanSkipCompTick(WorldObject worldObject)
@@ -137,10 +148,9 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
],
_whitelistedWorldObjectTypes =
[
// Settlement.TickInterval checks trader and defeat state even without a map.
typeof(WorldObject),
typeof(MapParent),
typeof(Settlement),
ModCompatibility.Types.RealRuins.POIWorldObject
typeof(MapParent)
];
public static void AddCompToWhiteList(Type compType)
@@ -211,8 +221,31 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
}
public static bool CacheDirty(WorldObjectsHolder instance)
=> CachedWorldObjectsVersion != instance.worldObjects._version
|| CachedMapsVersion != Current.gameInt.maps._version;
{
if (!ReferenceEquals(_cachedWorldObjectsReference, instance.worldObjects)
|| CachedWorldObjectsVersion != instance.worldObjects._version
|| !ReferenceEquals(_cachedMapsReference, Current.gameInt.maps)
|| CachedMapsVersion != Current.gameInt.maps._version)
return true;
for (var i = 0; i < _skippedWorldObjects.Count; i++)
{
var skipped = _skippedWorldObjects[i];
var worldObject = skipped.WorldObject;
var comps = worldObject.comps;
if (!ReferenceEquals(comps, skipped.Comps) || comps._version != skipped.CompsVersion
|| worldObject is MapParent { HasMap: true }
|| HasTickableContents(worldObject))
return true;
if (skipped.HasEnterCooldownComp)
for (var j = 0; j < comps.Count; j++)
if (comps[j] is EnterCooldownComp { Active: true })
return true;
}
return false;
}
public static void SetDirty() => CachedWorldObjectsVersion = CachedMapsVersion = -2;
@@ -35,6 +35,16 @@ public static class PrepatcherFields
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static extern List<Gene> GenesToTick(this Pawn_GeneTracker geneTracker);
#if V1_6
[PrepatcherField]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static extern ref GeneTrackerOptimization.GeneListVersionState GeneListVersions(this Pawn_GeneTracker geneTracker);
[PrepatcherField]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static extern ref CompTickOptimization.ThingTickCache ThingTickCache(this ThingWithComps thing);
#endif
[PrepatcherField]
[ValueInitializer(nameof(CreateWorldPawnsCache))]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -15,6 +15,8 @@ public sealed class PawnRenderingOptimizations : ClassWithFishPrepatches
public sealed class IsHiddenFromPlayerPatch : FishPrepatch
{
public override bool DefaultState => true;
// 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; }
@@ -53,6 +55,8 @@ public sealed class PawnRenderingOptimizations : ClassWithFishPrepatches
public sealed class PawnRenderTreeComputeMatrixPatch : FishPrepatch
{
public override bool DefaultState => true;
public override string? Description { get; }
= "Avoids Matrix4x4 inverse work when undoing a pivot translation in PawnRenderTree.ComputeMatrix.";
@@ -85,6 +89,8 @@ public sealed class PawnRenderingOptimizations : ClassWithFishPrepatches
public sealed class DamageFlasherCurColorPatch : FishPrepatch
{
public override bool DefaultState => true;
private static readonly AccessTools.FieldRef<DamageFlasher, int> _lastDamageTick
= AccessTools.FieldRefAccess<DamageFlasher, int>("lastDamageTick");
@@ -110,6 +116,8 @@ public sealed class PawnRenderingOptimizations : ClassWithFishPrepatches
public sealed class DamageFlasherGetDamagedMatPatch : FishPrepatch
{
public override bool DefaultState => true;
private static readonly AccessTools.FieldRef<DamageFlasher, int> _lastDamageTick
= AccessTools.FieldRefAccess<DamageFlasher, int>("lastDamageTick");
@@ -535,6 +535,13 @@ public sealed class ReflectionCaching : ClassWithFishPatches
public static bool Prefix(ICustomAttributeProvider obj, Type attributeType, bool inherit,
out object[]? __result, out CustomAttributeState __state)
{
if (obj is null || attributeType is null)
{
__state = default;
__result = null;
return true;
}
var key = new CustomAttributeCache(obj, attributeType.TypeHandle, inherit);
__state = new() { Key = key };
@@ -656,7 +663,9 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
var value = new CustomAttributeCacheValue
{
Attributes = CopyCachedAttributes(result),
Attributes = result.Length == 0
? (object[])Array.CreateInstance(Type.GetTypeFromHandle(key.Second), 0)
: CopyCachedAttributes(result),
Status = CustomAttributeCacheStatus.Completed
};
@@ -693,7 +702,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object[] CopyCachedAttributes(object[] attributes)
=> attributes.Length != 0 ? (object[])attributes.Clone() : Array.Empty<object>();
=> attributes.Length != 0 ? (object[])attributes.Clone() : attributes;
private static object _lock = new();
}
@@ -1024,8 +1033,6 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[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
@@ -1035,6 +1042,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches
return true;
}
// Unsupported fields fall back to Mono's implementation, including its exception behavior.
ValidateGetValueCall(__instance, obj);
__result = cache.Result!(obj);
return false;
}