add comp tick, needs tracker and pawn rendering optimizations, cache more dates, fix reflection patches, default to disabled for texture atlas patch

This commit is contained in:
localavaster
2026-04-18 15:18:26 -06:00
parent 1acf3ce319
commit 7d96ecdb1d
10 changed files with 984 additions and 80 deletions
Binary file not shown.
Binary file not shown.
@@ -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<bool> _canSkipInheritedBaseThingCompTick
= new(static () => _thingCompTickBaseIsTrivial && !HasHarmonyPatches(_thingCompTickMethod));
private static readonly Lazy<bool> _canSkipInheritedBaseThingCompTickInterval
= new(static () => _thingCompTickIntervalBaseIsTrivial && !HasHarmonyPatches(_thingCompTickIntervalMethod));
private static readonly Lazy<bool> _canSkipInheritedBaseThingCompTickRare
= new(static () => _thingCompTickRareBaseIsTrivial && !HasHarmonyPatches(_thingCompTickRareMethod));
private static readonly Lazy<bool> _canSkipInheritedBaseThingCompTickLong
= new(static () => _thingCompTickLongBaseIsTrivial && !HasHarmonyPatches(_thingCompTickLongMethod));
private static readonly Lazy<bool> _canSkipInheritedBaseHediffCompPostTick
= new(static () => _hediffCompPostTickBaseIsTrivial && !HasHarmonyPatches(_hediffCompPostTickMethod));
private static readonly Lazy<bool> _canSkipInheritedBaseHediffCompPostTickInterval = new(
static () => _hediffCompPostTickIntervalBaseIsTrivial && !HasHarmonyPatches(_hediffCompPostTickIntervalMethod));
private static readonly FishTable<Type, bool> _thingCompTickTypes = new()
{
ValueInitializer = static type => InheritsThingCompTick(type)
};
private static readonly FishTable<Type, bool> _thingCompTickIntervalTypes = new()
{
ValueInitializer = static type => InheritsThingCompTickInterval(type)
};
private static readonly FishTable<Type, bool> _thingCompTickRareTypes = new()
{
ValueInitializer = static type => InheritsThingCompTickRare(type)
};
private static readonly FishTable<Type, bool> _thingCompTickLongTypes = new()
{
ValueInitializer = static type => InheritsThingCompTickLong(type)
};
private static readonly FishTable<Type, bool> _hediffCompPostTickTypes = new()
{
ValueInitializer = static type => InheritsHediffCompPostTick(type)
};
private static readonly FishTable<Type, bool> _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);
}
}
+79 -3
View File
@@ -51,7 +51,7 @@ public sealed class GenLocalDateCaching : ClassWithFishPatches
return __state = true;
}
if (SavedTick != TickHelper.TicksGame)
if (SavedDayTickTick != TickHelper.TicksGame)
return __state = true;
__result = DayTick;
@@ -64,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<Map, int>)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<Map, Season>)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;
public static int HourInteger = -2;
public static Season Season = Season.Undefined;
}
@@ -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<Need>);
public static T? TryGetNeed<T>(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<Need>);
public static bool TryGetNeed<T>(Pawn_NeedsTracker instance, out T? need) where T : Need
=> TryGetNeedInternal(instance, out need);
}
private static bool TryGetNeedInternal<T>(Pawn_NeedsTracker instance, out T? need) where T : Need
{
ref var cache
= ref Cache.ByReference<Pawn_NeedsTracker, RuntimeTypeHandle, CacheValue>.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<T>(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<Need>? _listReference;
private int _listVersion = -2;
public Need? Need;
public bool Found;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty(List<Need> needs) => !ReferenceEquals(needs, _listReference) || needs._version != _listVersion;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Update(List<Need> needs, Need? need, bool found)
{
_listReference = needs;
_listVersion = needs._version;
Need = need;
Found = found;
}
}
}
@@ -59,7 +59,9 @@ public class FishPatchHolder : IExposable, IEnumerable<FishPatch>
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<FishPatch>
IEnumerator IEnumerable.GetEnumerator() => All.Values.GetEnumerator();
private Type _type;
}
}
@@ -37,7 +37,9 @@ public class FishPrepatchHolder : IExposable, IEnumerable<FishPrepatchBase>
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<FishPrepatchBase>
IEnumerator IEnumerable.GetEnumerator() => All.Values.GetEnumerator();
private Type _type;
}
}
@@ -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<HediffComp_Invisibility>();
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<DamageFlasher, int> _lastDamageTick
= AccessTools.FieldRefAccess<DamageFlasher, int>("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<DamageFlasher, int> _lastDamageTick
= AccessTools.FieldRefAccess<DamageFlasher, int>("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);
}
}
}
@@ -9,6 +9,8 @@ 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 "
@@ -10,7 +10,6 @@ using System.Security;
using System.Threading.Tasks;
using PerformanceFish.Cache;
using PerformanceFish.ModCompatibility;
#if false
using ConstructorInfoCache
= PerformanceFish.Cache.ByReference<System.RuntimeTypeHandle, System.Reflection.BindingFlags,
PerformanceFish.System.ReflectionCaching.RecordArray<System.Type>,
@@ -57,13 +56,11 @@ using ActivatorCache
using CustomAttributeCache
= PerformanceFish.Cache.ByReferenceClassic<System.Reflection.ICustomAttributeProvider, System.RuntimeTypeHandle, bool,
PerformanceFish.System.ReflectionCaching.MonoCustomAttrs.CustomAttributeCacheValue>;
#endif
namespace PerformanceFish.System;
public sealed class ReflectionCaching : ClassWithFishPatches
{
#if false
static ReflectionCaching() // necessary to prevent recursion between patches and function pointer creation
{
try
@@ -89,7 +86,6 @@ public sealed class ReflectionCaching : ClassWithFishPatches
ex}");
}
}
#endif
public record struct StateAndFlags
{
@@ -97,7 +93,6 @@ public sealed class ReflectionCaching : ClassWithFishPatches
public BindingFlags Flags;
}
#if false
public sealed class TypePatches
{
public sealed class GetField_Patch : FishPatch
@@ -387,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 "
@@ -397,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();
}
@@ -479,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<string?>(_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;
}
}
@@ -504,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.";
@@ -539,16 +559,22 @@ public sealed class ReflectionCaching : ClassWithFishPatches
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)
{
var localLookup = TryUseCache(CustomAttributeCache.Get, ref key, out result);
if (localLookup != CustomAttributeCacheLookup.Miss)
return localLookup;
var centralLookup = TryUseCache(CustomAttributeCache.GetDirectly, ref key, out var centralResult);
if (centralLookup == CustomAttributeCacheLookup.Hit)
{
result = centralResult;
result = CopyCachedAttributes(centralResult!);
CustomAttributeCache.Get[key] = new()
{
Attributes = centralResult,
@@ -567,19 +593,19 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Postfix(ICustomAttributeProvider obj, object[]? __result,
in CustomAttributeState __state)
ref CustomAttributeState __state)
{
if (!__state.ShouldStore || __result is null)
return;
StoreCompleted(ref Unsafe.AsRef(in __state.Key), __result);
StoreCompleted(ref __state.Key, __result);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static Exception? Finalizer(Exception? __exception, in CustomAttributeState __state)
public static Exception? Finalizer(Exception? __exception, ref CustomAttributeState __state)
{
if (__state.ShouldStore && __exception != null)
ClearInProgress(ref Unsafe.AsRef(in __state.Key));
ClearInProgress(ref __state.Key);
return __exception;
}
@@ -630,7 +656,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
var value = new CustomAttributeCacheValue
{
Attributes = result,
Attributes = CopyCachedAttributes(result),
Status = CustomAttributeCacheStatus.Completed
};
@@ -665,6 +691,10 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object[] CopyCachedAttributes(object[] attributes)
=> attributes.Length != 0 ? (object[])attributes.Clone() : Array.Empty<object>();
private static object _lock = new();
}
@@ -707,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)]
@@ -717,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<T>.Default.Equals(Array[i], other.Array[i]))
return false;
}
@@ -738,6 +775,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
public sealed class CreateInstance_Type : FishPatch
{
public override bool DefaultState => true;
public override List<string> IncompatibleModIDs { get; } = [PackageIDs.MULTIPLAYER];
public override string? Description { get; }
@@ -749,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<ActivatorCacheValue, Type, Func<object>>(ref cache,
type.TypeHandle, type))
&& !ActivatorCache.UpdateAsyncCache<ActivatorCacheValue, RuntimeType, Func<object>>(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<object> 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<object> _unsupportedActivatorSentinel
= static () => throw new InvalidOperationException("Unsupported activator sentinel");
}
// public sealed class CreateInstance_Type_Args : FishPatch
@@ -772,14 +884,34 @@ public sealed class ReflectionCaching : ClassWithFishPatches
// = (Func<Type, object[], object>)Activator.CreateInstance;
// }
public record struct ActivatorCacheValue : IAsyncCacheable<RuntimeTypeHandle, Type, Func<object>>
public record struct ActivatorCacheValue : IAsyncCacheable<RuntimeTypeHandle, RuntimeType, Func<object>>
{
public Func<object>? Result { get; set; }
public Task<Func<object>>? Task { get; set; }
public bool Dirty => Result == null;
public ValueTask<Func<object>?> MakeResultAsync(RuntimeTypeHandle key, Type type)
=> new(Reflection.CreateConstructorDelegate<Func<object>>(type));
public ValueTask<Func<object>?> MakeResultAsync(RuntimeTypeHandle key, RuntimeType type)
=> new(MakeActivatorDelegate(type));
[MethodImpl(MethodImplOptions.NoInlining)]
private static Func<object>? MakeActivatorDelegate(RuntimeType type)
{
if (type.IsValueType)
return Reflection.CreateConstructorDelegate<Func<object>>(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<object>)dynamicMethod.CreateDelegate(typeof(Func<object>));
}
}
// public record struct ActivatorWithArgumentsCacheValue : IAsyncCacheable<ActivatorWithArgumentsCache, Type,
@@ -869,6 +1001,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches
{
public sealed class GetValue_Patch : FishPatch
{
public override bool DefaultState => true;
public override string Description { get; }
= "Optimizes the GetValue method by invoking it through specialized cached delegates";
@@ -890,6 +1024,8 @@ 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
@@ -902,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";
@@ -927,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<FieldInfoSetterCache, FieldInfo, FieldSetter>(ref cache,
{
if (__instance.IsConst())
return ErrorForInvalidCallAttempt(__instance, ref cache);
return FieldSetters.UpdateAsyncCache<FieldInfoSetterCache, FieldInfo, FieldSetter>(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 "
@@ -944,7 +1112,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
}
}
private static Task<FieldSetter> _fieldSetterFailureTask = new(static () => null!);
private static Task<FieldSetter> _fieldSetterFailureTask = new(static () => null!); // sentinel only
public static FieldGetter? MakeGetterDelegate(FieldInfo info)
{
@@ -1116,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);
@@ -1158,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<T>, so the
// current byref fast path cannot safely hand out a Nullable<T>&.
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));
@@ -1252,7 +1465,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[SecuritySafeCritical]
public static ref TTo CastRefSafely<TTo>(ref object? from)
public static ref TTo CastRefSafely<TTo>(ref object? from) where TTo : class?
=> ref from is TTo or null
? ref Unsafe.As<object?, TTo>(ref from)
: ref ThrowInvalidCastException<TTo>();
@@ -1264,7 +1477,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
? ref Unsafe.Unbox<TTo>(from)
: ref Unsafe.Unbox<TTo>(from
= from is null
? default
? default(TTo)
: FisheryLib.Convert.Type<object, TTo>(from));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -1314,5 +1527,4 @@ public sealed class ReflectionCaching : ClassWithFishPatches
// populated with dynamic methods by MethodBasePatches.MakeInvokeDelegate, MakeGetterDelegate and
// MakeSetterDelegate
}
#endif
}