diff --git a/1.6/Assemblies/PerformanceFish.dll b/1.6/Assemblies/PerformanceFish.dll index f546adc..1798725 100644 Binary files a/1.6/Assemblies/PerformanceFish.dll and b/1.6/Assemblies/PerformanceFish.dll differ diff --git a/1.6/Assemblies/PerformanceFish.pdb b/1.6/Assemblies/PerformanceFish.pdb index 689dc06..bdc70d0 100644 Binary files a/1.6/Assemblies/PerformanceFish.pdb and b/1.6/Assemblies/PerformanceFish.pdb differ 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/GenLocalDateCaching.cs b/Source/PerformanceFish/GenLocalDateCaching.cs index 29fb66e..baa3dc6 100644 --- a/Source/PerformanceFish/GenLocalDateCaching.cs +++ b/Source/PerformanceFish/GenLocalDateCaching.cs @@ -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)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; + public static int HourInteger = -2; + public static Season Season = Season.Undefined; } 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/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/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/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 index 1c9d9e4..39c1b32 100644 --- a/Source/PerformanceFish/Rendering/TextureAtlasPatches.cs +++ b/Source/PerformanceFish/Rendering/TextureAtlasPatches.cs @@ -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 " diff --git a/Source/PerformanceFish/System/ReflectionCaching.cs b/Source/PerformanceFish/System/ReflectionCaching.cs index b793037..a725945 100644 --- a/Source/PerformanceFish/System/ReflectionCaching.cs +++ b/Source/PerformanceFish/System/ReflectionCaching.cs @@ -10,7 +10,6 @@ using System.Security; using System.Threading.Tasks; using PerformanceFish.Cache; using PerformanceFish.ModCompatibility; -#if false using ConstructorInfoCache = PerformanceFish.Cache.ByReference, @@ -57,13 +56,11 @@ using ActivatorCache using CustomAttributeCache = PerformanceFish.Cache.ByReferenceClassic; -#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(_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(); + 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.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 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>(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 @@ -772,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"; @@ -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(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 " @@ -944,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) { @@ -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, 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)); @@ -1252,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(); @@ -1264,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)] @@ -1314,5 +1527,4 @@ public sealed class ReflectionCaching : ClassWithFishPatches // populated with dynamic methods by MethodBasePatches.MakeInvokeDelegate, MakeGetterDelegate and // MakeSetterDelegate } -#endif }