Lots of new optimizations utilizing prepatcher

To start, this mod now requires Prepatcher and Fishery.
New optimizations include: gene ticking improvements, world object improvements, hediff optimizations, world pawn gc optimizations, better mothballing, dds support, ContentFinder caching, additions and changes to reflection caching, as well as various other additions. Multiplayer support has been improved too, but should be considered highly experimental. The WorkGiver_DoBill bug is fixed in this build. Multiple patches aiming to improve RimWorld's load time have been added too.
This commit is contained in:
bbradson
2023-06-03 20:24:38 +02:00
parent cb0a407425
commit ca9321add8
125 changed files with 11272 additions and 3681 deletions
@@ -0,0 +1,106 @@
// 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;
using RimWorld.Planet;
namespace PerformanceFish.Planet;
public class MothballOptimization : ClassWithFishPrepatches
{
public const string BASIC_DESCRIPTION
= "Mothballing means to \"take something out of operation but maintain it so that it can be used in the "
+ "future.\" RimWorld has this system for world pawns along with a number of conditions which can block it "
+ "from taking effect.";
public class WorldPawnsDefPreventingMothball : FishPrepatch
{
public override string? Description { get; }
= BASIC_DESCRIPTION + " This patch essentially serves as a whitelist to allow mothballing a lot more "
+ "pawns. Large performance impact, but not entirely side-effect free. Certain slowly progressing and not "
+ "lethal conditions like addictions or pregnancy do not progress on mothballed pawns.";
public static bool MothballEverything => FishSettings.MothballEverything;
public override MethodBase TargetMethodBase { get; }
= AccessTools.Method(typeof(WorldPawns), nameof(WorldPawns.DefPreventingMothball));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
{
var instructions = ilProcessor.instructions;
for (var i = 0; i < instructions.Count; i++)
{
if (instructions[i].Operand is not MethodReference { Name: "IsPermanent" })
continue;
ilProcessor.InsertAt(i++, OpCodes.Dup);
i++; // bool IsPermanent(hediff)
ilProcessor.InsertRange(i,
OpCodes.Ldarg_0,
OpCodes.Ldarg_1,
(OpCodes.Call, methodof(AllowMothball)));
break;
}
}
public static bool AllowMothball(Hediff hediff, bool isPermanent, WorldPawns worldPawns, Pawn pawn)
=> MothballEverything
|| isPermanent
|| !hediff.Visible
|| (/*IsLowPriority(worldPawns, pawn)
&&*/ AllowedDefs.Contains(hediff.def)
&& hediff is
{
CurStage.lifeThreatening: false, SummaryHealthPercentImpact: < 0.01f, Bleeding: false
}
&& (!HasBlockingComp(hediff) || !HasLifeThreateningStage(hediff.def)));
private static bool HasBlockingComp(Hediff hediff)
=> hediff.def.minSeverity <= 0f
&& hediff is HediffWithComps hediffWithComps
&& hediffWithComps.comps.ExistsAndNotNull(static comp
=> comp is HediffComp_TendDuration
&& comp.props is not HediffCompProperties_TendDuration { showTendQuality: false });
private static bool IsLowPriority(WorldPawns worldPawns, Pawn pawn)
=> WorldPawns.lowPrioritySituations.Contains(worldPawns.GetSituation(pawn));
}
public static HashSet<HediffDef> AllowedDefs => _allowedDefs ??= PrepareAllowedDefs();
[MethodImpl(MethodImplOptions.NoInlining)]
public static HashSet<HediffDef> PrepareAllowedDefs()
=> DefDatabase<HediffDef>.AllDefsListForReading.Where(static def
=> def.lethalSeverity < 0f
// && !def.displayWound
&& !CausesBleeding(def)
&& !LosesSeverityPerDay(def)
&& (!HasLifeThreateningStage(def) || !HasBlockingCompProperties(def)))
.ToHashSet();
private static bool HasBlockingCompProperties(HediffDef def)
=> def.minSeverity <= 0f
&& def.comps.ExistsAndNotNull(static props
=> (props.compClass?.IsAssignableTo(typeof(HediffComp_TendDuration)) ?? false)
&& props is not HediffCompProperties_TendDuration { showTendQuality: false });
private static bool LosesSeverityPerDay(HediffDef def)
=> def.minSeverity <= 0f
&& def.comps.ExistsAndNotNull(static props
=> props is HediffCompProperties_SeverityPerDay severityProps
&& (severityProps.severityPerDay <= -0.35f || severityProps.severityPerDayRange.max <= -0.35f));
private static bool HasLifeThreateningStage(HediffDef def)
=> def.stages.ExistsAndNotNull(static stage => stage.lifeThreatening);
private static bool CausesBleeding(HediffDef def) => def.injuryProps is { bleedRate: > 0.001f };
private static HashSet<HediffDef>? _allowedDefs;
}
@@ -0,0 +1,114 @@
// 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;
using RimWorld.Planet;
namespace PerformanceFish.Planet;
public class WorldObjectsOptimization : ClassWithFishPrepatches
{
public class WorldObjectsHolderTickPatch : FishPrepatch
{
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.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.Method(typeof(WorldObjectsHolder), nameof(WorldObjectsHolder.WorldObjectsHolderTick));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(WorldObjectsHolderTick);
public static void WorldObjectsHolderTick(WorldObjectsHolder instance)
{
if (CacheDirty(instance))
UpdateCache(instance);
var worldObjects = WorldObjectsHolder.tmpWorldObjects;
for (var i = worldObjects.Count; i-- > 0;)
worldObjects[i].Tick();
}
public static void UpdateCache(WorldObjectsHolder instance)
{
var staticWorldObjects = WorldObjectsHolder.tmpWorldObjects;
staticWorldObjects.Clear();
var instanceWorldObjects = instance.worldObjects;
for (var i = instanceWorldObjects.Count; i-- > 0;)
{
var worldObject = instanceWorldObjects[i];
if (worldObject is not MapParent { HasMap: true }
&& SkippableWorldObjects.Contains(worldObject.GetType())
&& (worldObject is not Settlement settlement || settlement.trader?.stock is null)
&& CanSkipCompTick(worldObject))
{
continue;
}
staticWorldObjects.Add(worldObject);
}
CachedWorldObjectsVersion = instanceWorldObjects._version;
CachedMapsVersion = Current.gameInt.maps._version;
}
private static bool CanSkipCompTick(WorldObject worldObject)
{
var comps = worldObject.comps;
for (var i = comps.Count; i-- > 0;)
{
var comp = comps[i];
if (comp is EnterCooldownComp { Active: true })
return false;
if (!SkippableComps.Contains(comp.GetType()))
return false;
comp.CompTick();
}
return true;
}
public static int
CachedWorldObjectsVersion,
CachedMapsVersion;
private static Type[]
_whitelistedTickingCompTypes
=
{
typeof(WorldObjectComp),
typeof(FormCaravanComp),
typeof(TimedDetectionRaids),
typeof(EnterCooldownComp)
},
_whitelistedWorldObjectTypes = { typeof(WorldObject), typeof(MapParent), typeof(Settlement) };
public static HashSet<Type>
SkippableComps = MakeSubclassHashSet(typeof(WorldObjectComp), nameof(WorldObjectComp.CompTick),
_whitelistedTickingCompTypes),
SkippableWorldObjects = MakeSubclassHashSet(typeof(WorldObject), nameof(WorldObject.Tick),
_whitelistedWorldObjectTypes);
private static HashSet<Type> MakeSubclassHashSet(Type type, string name, Type[] allowedDeclaringTypes)
=> type.SubclassesWithNoMethodOverrideAndSelf(allowedDeclaringTypes, name).ToHashSet();
public static bool CacheDirty(WorldObjectsHolder instance)
=> CachedWorldObjectsVersion != instance.worldObjects._version
|| CachedMapsVersion != Current.gameInt.maps._version;
}
}
@@ -0,0 +1,106 @@
// Copyright (c) 2023 bradson
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
using Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
using RimWorld.Planet;
namespace PerformanceFish.Planet;
public class WorldPawnGCOptimization : ClassWithFishPrepatches
{
public class AddRelationshipsPatch : FishPrepatch
{
public override string? Description { get; }
= "The world pawn GC is responsible for deleting no longer relevant pawns. AddAllRelationships here tries "
+ "to find every pawn another has any kind of relation to, no matter how distant and through how many "
+ "other relatives, to block these pawns from getting deleted. This normally runs for every pawn the game "
+ "considers critical, which includes colonists, faction leaders, spawned pawns and many other less "
+ "important pawns, like someone who happened to have come up in someone's battle log, in a tale for art, "
+ "in a quest etc. This patch prevents this relationship calculation for many of the less important pawns.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.Method(typeof(WorldPawnGC), nameof(WorldPawnGC.AddAllRelationships));
public static bool Prefix(WorldPawnGC __instance, Pawn pawn, Dictionary<Pawn, string> keptPawns)
=> AcceptedCriticalPawnReasons.Contains(keptPawns[pawn]);
}
public class WorldPawnGCTickPatch : FishPrepatch
{
public override string? Description { get; }
= "Normally the world pawn GC processes 1 pawn per tick, and speeds up if it fails to complete its "
+ "processing before the game had to add or remove world pawns through other means. This patch changes the "
+ "baseline to one pawn every 4 ticks to make the slowdown less noticeable.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.Method(typeof(WorldPawnGC), nameof(WorldPawnGC.WorldPawnGCTick));
public static bool Prefix() => TickHelper.MatchesModulo(4);
}
public class CancelGCPassPatch : FishPrepatch
{
public override string? Description { get; }
= "When the game determines that it has to speed up its GC process because of world pawn additions or "
+ "removals it normally aborts the process and entirely starts over. This patch makes it not do that, and "
+ "instead just lets it continue at the faster rate.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.Method(typeof(WorldPawnGC), nameof(WorldPawnGC.CancelGCPass));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static void ReplacementBody(WorldPawnGC instance)
{
if (instance.activeGCProcess == null)
return;
// instance.activeGCProcess = null;
instance.currentGCRate = Mathf.Min(instance.currentGCRate * 2, 16777216);
if (DebugViewSettings.logWorldPawnGC)
Log.Message("World pawn GC cancelled");
}
}
public class GetCriticalPawnReasonPatch : FishPrepatch
{
public override string? Description { get; }
= "Critical pawns are pawns the game's world pawn GC does not delete. Relationships other than parents of "
+ "colonists are normally not included here, and instead get calculated within the AddRelationships "
+ "method. The AddRelationships patch Performance Fish includes prevents many of the relationship "
+ "calculations in that method however, so this here adds every direct relation to player-visible pawns "
+ "as critical pawn reason, preventing these relatives from getting deleted.";
public override MethodBase TargetMethodBase { get; }
= AccessTools.Method(typeof(WorldPawnGC), nameof(WorldPawnGC.GetCriticalPawnReason));
public static void Postfix(ref string? __result, Pawn pawn)
{
if (__result != null || pawn.relations is not { } relations)
return;
foreach (var pawnWithDirectRelation in relations.pawnsWithDirectRelationsWithMe)
{
if (pawnWithDirectRelation is (not { RaceProps.Humanlike: true }) or
{
Faction: not { IsPlayer: true }, Spawned: false,
HostFaction: not { IsPlayer: true }
})
{
continue;
}
__result = "Relationship";
return;
}
}
}
public static HashSet<string> AcceptedCriticalPawnReasons
= new(new[] { "Colonist", "Generating", "Kidnapped", "CaravanMember", "TransportPod", "Spawned" });
}
@@ -0,0 +1,111 @@
// Copyright (c) 2023 bradson
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
using Mono.Cecil;
using Mono.Cecil.Cil;
using PerformanceFish.Prepatching;
using RimWorld.Planet;
namespace PerformanceFish.Planet;
public class WorldPawnsOptimization : ClassWithFishPrepatches
{
public class AllPawnsAlivePatch : FishPrepatch
{
public override string? Description { get; }
= "Minor optimization to reduce the amount of copying happening in the WorldPawns.AllPawnsAlive method";
public override MethodBase TargetMethodBase { get; }
= AccessTools.PropertyGetter(typeof(WorldPawns), nameof(WorldPawns.AllPawnsAlive));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static List<Pawn> ReplacementBody(WorldPawns instance)
{
ref var cache = ref instance.Cache();
if (CacheDirty(ref cache, instance))
{
instance.allPawnsAliveResult.Clear();
instance.allPawnsAliveResult.AddRange(instance.pawnsAlive);
instance.allPawnsAliveResult.AddRange(instance.pawnsMothballed);
UpdateCache(ref cache, instance);
}
return instance.allPawnsAliveResult;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void UpdateCache(ref Cache cache, WorldPawns instance)
{
cache.PawnsAliveVersion = instance.pawnsAlive._version;
cache.PawnsMothballedVersion = instance.pawnsMothballed._version;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool CacheDirty(ref Cache cache, WorldPawns instance)
=> instance.pawnsAlive._version != cache.PawnsAliveVersion
|| instance.pawnsMothballed._version != cache.PawnsMothballedVersion;
}
public class AllPawnsAliveOrDeadPatch : FishPrepatch
{
public override string? Description { get; }
= "Minor optimization to reduce the amount of copying happening in the WorldPawns.AllPawnsAliveOrDead "
+ "method";
public override MethodBase TargetMethodBase { get; }
= AccessTools.PropertyGetter(typeof(WorldPawns), nameof(WorldPawns.AllPawnsAliveOrDead));
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
public static List<Pawn> ReplacementBody(WorldPawns instance)
{
ref var cache = ref instance.Cache();
var allPawnsAlive = instance.AllPawnsAlive;
var allPawnsDead = instance.AllPawnsDead;
if (CacheDirty(ref cache, allPawnsAlive, allPawnsDead))
{
instance.allPawnsAliveOrDeadResult.Clear();
instance.allPawnsAliveOrDeadResult.AddRange(allPawnsAlive);
instance.allPawnsAliveOrDeadResult.AddRange(allPawnsDead);
UpdateCache(ref cache, allPawnsAlive, allPawnsDead);
}
return instance.allPawnsAliveOrDeadResult;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void UpdateCache(ref Cache cache, List<Pawn> allPawnsAlive, HashSet<Pawn> allPawnsDead)
{
cache.AllPawnsAliveVersion = allPawnsAlive._version;
cache.AllPawnsDeadVersion = allPawnsDead._version;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool CacheDirty(ref Cache cache, List<Pawn> allPawnsAlive, HashSet<Pawn> allPawnsDead)
=> allPawnsAlive._version != cache.AllPawnsAliveVersion
|| allPawnsDead._version != cache.AllPawnsDeadVersion;
}
public record struct Cache
{
public int
PawnsAliveVersion = -1,
PawnsMothballedVersion = -1,
AllPawnsAliveVersion = -1,
AllPawnsDeadVersion = -1;
public Cache()
{
}
}
}