diff --git a/1.6/Assemblies/PerformanceFish.dll b/1.6/Assemblies/PerformanceFish.dll index 6137aa9..2f33257 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 333a233..7d25cf5 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 index f949be1..90952b7 100644 --- a/Source/PerformanceFish/CompTickOptimization.cs +++ b/Source/PerformanceFish/CompTickOptimization.cs @@ -48,27 +48,19 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches private static readonly bool _hediffCompPostTickIntervalBaseIsTrivial = IsTrivialVoidMethod(_hediffCompPostTickIntervalMethod); -#if V1_6 - private static int _canSkipInheritedBaseThingCompTickState; -#else - private static readonly Lazy _canSkipInheritedBaseThingCompTick - = new(static () => _thingCompTickBaseIsTrivial && !HasHarmonyPatches(_thingCompTickMethod)); -#endif + private struct BasePatchState + { + public int CheckedTick; + public bool HasChecked; + public bool CanSkip; + } - 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 BasePatchState _thingCompTickState; + private static BasePatchState _thingCompTickIntervalState; + private static BasePatchState _thingCompTickRareState; + private static BasePatchState _thingCompTickLongState; + private static BasePatchState _hediffCompPostTickState; + private static BasePatchState _hediffCompPostTickIntervalState; private static readonly FishTable _thingCompTickTypes = new() { @@ -196,12 +188,22 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches public static void ReplacementBody(ThingWithComps instance, int delta) { + #if V1_6 + var comps = instance.comps; + if (comps is null) + return; + #else var comps = instance.AllComps; + #endif var count = comps.Count; var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickInterval(); for (var i = 0; i < count; i++) { + #if V1_6 + var comp = instance.comps![i]; + #else var comp = comps[i]; + #endif if (!canSkipInheritedBase || !_thingCompTickIntervalTypes.GetOrAdd(comp.GetType())) comp.CompTickInterval(delta); } @@ -223,12 +225,22 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches public static void ReplacementBody(ThingWithComps instance) { + #if V1_6 + var comps = instance.comps; + if (comps is null) + return; + #else var comps = instance.AllComps; + #endif var count = comps.Count; var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickRare(); for (var i = 0; i < count; i++) { + #if V1_6 + var comp = instance.comps![i]; + #else var comp = comps[i]; + #endif if (!canSkipInheritedBase || !_thingCompTickRareTypes.GetOrAdd(comp.GetType())) comp.CompTickRare(); } @@ -250,12 +262,22 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches public static void ReplacementBody(ThingWithComps instance) { + #if V1_6 + var comps = instance.comps; + if (comps is null) + return; + #else var comps = instance.AllComps; + #endif var count = comps.Count; var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickLong(); for (var i = 0; i < count; i++) { + #if V1_6 + var comp = instance.comps![i]; + #else var comp = comps[i]; + #endif if (!canSkipInheritedBase || !_thingCompTickLongTypes.GetOrAdd(comp.GetType())) comp.CompTickLong(); } @@ -281,13 +303,12 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches if (comps == null) return; - var count = comps.Count; var severityAdjustment = 0f; var canSkipInheritedBase = CanSkipInheritedBaseHediffCompPostTick(); - for (var i = 0; i < count; i++) + for (var i = 0; i < instance.comps!.Count; i++) { - var comp = comps[i]; + var comp = instance.comps[i]; if (!canSkipInheritedBase || !_hediffCompPostTickTypes.GetOrAdd(comp.GetType())) comp.CompPostTick(ref severityAdjustment); } @@ -316,13 +337,12 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches if (comps == null) return; - var count = comps.Count; var severityAdjustment = 0f; var canSkipInheritedBase = CanSkipInheritedBaseHediffCompPostTickInterval(); - for (var i = 0; i < count; i++) + for (var i = 0; i < instance.comps!.Count; i++) { - var comp = comps[i]; + var comp = instance.comps[i]; if (!canSkipInheritedBase || !_hediffCompPostTickIntervalTypes.GetOrAdd(comp.GetType())) comp.CompPostTickInterval(ref severityAdjustment, delta); } @@ -334,39 +354,47 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool CanSkipInheritedBaseThingCompTick() - { -#if V1_6 - var state = _canSkipInheritedBaseThingCompTickState; - if (state == 0) - { - state = _thingCompTickBaseIsTrivial && !HasHarmonyPatches(_thingCompTickMethod) ? 2 : 1; - _canSkipInheritedBaseThingCompTickState = state; - } - return state == 2; -#else - return _canSkipInheritedBaseThingCompTick.Value; -#endif - } + => CanSkipBaseMethod(_thingCompTickMethod, _thingCompTickBaseIsTrivial, ref _thingCompTickState); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool CanSkipInheritedBaseThingCompTickInterval() - => _canSkipInheritedBaseThingCompTickInterval.Value; + => CanSkipBaseMethod(_thingCompTickIntervalMethod, _thingCompTickIntervalBaseIsTrivial, + ref _thingCompTickIntervalState); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool CanSkipInheritedBaseThingCompTickRare() - => _canSkipInheritedBaseThingCompTickRare.Value; + => CanSkipBaseMethod(_thingCompTickRareMethod, _thingCompTickRareBaseIsTrivial, ref _thingCompTickRareState); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool CanSkipInheritedBaseThingCompTickLong() - => _canSkipInheritedBaseThingCompTickLong.Value; + => CanSkipBaseMethod(_thingCompTickLongMethod, _thingCompTickLongBaseIsTrivial, ref _thingCompTickLongState); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool CanSkipInheritedBaseHediffCompPostTick() - => _canSkipInheritedBaseHediffCompPostTick.Value; + => CanSkipBaseMethod(_hediffCompPostTickMethod, _hediffCompPostTickBaseIsTrivial, + ref _hediffCompPostTickState); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool CanSkipInheritedBaseHediffCompPostTickInterval() - => _canSkipInheritedBaseHediffCompPostTickInterval.Value; + => CanSkipBaseMethod(_hediffCompPostTickIntervalMethod, _hediffCompPostTickIntervalBaseIsTrivial, + ref _hediffCompPostTickIntervalState); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanSkipBaseMethod(MethodBase method, bool isTrivial, ref BasePatchState state) + { + if (!isTrivial) + return false; + + var tick = Find.TickManager?.TicksGame ?? int.MinValue; + if (!state.HasChecked || state.CheckedTick != tick) + { + state.CanSkip = !HasHarmonyPatches(method); + state.CheckedTick = tick; + state.HasChecked = true; + } + + return state.CanSkip; + } private static bool InheritsThingCompTick(Type type) => AccessTools.Method(type, nameof(ThingComp.CompTick))?.DeclaringType == typeof(ThingComp); diff --git a/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs b/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs index b746a5b..1c02646 100644 --- a/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs +++ b/Source/PerformanceFish/Planet/WorldObjectsOptimization.cs @@ -3,7 +3,6 @@ // 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; @@ -17,19 +16,12 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches { public override bool DefaultState => true; - private static FishTable _skippableCompTypes = null!; - private static FishTable _skippableWorldObjectTypes = null!; - private static readonly List _tmpThingHolders = []; - private static readonly List _skippedWorldObjects = []; private static List? _cachedWorldObjectsReference; - private static List? _cachedMapsReference; - - private readonly record struct SkippedWorldObject(WorldObject WorldObject, - List Comps, int CompsVersion, bool HasEnterCooldownComp); + private static List? _cachedSnapshotReference; + private static int _cachedSnapshotVersion = -2; public override string? Description { get; } - = "Caches the world objects that need ticking and skips objects with no active tick behavior. " - + "Objects with changing contents or components are rechecked before each tick."; + = "Reuses the world-object tick snapshot until the source list changes. Every object still receives DoTick."; public override MethodBase TargetMethodBase { get; } = AccessTools.Method(typeof(WorldObjectsHolder), nameof(WorldObjectsHolder.WorldObjectsHolderTick)); @@ -53,206 +45,40 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches public static void UpdateCache(WorldObjectsHolder instance) { - var staticWorldObjects = WorldObjectsHolder.tmpWorldObjects; - staticWorldObjects.Clear(); - _skippedWorldObjects.Clear(); - var instanceWorldObjects = instance.worldObjects; - - for (var i = 0; i < instanceWorldObjects.Count; i++) - { - var worldObject = instanceWorldObjects[i]; - - if (worldObject is not MapParent { HasMap: true } - && CanSkipWorldObjectTick(worldObject) - && CanSkipCompTick(worldObject)) - { - var comps = worldObject.comps; - var hasEnterCooldownComp = false; - for (var j = 0; j < comps.Count; j++) - hasEnterCooldownComp |= comps[j] is EnterCooldownComp; - _skippedWorldObjects.Add(new(worldObject, comps, comps._version, hasEnterCooldownComp)); - continue; - } - - staticWorldObjects.Add(worldObject); - } + var snapshot = WorldObjectsHolder.tmpWorldObjects; + snapshot.Clear(); + snapshot.AddRange(instanceWorldObjects); CachedWorldObjectsVersion = instanceWorldObjects._version; - CachedMapsVersion = Current.gameInt.maps._version; _cachedWorldObjectsReference = instanceWorldObjects; - _cachedMapsReference = Current.gameInt.maps; - } - - private static bool CanSkipWorldObjectTick(WorldObject worldObject) - { - return IsSkippableWorldObjectType(worldObject.GetType()) && !HasTickableContents(worldObject); - } - - private static bool HasTickableContents(WorldObject worldObject) - { - if (worldObject is not IThingHolder holder - || worldObject is IThingHolderTickable { ShouldTickContents: false }) - return false; - - lock (_tmpThingHolders) - { - _tmpThingHolders.Clear(); - try - { - _tmpThingHolders.Add(holder); - holder.GetChildHolders(_tmpThingHolders); - for (var i = 0; i < _tmpThingHolders.Count; i++) - { - var heldThings = _tmpThingHolders[i].GetDirectlyHeldThings(); - if (heldThings is not null && heldThings.Owner is not Map and not Caravan) - return true; - } - - return false; - } - finally - { - _tmpThingHolders.Clear(); - } - } - } - - private static bool CanSkipCompTick(WorldObject worldObject) - { - var comps = worldObject.comps; - if (comps.Count == 0) - return true; - - for (var i = comps.Count; i-- > 0;) - { - var comp = comps[i]; - if (comp is EnterCooldownComp { Active: true }) - return false; - - if (!IsSkippableCompType(comp.GetType())) - return false; - } - - return true; + _cachedSnapshotReference = snapshot; + _cachedSnapshotVersion = snapshot._version; } public static int CachedWorldObjectsVersion = -2, CachedMapsVersion = -2; - private static Type?[] - _whitelistedTickingCompTypes = - [ - typeof(WorldObjectComp) - ], - _whitelistedWorldObjectTypes = - [ - // Settlement.TickInterval checks trader and defeat state even without a map. - typeof(WorldObject), - typeof(MapParent) - ]; - - public static void AddCompToWhiteList(Type compType) - { - _whitelistedTickingCompTypes = _whitelistedTickingCompTypes.Add(compType); - ResetSkippableCompTypes(); - } - - public static void AddWorldObjectToWhiteList(Type worldObjectType) - { - _whitelistedWorldObjectTypes = _whitelistedWorldObjectTypes.Add(worldObjectType); - ResetSkippableWorldObjectTypes(); - } - - private static void ResetSkippableCompTypes() - { - _skippableCompTypes = new() - { - ValueInitializer = static type => ComputeIsSkippableCompType(type) - }; - } - - private static void ResetSkippableWorldObjectTypes() - { - _skippableWorldObjectTypes = new() - { - ValueInitializer = static type => ComputeIsSkippableWorldObjectType(type) - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsSkippableCompType(Type type) => _skippableCompTypes.GetOrAdd(type); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsSkippableWorldObjectType(Type type) => _skippableWorldObjectTypes.GetOrAdd(type); - - private static bool ComputeIsSkippableCompType(Type type) - { - if (AccessTools.Method(type, nameof(WorldObjectComp.CompTick))?.DeclaringType is not { } declaringType - || !_whitelistedTickingCompTypes.Contains(declaringType)) - { - return false; - } - -#if V1_6 - return AccessTools.Method(type, nameof(WorldObjectComp.CompTickInterval), [typeof(int)])?.DeclaringType - is { } intervalDeclaringType - && _whitelistedTickingCompTypes.Contains(intervalDeclaringType); -#else - return true; -#endif - } - - private static bool ComputeIsSkippableWorldObjectType(Type type) - { - if (AccessTools.Method(type, "Tick")?.DeclaringType is not { } declaringType - || !_whitelistedWorldObjectTypes.Contains(declaringType)) - { - return false; - } - -#if V1_6 - return AccessTools.Method(type, "TickInterval", [typeof(int)])?.DeclaringType is { } intervalDeclaringType - && _whitelistedWorldObjectTypes.Contains(intervalDeclaringType); -#else - return true; -#endif - } + // Retained for mods that called the old whitelist API. The snapshot no longer skips any ticks. + public static void AddCompToWhiteList(Type compType) => SetDirty(); + public static void AddWorldObjectToWhiteList(Type worldObjectType) => SetDirty(); public static bool CacheDirty(WorldObjectsHolder instance) { - if (!ReferenceEquals(_cachedWorldObjectsReference, instance.worldObjects) - || CachedWorldObjectsVersion != instance.worldObjects._version - || !ReferenceEquals(_cachedMapsReference, Current.gameInt.maps) - || CachedMapsVersion != Current.gameInt.maps._version) - return true; - - for (var i = 0; i < _skippedWorldObjects.Count; i++) - { - var skipped = _skippedWorldObjects[i]; - var worldObject = skipped.WorldObject; - var comps = worldObject.comps; - if (!ReferenceEquals(comps, skipped.Comps) || comps._version != skipped.CompsVersion - || worldObject is MapParent { HasMap: true } - || HasTickableContents(worldObject)) - return true; - - if (skipped.HasEnterCooldownComp) - for (var j = 0; j < comps.Count; j++) - if (comps[j] is EnterCooldownComp { Active: true }) - return true; - } - - return false; + var source = instance.worldObjects; + var snapshot = WorldObjectsHolder.tmpWorldObjects; + return !ReferenceEquals(_cachedWorldObjectsReference, source) + || CachedWorldObjectsVersion != source._version + || !ReferenceEquals(_cachedSnapshotReference, snapshot) + || _cachedSnapshotVersion != snapshot._version + || snapshot.Count != source.Count; } public static void SetDirty() => CachedWorldObjectsVersion = CachedMapsVersion = -2; static WorldObjectsHolderTickPatch() { - ResetSkippableCompTypes(); - ResetSkippableWorldObjectTypes(); Cache.Utility.Cleared += SetDirty; } } diff --git a/Source/PerformanceFish/Rendering/TextureAtlasPatches.cs b/Source/PerformanceFish/Rendering/TextureAtlasPatches.cs deleted file mode 100644 index 39c1b32..0000000 --- a/Source/PerformanceFish/Rendering/TextureAtlasPatches.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2023 bradson -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -namespace PerformanceFish.Rendering; - -public sealed class TextureAtlasPatches : ClassWithFishPatches -{ - public sealed class StaticTextureAtlas_ApplyTextureCompression : FishPatch - { - public override bool DefaultState => false; - - public override string? Description { get; } - = "Routes texture atlas compression through the GPU path when compute shaders are available, " - + "regardless of the VRAM threshold check in UnityData.ComputeShadersSupported. Prevents native " - + "crashes in debug Mono builds."; - - public override MethodBase TargetMethodInfo - => AccessTools.DeclaredMethod(typeof(StaticTextureAtlas), "ApplyTextureCompression"); - - public static bool Prefix(StaticTextureAtlas __instance, bool noGpuCompressionSupport) - { - if (!noGpuCompressionSupport || !SystemInfo.supportsComputeShaders) - return true; - - if (__instance.ColorTexture != null) - { - var name = __instance.ColorTexture.name; - var compressed = StaticTextureAtlas.FastCompressDXT(__instance.ColorTexture, deleteOriginal: true); - compressed.name = name; - _colorTextureField.SetValue(__instance, compressed); - } - - if (__instance.MaskTexture != null) - { - var name = __instance.MaskTexture.name; - var compressed = StaticTextureAtlas.FastCompressDXT(__instance.MaskTexture, deleteOriginal: true); - compressed.name = name; - _maskTextureField.SetValue(__instance, compressed); - } - - return false; - } - - private static readonly FieldInfo _colorTextureField - = AccessTools.DeclaredField(typeof(StaticTextureAtlas), "colorTexture"); - - private static readonly FieldInfo _maskTextureField - = AccessTools.DeclaredField(typeof(StaticTextureAtlas), "maskTexture"); - } -} diff --git a/Source/PerformanceFish/System/ReflectionCaching.cs b/Source/PerformanceFish/System/ReflectionCaching.cs index d929674..d1c8ce3 100644 --- a/Source/PerformanceFish/System/ReflectionCaching.cs +++ b/Source/PerformanceFish/System/ReflectionCaching.cs @@ -522,10 +522,13 @@ public sealed class ReflectionCaching : ClassWithFishPatches { public sealed class GetCustomAttributes : FishPatch { + private static readonly Type RuntimeTypeType = typeof(object).GetType(); + public override bool DefaultState => true; public override string? Description { get; } - = "Caches attributes for reflection lookups. Decent load time improvement."; + = "Caches empty attribute lookups for fixed metadata. Nonempty results are recreated so attribute " + + "constructors and mutable attribute instances retain normal reflection behavior."; public override Delegate? TargetMethodGroup { get; } = (Func)global::System.MonoCustomAttrs @@ -535,7 +538,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches public static bool Prefix(ICustomAttributeProvider obj, Type attributeType, bool inherit, out object[]? __result, out CustomAttributeState __state) { - if (obj is null || attributeType is null) + if (obj is null || attributeType is null || !HasFixedAttributeMetadata(obj) + || attributeType.GetType() != RuntimeTypeType || attributeType.Assembly.IsDynamic) { __state = default; __result = null; @@ -568,10 +572,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches { 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; @@ -581,7 +582,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches var centralLookup = TryUseCache(CustomAttributeCache.GetDirectly, ref key, out var centralResult); if (centralLookup == CustomAttributeCacheLookup.Hit) { - result = CopyCachedAttributes(centralResult!); + result = centralResult; CustomAttributeCache.Get[key] = new() { Attributes = centralResult, @@ -605,7 +606,10 @@ public sealed class ReflectionCaching : ClassWithFishPatches if (!__state.ShouldStore || __result is null) return; - StoreCompleted(ref __state.Key, __result); + if (__result.Length == 0) + StoreCompleted(ref __state.Key, __result); + else + ClearInProgress(ref __state.Key); } [MethodImpl(MethodImplOptions.NoInlining)] @@ -663,9 +667,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches { var value = new CustomAttributeCacheValue { - Attributes = result.Length == 0 - ? (object[])Array.CreateInstance(Type.GetTypeFromHandle(key.Second), 0) - : CopyCachedAttributes(result), + Attributes = result, Status = CustomAttributeCacheStatus.Completed }; @@ -700,9 +702,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches { } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object[] CopyCachedAttributes(object[] attributes) - => attributes.Length != 0 ? (object[])attributes.Clone() : attributes; + private static bool HasFixedAttributeMetadata(ICustomAttributeProvider provider) + => provider.GetType().Assembly == typeof(object).Assembly && (provider switch + { + MemberInfo member => !member.Module.Assembly.IsDynamic, + ParameterInfo parameter => parameter.Member is MemberInfo member + && !member.Module.Assembly.IsDynamic, + Assembly assembly => !assembly.IsDynamic, + Module module => !module.Assembly.IsDynamic, + _ => false + }); private static object _lock = new(); } @@ -1008,12 +1017,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches public sealed class FieldInfoPatches { + private static readonly Type? RuntimeFieldInfoType + = AccessTools.TypeByName("System.Reflection.RuntimeFieldInfo") + ?? AccessTools.TypeByName("System.Reflection.MonoField"); + 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"; + = "Uses cached getters for ordinary fields. Generic, dynamic and invalid calls use Mono reflection."; //public GetValue_Patch() //{ @@ -1033,7 +1046,12 @@ public sealed class ReflectionCaching : ClassWithFishPatches [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(FieldInfo __instance, object? obj, ref object? __result) { + if (__instance.GetType() != RuntimeFieldInfoType) + return true; + ref var cache = ref FieldGetters.GetOrAddReference(__instance.FieldHandle); + if (!CanUseFieldFastPath(__instance, obj, ref cache.FastPath)) + return true; if (cache.Dirty && !FieldGetters.UpdateAsyncCache(ref cache, @@ -1042,43 +1060,17 @@ public sealed class ReflectionCaching : ClassWithFishPatches return true; } - // Unsupported fields fall back to Mono's implementation, including its exception behavior. - ValidateGetValueCall(__instance, obj); __result = cache.Result!(obj); return false; } - - [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 bool DefaultState => true; public override string Description { get; } - = "Optimizes the SetValue method by invoking it through specialized cached delegates"; + = "Uses cached setters only for ordinary fields and already-compatible values. Other calls use Mono reflection."; public override MethodBase? TargetMethodInfo { get; } = AccessTools.DeclaredMethod(typeof(FieldInfo), nameof(FieldInfo.SetValue), @@ -1087,7 +1079,13 @@ public sealed class ReflectionCaching : ClassWithFishPatches [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(FieldInfo __instance, object? obj, object? value) { + if (__instance.GetType() != RuntimeFieldInfoType) + return true; + ref var cache = ref FieldSetters.GetOrAddReference(__instance.FieldHandle); + if (!CanUseFieldFastPath(__instance, obj, ref cache.FastPath, forSetter: true) + || !CanSetFieldExactly(ref cache.FastPath, value)) + return true; if (cache.Dirty && !TryUpdate(__instance, ref cache)) return true; @@ -1099,29 +1097,67 @@ public sealed class ReflectionCaching : ClassWithFishPatches [MethodImpl(MethodImplOptions.NoInlining)] private static bool TryUpdate(FieldInfo __instance, ref FieldInfoSetterCache 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 " - + $"setting a value will always fail.\n{new StackTrace(2, true)}"); - - return false; - } } - private static Task _fieldSetterFailureTask = new(static () => null!); // sentinel only + public struct FieldFastPath + { + public bool Checked; + public bool Unsupported; + public bool IsStatic; + public bool IsValueType; + public Type? DeclaringType; + public Type? FieldType; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanUseFieldFastPath(FieldInfo info, object? obj, ref FieldFastPath state, + bool forSetter = false) + { + if (!state.Checked) + InitializeFieldFastPath(info, ref state, forSetter); + + if (state.Unsupported) + return false; + + if (state.IsStatic) + return true; + + return obj is not null && obj is not MarshalByRefObject + && (obj.GetType() == state.DeclaringType || state.DeclaringType!.IsInstanceOfType(obj)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanSetFieldExactly(ref FieldFastPath state, object? value) + { + if (value is null) + return !state.IsValueType; + + var fieldType = state.FieldType!; + var valueType = value.GetType(); + return valueType == fieldType || !state.IsValueType && fieldType.IsAssignableFrom(valueType); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void InitializeFieldFastPath(FieldInfo info, ref FieldFastPath state, bool forSetter) + { + var declaringType = info.DeclaringType; + var fieldType = info.FieldType; + state.DeclaringType = declaringType; + state.FieldType = fieldType; + state.IsStatic = info.IsStatic; + state.IsValueType = fieldType.IsValueType; + // Constructed generic fields can share a RuntimeFieldHandle but need different delegates. + state.Unsupported = declaringType is null || declaringType.IsGenericType + || declaringType.ContainsGenericParameters + || typeof(MarshalByRefObject).IsAssignableFrom(declaringType) + || info.Module.Assembly.IsDynamic || info.GetRequiredCustomModifiers().Length != 0 + || info.IsLiteral || forSetter && info.IsInitOnly || fieldType.IsPointer + || fieldType.IsByRef || fieldType.ContainsGenericParameters; + state.Checked = true; + } public static FieldGetter? MakeGetterDelegate(FieldInfo info) { @@ -1206,6 +1242,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches { public FieldGetter? Result { get; set; } public Task? Task { get; set; } + public FieldFastPath FastPath; public bool Dirty => Result == null; @@ -1217,6 +1254,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches { public FieldSetter? Result { get; set; } public Task? Task { get; set; } + public FieldFastPath FastPath; public bool Dirty => Result == null; @@ -1232,13 +1270,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches public sealed class MethodBasePatches { + private static readonly Type? RuntimeMethodInfoType + = AccessTools.TypeByName("System.Reflection.RuntimeMethodInfo") + ?? AccessTools.TypeByName("System.Reflection.MonoMethod"); + public sealed class Invoke_Patch : FishPatch { - public override bool DefaultState => false; // TODO: fix + public override bool DefaultState => true; public override string Description { get; } - = "Optimizes the Invoke method by compiling specialized cached delegates. Disabled by default due to " - + "issues with various mods, like RIMMSqol and Performance Optimizer"; + = "Uses cached delegates for static, parameterless methods with fixed metadata. Other calls use Mono reflection."; public override MethodBase? TargetMethodInfo { get; } = AccessTools.DeclaredMethod(typeof(MethodBase), nameof(MethodBase.Invoke), @@ -1247,22 +1288,21 @@ public sealed class ReflectionCaching : ClassWithFishPatches [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(MethodBase __instance, object? obj, object[]? parameters, ref object? __result) { - ref var cache = ref MethodInvokers.GetOrAddReference(__instance.MethodHandle); + if (__instance is not MethodInfo methodInfo + || methodInfo.GetType() != RuntimeMethodInfoType || parameters is { Length: > 0 }) + return true; - // if (cache.Dirty) // TODO: remove - // { - // if (OnStartup.State.Initialized) - // { - // Log.Message($"Called Invoke for {__instance.FullDescription()} with {obj.ToStringSafe()}, { - // parameters?.Length.ToStringSafe() ?? "null"} parameters"); - // } - // - // if (!MethodInvokers.UpdateAsyncCache(ref cache, - // __instance.MethodHandle, __instance)) - // { - // return true; - // } - // } + ref var cache = ref MethodInvokers.GetOrAddReference(__instance.MethodHandle); + if (cache.Unsupported) + return true; + + if (!cache.EligibilityChecked) + { + cache.EligibilityChecked = true; + cache.Unsupported = !CanCacheInvoke(methodInfo); + if (cache.Unsupported) + return true; + } if (cache.Dirty && !MethodInvokers.UpdateAsyncCache(ref cache, @@ -1271,58 +1311,31 @@ public sealed class ReflectionCaching : ClassWithFishPatches return true; } - __result = cache.Result!(obj, parameters); + try + { + __result = cache.Result!(obj, parameters); + } + catch (Exception ex) + { + throw new TargetInvocationException(ex); + } return false; } } - // Similar to HarmonyLib.MethodInvoker, but correctly handles a couple of edge cases Harmony would throw on. - [SuppressMessage("Globalization", "CA1310")] + // Restrict the delegate to a method whose reflection behavior can be reproduced cheaply. public static MethodInvoker? MakeInvokeDelegate(MethodBase methodBase) { - if (methodBase.DeclaringType is null && methodBase.Name.StartsWith("PadMethod")) - return null; // padding methods generated by harmony for linux - + if (methodBase is not MethodInfo methodInfo || !CanCacheInvoke(methodInfo)) + return null; + try { - var methodInfo = methodBase as MethodInfo; - if (methodInfo is { ReturnType.IsByRef: true }) - return null; // how to box a ref return? - - var constructorInfo = methodBase as ConstructorInfo; - - 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(); - - if (methodInfo != null && !methodBase.IsStatic) - EmitInstanceArgument(il, methodBase); - - for (var i = 0; i < parameters.Length; i++) - { - il.Emit(FishTranspiler.Argument(1)); - il.Emit(FishTranspiler.Constant(i)); - - var parameterType = parameters[i].ParameterType; - - if (parameterType.IsByRef) - EmitInvokeByRefParameterLoad(il, parameterType); - else - EmitInvokeParameterLoad(il, parameterType); - } - - if (methodInfo != null) - EmitInvokeMethodCall(methodBase, il, methodInfo); - else if (constructorInfo != null) - il.Emit(FishTranspiler.New(constructorInfo)); + EmitInvokeMethodCall(methodInfo, il); il.Emit(FishTranspiler.Return); @@ -1337,109 +1350,45 @@ public sealed class ReflectionCaching : ClassWithFishPatches } } - private static bool CanCacheInvoke(MethodBase methodBase, MethodInfo? methodInfo, - ConstructorInfo? constructorInfo, ParameterInfo[] parameters) + private static bool CanCacheInvoke(MethodInfo methodInfo) { - if (methodBase.ContainsGenericParameters - || (methodBase.CallingConvention & CallingConventions.VarArgs) != 0) + try + { + var declaringType = methodInfo.DeclaringType; + var returnType = methodInfo.ReturnType; + // Constructed generic methods and types need more than a RuntimeMethodHandle as a key. + return methodInfo.IsStatic && !methodInfo.IsGenericMethod + && declaringType is not null && !declaringType.IsGenericType + && !declaringType.ContainsGenericParameters + && !methodInfo.Module.Assembly.IsDynamic + && methodInfo.GetParameters().Length == 0 + && !methodInfo.ContainsGenericParameters + && (methodInfo.CallingConvention & CallingConventions.VarArgs) == 0 + && !returnType.IsByRef && !returnType.IsPointer + && !returnType.ContainsGenericParameters + && methodInfo.GetMethodBody() is not null; + } + catch (Exception) { 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) + private static void EmitInvokeMethodCall(MethodInfo methodInfo, ILGenerator il) { - // 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)); - - if (!methodInfo.ReturnType.IsValueType) - return; - - il.Emit(methodInfo.ReturnType == typeof(void) - ? FishTranspiler.Null - : FishTranspiler.Box(methodInfo.ReturnType)); - } - - private static void EmitInvokeParameterLoad(ILGenerator il, Type parameterType) - { - il.Emit(FishTranspiler.LoadElement()); - - il.Emit(parameterType.IsValueType - ? FishTranspiler.Call(typeof(ReflectionCaching), - parameterType.IsNullable() ? nameof(UnboxNullableSafely) : nameof(UnboxSafely), - [typeof(object)], - [ - parameterType.IsNullable() - ? Nullable.GetUnderlyingType(parameterType) - : parameterType - ]) - : FishTranspiler.Call(typeof(ReflectionCaching), nameof(CastOrConvert), - generics: [parameterType])); - } - - private static void EmitInvokeByRefParameterLoad(ILGenerator il, Type parameterType) - { - parameterType = parameterType.GetElementType()!; - - il.Emit(FishTranspiler.LoadElementAddress()); - - if (parameterType.IsNullable()) - { - il.Emit(FishTranspiler.Call(typeof(ReflectionCaching), nameof(VerifyNullableBoxObject), - generics: [Nullable.GetUnderlyingType(parameterType)])); - - il.Emit(FishTranspiler.UnboxAddress(parameterType)); - } - else - { - il.Emit(parameterType.IsValueType - ? FishTranspiler.Call(typeof(ReflectionCaching), nameof(UnboxRefSafely), - generics: - [ - /*parameterType.IsNullable() - ? Nullable.GetUnderlyingType(parameterType) - :*/ parameterType - ]) - : FishTranspiler.Call(typeof(ReflectionCaching), nameof(CastRefSafely), generics: [parameterType])); - } + il.Emit(FishTranspiler.Call(methodInfo)); + if (methodInfo.ReturnType == typeof(void)) + il.Emit(FishTranspiler.Null); + else if (methodInfo.ReturnType.IsValueType) + il.Emit(FishTranspiler.Box(methodInfo.ReturnType)); } public record struct MethodInvokerCache : IAsyncCacheable { public MethodInvoker? Result { get; set; } public Task? Task { get; set; } + public bool EligibilityChecked; + public bool Unsupported; public bool Dirty => Result == null; public ValueTask MakeResultAsync(RuntimeMethodHandle key, MethodBase method)