Compare commits
3 Commits
e1a79fc0b0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
063d5641d0
|
|||
|
b6f255ebbb
|
|||
|
e766ac833c
|
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
3811986740
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
NOTE: SWAP TO THE rimworld-1.6 BRANCH BEFORE DOWNLOADING OTHERWISE IT WILL NOT WORK FOR 1.6!
|
|
||||||
# Performance Fish
|
# Performance Fish
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -48,27 +48,19 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
|
|||||||
private static readonly bool _hediffCompPostTickIntervalBaseIsTrivial
|
private static readonly bool _hediffCompPostTickIntervalBaseIsTrivial
|
||||||
= IsTrivialVoidMethod(_hediffCompPostTickIntervalMethod);
|
= IsTrivialVoidMethod(_hediffCompPostTickIntervalMethod);
|
||||||
|
|
||||||
#if V1_6
|
private struct BasePatchState
|
||||||
private static int _canSkipInheritedBaseThingCompTickState;
|
{
|
||||||
#else
|
public int CheckedTick;
|
||||||
private static readonly Lazy<bool> _canSkipInheritedBaseThingCompTick
|
public bool HasChecked;
|
||||||
= new(static () => _thingCompTickBaseIsTrivial && !HasHarmonyPatches(_thingCompTickMethod));
|
public bool CanSkip;
|
||||||
#endif
|
}
|
||||||
|
|
||||||
private static readonly Lazy<bool> _canSkipInheritedBaseThingCompTickInterval
|
private static BasePatchState _thingCompTickState;
|
||||||
= new(static () => _thingCompTickIntervalBaseIsTrivial && !HasHarmonyPatches(_thingCompTickIntervalMethod));
|
private static BasePatchState _thingCompTickIntervalState;
|
||||||
|
private static BasePatchState _thingCompTickRareState;
|
||||||
private static readonly Lazy<bool> _canSkipInheritedBaseThingCompTickRare
|
private static BasePatchState _thingCompTickLongState;
|
||||||
= new(static () => _thingCompTickRareBaseIsTrivial && !HasHarmonyPatches(_thingCompTickRareMethod));
|
private static BasePatchState _hediffCompPostTickState;
|
||||||
|
private static BasePatchState _hediffCompPostTickIntervalState;
|
||||||
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()
|
private static readonly FishTable<Type, bool> _thingCompTickTypes = new()
|
||||||
{
|
{
|
||||||
@@ -196,12 +188,22 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
|
|||||||
|
|
||||||
public static void ReplacementBody(ThingWithComps instance, int delta)
|
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;
|
var comps = instance.AllComps;
|
||||||
|
#endif
|
||||||
var count = comps.Count;
|
var count = comps.Count;
|
||||||
var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickInterval();
|
var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickInterval();
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
|
#if V1_6
|
||||||
|
var comp = instance.comps![i];
|
||||||
|
#else
|
||||||
var comp = comps[i];
|
var comp = comps[i];
|
||||||
|
#endif
|
||||||
if (!canSkipInheritedBase || !_thingCompTickIntervalTypes.GetOrAdd(comp.GetType()))
|
if (!canSkipInheritedBase || !_thingCompTickIntervalTypes.GetOrAdd(comp.GetType()))
|
||||||
comp.CompTickInterval(delta);
|
comp.CompTickInterval(delta);
|
||||||
}
|
}
|
||||||
@@ -223,12 +225,22 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
|
|||||||
|
|
||||||
public static void ReplacementBody(ThingWithComps instance)
|
public static void ReplacementBody(ThingWithComps instance)
|
||||||
{
|
{
|
||||||
|
#if V1_6
|
||||||
|
var comps = instance.comps;
|
||||||
|
if (comps is null)
|
||||||
|
return;
|
||||||
|
#else
|
||||||
var comps = instance.AllComps;
|
var comps = instance.AllComps;
|
||||||
|
#endif
|
||||||
var count = comps.Count;
|
var count = comps.Count;
|
||||||
var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickRare();
|
var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickRare();
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
|
#if V1_6
|
||||||
|
var comp = instance.comps![i];
|
||||||
|
#else
|
||||||
var comp = comps[i];
|
var comp = comps[i];
|
||||||
|
#endif
|
||||||
if (!canSkipInheritedBase || !_thingCompTickRareTypes.GetOrAdd(comp.GetType()))
|
if (!canSkipInheritedBase || !_thingCompTickRareTypes.GetOrAdd(comp.GetType()))
|
||||||
comp.CompTickRare();
|
comp.CompTickRare();
|
||||||
}
|
}
|
||||||
@@ -250,12 +262,22 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
|
|||||||
|
|
||||||
public static void ReplacementBody(ThingWithComps instance)
|
public static void ReplacementBody(ThingWithComps instance)
|
||||||
{
|
{
|
||||||
|
#if V1_6
|
||||||
|
var comps = instance.comps;
|
||||||
|
if (comps is null)
|
||||||
|
return;
|
||||||
|
#else
|
||||||
var comps = instance.AllComps;
|
var comps = instance.AllComps;
|
||||||
|
#endif
|
||||||
var count = comps.Count;
|
var count = comps.Count;
|
||||||
var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickLong();
|
var canSkipInheritedBase = CanSkipInheritedBaseThingCompTickLong();
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
|
#if V1_6
|
||||||
|
var comp = instance.comps![i];
|
||||||
|
#else
|
||||||
var comp = comps[i];
|
var comp = comps[i];
|
||||||
|
#endif
|
||||||
if (!canSkipInheritedBase || !_thingCompTickLongTypes.GetOrAdd(comp.GetType()))
|
if (!canSkipInheritedBase || !_thingCompTickLongTypes.GetOrAdd(comp.GetType()))
|
||||||
comp.CompTickLong();
|
comp.CompTickLong();
|
||||||
}
|
}
|
||||||
@@ -281,13 +303,12 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
|
|||||||
if (comps == null)
|
if (comps == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var count = comps.Count;
|
|
||||||
var severityAdjustment = 0f;
|
var severityAdjustment = 0f;
|
||||||
var canSkipInheritedBase = CanSkipInheritedBaseHediffCompPostTick();
|
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()))
|
if (!canSkipInheritedBase || !_hediffCompPostTickTypes.GetOrAdd(comp.GetType()))
|
||||||
comp.CompPostTick(ref severityAdjustment);
|
comp.CompPostTick(ref severityAdjustment);
|
||||||
}
|
}
|
||||||
@@ -316,13 +337,12 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
|
|||||||
if (comps == null)
|
if (comps == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var count = comps.Count;
|
|
||||||
var severityAdjustment = 0f;
|
var severityAdjustment = 0f;
|
||||||
var canSkipInheritedBase = CanSkipInheritedBaseHediffCompPostTickInterval();
|
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()))
|
if (!canSkipInheritedBase || !_hediffCompPostTickIntervalTypes.GetOrAdd(comp.GetType()))
|
||||||
comp.CompPostTickInterval(ref severityAdjustment, delta);
|
comp.CompPostTickInterval(ref severityAdjustment, delta);
|
||||||
}
|
}
|
||||||
@@ -334,39 +354,47 @@ public sealed class CompTickOptimization : ClassWithFishPrepatches
|
|||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static bool CanSkipInheritedBaseThingCompTick()
|
private static bool CanSkipInheritedBaseThingCompTick()
|
||||||
{
|
=> CanSkipBaseMethod(_thingCompTickMethod, _thingCompTickBaseIsTrivial, ref _thingCompTickState);
|
||||||
#if V1_6
|
|
||||||
var state = _canSkipInheritedBaseThingCompTickState;
|
|
||||||
if (state == 0)
|
|
||||||
{
|
|
||||||
state = _thingCompTickBaseIsTrivial && !HasHarmonyPatches(_thingCompTickMethod) ? 2 : 1;
|
|
||||||
_canSkipInheritedBaseThingCompTickState = state;
|
|
||||||
}
|
|
||||||
return state == 2;
|
|
||||||
#else
|
|
||||||
return _canSkipInheritedBaseThingCompTick.Value;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static bool CanSkipInheritedBaseThingCompTickInterval()
|
private static bool CanSkipInheritedBaseThingCompTickInterval()
|
||||||
=> _canSkipInheritedBaseThingCompTickInterval.Value;
|
=> CanSkipBaseMethod(_thingCompTickIntervalMethod, _thingCompTickIntervalBaseIsTrivial,
|
||||||
|
ref _thingCompTickIntervalState);
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static bool CanSkipInheritedBaseThingCompTickRare()
|
private static bool CanSkipInheritedBaseThingCompTickRare()
|
||||||
=> _canSkipInheritedBaseThingCompTickRare.Value;
|
=> CanSkipBaseMethod(_thingCompTickRareMethod, _thingCompTickRareBaseIsTrivial, ref _thingCompTickRareState);
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static bool CanSkipInheritedBaseThingCompTickLong()
|
private static bool CanSkipInheritedBaseThingCompTickLong()
|
||||||
=> _canSkipInheritedBaseThingCompTickLong.Value;
|
=> CanSkipBaseMethod(_thingCompTickLongMethod, _thingCompTickLongBaseIsTrivial, ref _thingCompTickLongState);
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static bool CanSkipInheritedBaseHediffCompPostTick()
|
private static bool CanSkipInheritedBaseHediffCompPostTick()
|
||||||
=> _canSkipInheritedBaseHediffCompPostTick.Value;
|
=> CanSkipBaseMethod(_hediffCompPostTickMethod, _hediffCompPostTickBaseIsTrivial,
|
||||||
|
ref _hediffCompPostTickState);
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static bool CanSkipInheritedBaseHediffCompPostTickInterval()
|
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)
|
private static bool InheritsThingCompTick(Type type)
|
||||||
=> AccessTools.Method(type, nameof(ThingComp.CompTick))?.DeclaringType == typeof(ThingComp);
|
=> AccessTools.Method(type, nameof(ThingComp.CompTick))?.DeclaringType == typeof(ThingComp);
|
||||||
|
|||||||
@@ -72,13 +72,89 @@ public sealed class GasGridOptimization : ClassWithFishPrepatches
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if V1_6
|
||||||
|
public abstract class PackedGasPatch : FishPrepatch
|
||||||
|
{
|
||||||
|
public override bool ShowSettings => false;
|
||||||
|
public override bool Enabled
|
||||||
|
{
|
||||||
|
get => base.Enabled && Get<TickPatch>().Enabled;
|
||||||
|
set => base.Enabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static uint PackedDensityAt(GasGrid grid, int index)
|
||||||
|
{
|
||||||
|
var grids = grid.ParallelGasGrids();
|
||||||
|
return ((uint)grids[3].DensityAt(index) << 24)
|
||||||
|
| ((uint)grids[2].DensityAt(index) << 16)
|
||||||
|
| ((uint)grids[1].DensityAt(index) << 8)
|
||||||
|
| grids[0].DensityAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GetDirectIndexPatch : PackedGasPatch
|
||||||
|
{
|
||||||
|
public override MethodBase TargetMethodBase { get; }
|
||||||
|
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.GetDirect), [typeof(int)]);
|
||||||
|
|
||||||
|
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
|
||||||
|
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
|
||||||
|
|
||||||
|
public static uint ReplacementBody(GasGrid __instance, int index)
|
||||||
|
=> PackedDensityAt(__instance, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GetDirectCellPatch : PackedGasPatch
|
||||||
|
{
|
||||||
|
public override MethodBase TargetMethodBase { get; }
|
||||||
|
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.GetDirect), [typeof(IntVec3)]);
|
||||||
|
|
||||||
|
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
|
||||||
|
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
|
||||||
|
|
||||||
|
public static uint ReplacementBody(GasGrid __instance, IntVec3 c)
|
||||||
|
=> PackedDensityAt(__instance, c.CellToIndex(__instance.map));
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SetPackedDirectPatch : PackedGasPatch
|
||||||
|
{
|
||||||
|
public override MethodBase TargetMethodBase { get; }
|
||||||
|
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.SetDirect),
|
||||||
|
[typeof(int), typeof(uint)]);
|
||||||
|
|
||||||
|
public static void Postfix(GasGrid __instance, int index)
|
||||||
|
{
|
||||||
|
var density = __instance.gasDensity[index];
|
||||||
|
SetDirectPatch.ReplacementBody(__instance, index, (byte)density, (byte)(density >> 8),
|
||||||
|
(byte)(density >> 16), (byte)(density >> 24));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ClearCellUnsafePatch : PackedGasPatch
|
||||||
|
{
|
||||||
|
public override MethodBase TargetMethodBase { get; }
|
||||||
|
= AccessTools.DeclaredMethod(typeof(GasGrid), nameof(GasGrid.ClearCellUnsafe));
|
||||||
|
|
||||||
|
public static void Postfix(GasGrid __instance, IntVec3 cell)
|
||||||
|
{
|
||||||
|
var index = cell.CellToIndex(__instance.map);
|
||||||
|
var grids = __instance.ParallelGasGrids();
|
||||||
|
for (var i = 0; i < grids.Length; i++)
|
||||||
|
grids[i].SetDirect(index, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
public sealed class TickPatch : FishPrepatch
|
public sealed class TickPatch : FishPrepatch
|
||||||
{
|
{
|
||||||
public override List<Type> LinkedPatches { get; } =
|
public override List<Type> LinkedPatches { get; } =
|
||||||
[
|
[
|
||||||
typeof(SetDirectPatch),
|
typeof(SetDirectPatch),
|
||||||
#if V1_4
|
typeof(CalculateGasEffectsPatch),
|
||||||
typeof(ColorAtPatch),
|
typeof(ColorAtPatch),
|
||||||
|
#if V1_6
|
||||||
|
typeof(GetDirectIndexPatch), typeof(GetDirectCellPatch),
|
||||||
|
typeof(SetPackedDirectPatch), typeof(ClearCellUnsafePatch),
|
||||||
#endif
|
#endif
|
||||||
typeof(AddGasPatch), typeof(AnyGasAtPatch),
|
typeof(AddGasPatch), typeof(AnyGasAtPatch),
|
||||||
typeof(DensityAtPatch), typeof(Debug_ClearAllPatch), typeof(Debug_FillAllPatch),
|
typeof(DensityAtPatch), typeof(Debug_ClearAllPatch), typeof(Debug_FillAllPatch),
|
||||||
@@ -174,6 +250,35 @@ public sealed class GasGridOptimization : ClassWithFishPrepatches
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class CalculateGasEffectsPatch : FishPrepatch
|
||||||
|
{
|
||||||
|
public override bool ShowSettings => false;
|
||||||
|
public override bool Enabled
|
||||||
|
{
|
||||||
|
get => base.Enabled && Get<TickPatch>().Enabled;
|
||||||
|
set => base.Enabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override MethodBase TargetMethodBase { get; }
|
||||||
|
= AccessTools.PropertyGetter(typeof(GasGrid), nameof(GasGrid.CalculateGasEffects));
|
||||||
|
|
||||||
|
public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module)
|
||||||
|
=> ilProcessor.ReplaceBodyWith(ReplacementBody);
|
||||||
|
|
||||||
|
public static bool ReplacementBody(GasGrid __instance)
|
||||||
|
{
|
||||||
|
// Vanilla uses this to decide whether to equalize gas through buildings.
|
||||||
|
var gasGrids = __instance.ParallelGasGrids();
|
||||||
|
for (var i = 0; i < gasGrids.Length; i++)
|
||||||
|
{
|
||||||
|
if (gasGrids[i].CalculateGasEffects)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if !V1_4
|
#if !V1_4
|
||||||
public class ParallelJob : IJobParallelFor
|
public class ParallelJob : IJobParallelFor
|
||||||
{
|
{
|
||||||
@@ -186,6 +291,11 @@ public sealed class GasGridOptimization : ClassWithFishPrepatches
|
|||||||
public sealed class ColorAtPatch : FishPrepatch
|
public sealed class ColorAtPatch : FishPrepatch
|
||||||
{
|
{
|
||||||
public override bool ShowSettings => false;
|
public override bool ShowSettings => false;
|
||||||
|
public override bool Enabled
|
||||||
|
{
|
||||||
|
get => base.Enabled && Get<TickPatch>().Enabled;
|
||||||
|
set => base.Enabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
public override MethodBase TargetMethodBase { get; }
|
public override MethodBase TargetMethodBase { get; }
|
||||||
#if V1_4
|
#if V1_4
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
// 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 System.Collections;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace PerformanceFish.Listers;
|
||||||
|
|
||||||
|
// ListerThings contains object instances. An index map keeps removals constant-time without
|
||||||
|
// using FishTable's collision-chain relocation, which can fail during removal.
|
||||||
|
internal sealed class IndexedReferenceList<T> : IList<T>, IList, IReadOnlyList<T> where T : Thing
|
||||||
|
{
|
||||||
|
private readonly List<T> _items = [];
|
||||||
|
private readonly Dictionary<T, int> _indices = new(ReferenceComparer.Instance);
|
||||||
|
|
||||||
|
public T this[int index]
|
||||||
|
{
|
||||||
|
get => _items[index];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
throw new ArgumentNullException(nameof(value));
|
||||||
|
|
||||||
|
var previous = _items[index];
|
||||||
|
if (ReferenceEquals(previous, value))
|
||||||
|
return;
|
||||||
|
if (_indices.ContainsKey(value))
|
||||||
|
throw new InvalidOperationException("Thing is already in the type cache.");
|
||||||
|
|
||||||
|
_indices.Remove(previous);
|
||||||
|
_items[index] = value;
|
||||||
|
_indices.Add(value, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object? IList.this[int index]
|
||||||
|
{
|
||||||
|
get => this[index];
|
||||||
|
set => this[index] = RequireThing(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _items.Count;
|
||||||
|
public bool IsReadOnly => false;
|
||||||
|
public bool IsFixedSize => false;
|
||||||
|
public bool IsSynchronized => false;
|
||||||
|
public object SyncRoot => this;
|
||||||
|
|
||||||
|
public void Add(T item)
|
||||||
|
{
|
||||||
|
if (item is null)
|
||||||
|
throw new ArgumentNullException(nameof(item));
|
||||||
|
if (_indices.ContainsKey(item))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_indices.Add(item, _items.Count);
|
||||||
|
_items.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
int IList.Add(object? value)
|
||||||
|
{
|
||||||
|
var item = RequireThing(value);
|
||||||
|
if (_indices.TryGetValue(item, out var index))
|
||||||
|
return index;
|
||||||
|
Add(item);
|
||||||
|
return _items.Count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(T item)
|
||||||
|
{
|
||||||
|
if (item is null || !_indices.TryGetValue(item, out var index))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
RemoveAt(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IList.Remove(object? value)
|
||||||
|
{
|
||||||
|
if (value is T item)
|
||||||
|
Remove(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveAt(int index)
|
||||||
|
{
|
||||||
|
var item = _items[index];
|
||||||
|
var lastIndex = _items.Count - 1;
|
||||||
|
var lastItem = _items[lastIndex];
|
||||||
|
_indices.Remove(item);
|
||||||
|
if (index != lastIndex)
|
||||||
|
{
|
||||||
|
_items[index] = lastItem;
|
||||||
|
_indices[lastItem] = index;
|
||||||
|
}
|
||||||
|
_items.RemoveAt(lastIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Insert(int index, T item)
|
||||||
|
{
|
||||||
|
if (item is null)
|
||||||
|
throw new ArgumentNullException(nameof(item));
|
||||||
|
if ((uint)index > (uint)_items.Count)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
if (_indices.ContainsKey(item))
|
||||||
|
return;
|
||||||
|
if (index == _items.Count)
|
||||||
|
{
|
||||||
|
Add(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var displaced = _items[index];
|
||||||
|
_items.Add(displaced);
|
||||||
|
_items[index] = item;
|
||||||
|
_indices[displaced] = _items.Count - 1;
|
||||||
|
_indices.Add(item, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IList.Insert(int index, object? value) => Insert(index, RequireThing(value));
|
||||||
|
|
||||||
|
public bool Contains(T item) => item is not null && _indices.ContainsKey(item);
|
||||||
|
bool IList.Contains(object? value) => value is T item && Contains(item);
|
||||||
|
|
||||||
|
public int IndexOf(T item) => item is not null && _indices.TryGetValue(item, out var index) ? index : -1;
|
||||||
|
int IList.IndexOf(object? value) => value is T item ? IndexOf(item) : -1;
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_indices.Clear();
|
||||||
|
_items.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyTo(T[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
|
||||||
|
void ICollection.CopyTo(Array array, int index) => ((ICollection)_items).CopyTo(array, index);
|
||||||
|
|
||||||
|
public List<T>.Enumerator GetEnumerator() => _items.GetEnumerator();
|
||||||
|
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
|
private static T RequireThing(object? value)
|
||||||
|
=> value is T item ? item : throw new ArgumentException("Expected a thing of the cached type.", nameof(value));
|
||||||
|
|
||||||
|
private sealed class ReferenceComparer : IEqualityComparer<T>
|
||||||
|
{
|
||||||
|
public static readonly ReferenceComparer Instance = new();
|
||||||
|
public bool Equals(T? x, T? y) => ReferenceEquals(x, y);
|
||||||
|
public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,12 @@ public sealed class ThingsPrepatches : ClassWithFishPrepatches
|
|||||||
|
|
||||||
lock (__instance.LockObject())
|
lock (__instance.LockObject())
|
||||||
{
|
{
|
||||||
|
// Re-registering the same thing must not duplicate the def and group lists.
|
||||||
|
if (__instance.IndexMapByDef().ContainsKey(t.GetKey())
|
||||||
|
&& __instance.listsByDef.TryGetValue(t.def, out var existing)
|
||||||
|
&& existing.Contains(t))
|
||||||
|
return;
|
||||||
|
|
||||||
AddToDefList(__instance, t);
|
AddToDefList(__instance, t);
|
||||||
if (t is IHaulSource haulSource)
|
if (t is IHaulSource haulSource)
|
||||||
_haulSources(__instance).Add(haulSource);
|
_haulSources(__instance).Add(haulSource);
|
||||||
@@ -572,8 +578,12 @@ public sealed class Things : ClassWithFishPatches
|
|||||||
};
|
};
|
||||||
|
|
||||||
private static readonly MethodInfo _thingsListInitializerDefinition
|
private static readonly MethodInfo _thingsListInitializerDefinition
|
||||||
= methodof(InitializeThingsList<Thing>).GetGenericMethodDefinition();
|
= methodof(InitializeReferenceThingsList<Thing>).GetGenericMethodDefinition();
|
||||||
|
|
||||||
|
private static IndexedReferenceList<T> InitializeReferenceThingsList<T>() where T : Thing => new();
|
||||||
|
|
||||||
|
// Keep the public factory for mods that call it directly; the ListerThings type cache uses the
|
||||||
|
// reference-indexed implementation above.
|
||||||
public static unsafe IndexedFishSet<T> InitializeThingsList<T>() where T : Thing
|
public static unsafe IndexedFishSet<T> InitializeThingsList<T>() where T : Thing
|
||||||
#pragma warning disable CS8622
|
#pragma warning disable CS8622
|
||||||
=> new(static () => new(0,
|
=> new(static () => new(0,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
// 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/.
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
using System.Linq;
|
|
||||||
using Mono.Cecil;
|
using Mono.Cecil;
|
||||||
using Mono.Cecil.Cil;
|
using Mono.Cecil.Cil;
|
||||||
using PerformanceFish.Prepatching;
|
using PerformanceFish.Prepatching;
|
||||||
@@ -17,19 +16,12 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
|
|||||||
{
|
{
|
||||||
public override bool DefaultState => true;
|
public override bool DefaultState => true;
|
||||||
|
|
||||||
private static FishTable<Type, bool> _skippableCompTypes = null!;
|
|
||||||
private static FishTable<Type, bool> _skippableWorldObjectTypes = null!;
|
|
||||||
private static readonly List<IThingHolder> _tmpThingHolders = [];
|
|
||||||
private static readonly List<SkippedWorldObject> _skippedWorldObjects = [];
|
|
||||||
private static List<WorldObject>? _cachedWorldObjectsReference;
|
private static List<WorldObject>? _cachedWorldObjectsReference;
|
||||||
private static List<Map>? _cachedMapsReference;
|
private static List<WorldObject>? _cachedSnapshotReference;
|
||||||
|
private static int _cachedSnapshotVersion = -2;
|
||||||
private readonly record struct SkippedWorldObject(WorldObject WorldObject,
|
|
||||||
List<WorldObjectComp> Comps, int CompsVersion, bool HasEnterCooldownComp);
|
|
||||||
|
|
||||||
public override string? Description { get; }
|
public override string? Description { get; }
|
||||||
= "Caches the world objects that need ticking and skips objects with no active tick behavior. "
|
= "Reuses the world-object tick snapshot until the source list changes. Every object still receives DoTick.";
|
||||||
+ "Objects with changing contents or components are rechecked before each tick.";
|
|
||||||
|
|
||||||
public override MethodBase TargetMethodBase { get; }
|
public override MethodBase TargetMethodBase { get; }
|
||||||
= AccessTools.Method(typeof(WorldObjectsHolder), nameof(WorldObjectsHolder.WorldObjectsHolderTick));
|
= AccessTools.Method(typeof(WorldObjectsHolder), nameof(WorldObjectsHolder.WorldObjectsHolderTick));
|
||||||
@@ -53,206 +45,40 @@ public sealed class WorldObjectsOptimization : ClassWithFishPrepatches
|
|||||||
|
|
||||||
public static void UpdateCache(WorldObjectsHolder instance)
|
public static void UpdateCache(WorldObjectsHolder instance)
|
||||||
{
|
{
|
||||||
var staticWorldObjects = WorldObjectsHolder.tmpWorldObjects;
|
|
||||||
staticWorldObjects.Clear();
|
|
||||||
_skippedWorldObjects.Clear();
|
|
||||||
|
|
||||||
var instanceWorldObjects = instance.worldObjects;
|
var instanceWorldObjects = instance.worldObjects;
|
||||||
|
var snapshot = WorldObjectsHolder.tmpWorldObjects;
|
||||||
for (var i = 0; i < instanceWorldObjects.Count; i++)
|
snapshot.Clear();
|
||||||
{
|
snapshot.AddRange(instanceWorldObjects);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
CachedWorldObjectsVersion = instanceWorldObjects._version;
|
CachedWorldObjectsVersion = instanceWorldObjects._version;
|
||||||
CachedMapsVersion = Current.gameInt.maps._version;
|
|
||||||
_cachedWorldObjectsReference = instanceWorldObjects;
|
_cachedWorldObjectsReference = instanceWorldObjects;
|
||||||
_cachedMapsReference = Current.gameInt.maps;
|
_cachedSnapshotReference = snapshot;
|
||||||
}
|
_cachedSnapshotVersion = snapshot._version;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int
|
public static int
|
||||||
CachedWorldObjectsVersion = -2,
|
CachedWorldObjectsVersion = -2,
|
||||||
CachedMapsVersion = -2;
|
CachedMapsVersion = -2;
|
||||||
|
|
||||||
private static Type?[]
|
// Retained for mods that called the old whitelist API. The snapshot no longer skips any ticks.
|
||||||
_whitelistedTickingCompTypes =
|
public static void AddCompToWhiteList(Type compType) => SetDirty();
|
||||||
[
|
public static void AddWorldObjectToWhiteList(Type worldObjectType) => SetDirty();
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool CacheDirty(WorldObjectsHolder instance)
|
public static bool CacheDirty(WorldObjectsHolder instance)
|
||||||
{
|
{
|
||||||
if (!ReferenceEquals(_cachedWorldObjectsReference, instance.worldObjects)
|
var source = instance.worldObjects;
|
||||||
|| CachedWorldObjectsVersion != instance.worldObjects._version
|
var snapshot = WorldObjectsHolder.tmpWorldObjects;
|
||||||
|| !ReferenceEquals(_cachedMapsReference, Current.gameInt.maps)
|
return !ReferenceEquals(_cachedWorldObjectsReference, source)
|
||||||
|| CachedMapsVersion != Current.gameInt.maps._version)
|
|| CachedWorldObjectsVersion != source._version
|
||||||
return true;
|
|| !ReferenceEquals(_cachedSnapshotReference, snapshot)
|
||||||
|
|| _cachedSnapshotVersion != snapshot._version
|
||||||
for (var i = 0; i < _skippedWorldObjects.Count; i++)
|
|| snapshot.Count != source.Count;
|
||||||
{
|
|
||||||
var skipped = _skippedWorldObjects[i];
|
|
||||||
var worldObject = skipped.WorldObject;
|
|
||||||
var comps = worldObject.comps;
|
|
||||||
if (!ReferenceEquals(comps, skipped.Comps) || comps._version != skipped.CompsVersion
|
|
||||||
|| worldObject is MapParent { HasMap: true }
|
|
||||||
|| HasTickableContents(worldObject))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (skipped.HasEnterCooldownComp)
|
|
||||||
for (var j = 0; j < comps.Count; j++)
|
|
||||||
if (comps[j] is EnterCooldownComp { Active: true })
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetDirty() => CachedWorldObjectsVersion = CachedMapsVersion = -2;
|
public static void SetDirty() => CachedWorldObjectsVersion = CachedMapsVersion = -2;
|
||||||
|
|
||||||
static WorldObjectsHolderTickPatch()
|
static WorldObjectsHolderTickPatch()
|
||||||
{
|
{
|
||||||
ResetSkippableCompTypes();
|
|
||||||
ResetSkippableWorldObjectTypes();
|
|
||||||
Cache.Utility.Cleared += SetDirty;
|
Cache.Utility.Cleared += SetDirty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -522,10 +522,13 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
{
|
{
|
||||||
public sealed class GetCustomAttributes : FishPatch
|
public sealed class GetCustomAttributes : FishPatch
|
||||||
{
|
{
|
||||||
|
private static readonly Type RuntimeTypeType = typeof(object).GetType();
|
||||||
|
|
||||||
public override bool DefaultState => true;
|
public override bool DefaultState => true;
|
||||||
|
|
||||||
public override string? Description { get; }
|
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; }
|
public override Delegate? TargetMethodGroup { get; }
|
||||||
= (Func<ICustomAttributeProvider, Type, bool, object[]>)global::System.MonoCustomAttrs
|
= (Func<ICustomAttributeProvider, Type, bool, object[]>)global::System.MonoCustomAttrs
|
||||||
@@ -535,7 +538,8 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
public static bool Prefix(ICustomAttributeProvider obj, Type attributeType, bool inherit,
|
public static bool Prefix(ICustomAttributeProvider obj, Type attributeType, bool inherit,
|
||||||
out object[]? __result, out CustomAttributeState __state)
|
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;
|
__state = default;
|
||||||
__result = null;
|
__result = null;
|
||||||
@@ -568,10 +572,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
{
|
{
|
||||||
var localLookup = TryUseCache(CustomAttributeCache.Get, ref key, out result);
|
var localLookup = TryUseCache(CustomAttributeCache.Get, ref key, out result);
|
||||||
if (localLookup == CustomAttributeCacheLookup.Hit)
|
if (localLookup == CustomAttributeCacheLookup.Hit)
|
||||||
{
|
|
||||||
result = CopyCachedAttributes(result!);
|
|
||||||
return CustomAttributeCacheLookup.Hit;
|
return CustomAttributeCacheLookup.Hit;
|
||||||
}
|
|
||||||
|
|
||||||
if (localLookup == CustomAttributeCacheLookup.InProgress)
|
if (localLookup == CustomAttributeCacheLookup.InProgress)
|
||||||
return localLookup;
|
return localLookup;
|
||||||
@@ -581,7 +582,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
var centralLookup = TryUseCache(CustomAttributeCache.GetDirectly, ref key, out var centralResult);
|
var centralLookup = TryUseCache(CustomAttributeCache.GetDirectly, ref key, out var centralResult);
|
||||||
if (centralLookup == CustomAttributeCacheLookup.Hit)
|
if (centralLookup == CustomAttributeCacheLookup.Hit)
|
||||||
{
|
{
|
||||||
result = CopyCachedAttributes(centralResult!);
|
result = centralResult;
|
||||||
CustomAttributeCache.Get[key] = new()
|
CustomAttributeCache.Get[key] = new()
|
||||||
{
|
{
|
||||||
Attributes = centralResult,
|
Attributes = centralResult,
|
||||||
@@ -605,7 +606,10 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
if (!__state.ShouldStore || __result is null)
|
if (!__state.ShouldStore || __result is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (__result.Length == 0)
|
||||||
StoreCompleted(ref __state.Key, __result);
|
StoreCompleted(ref __state.Key, __result);
|
||||||
|
else
|
||||||
|
ClearInProgress(ref __state.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
@@ -663,9 +667,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
{
|
{
|
||||||
var value = new CustomAttributeCacheValue
|
var value = new CustomAttributeCacheValue
|
||||||
{
|
{
|
||||||
Attributes = result.Length == 0
|
Attributes = result,
|
||||||
? (object[])Array.CreateInstance(Type.GetTypeFromHandle(key.Second), 0)
|
|
||||||
: CopyCachedAttributes(result),
|
|
||||||
Status = CustomAttributeCacheStatus.Completed
|
Status = CustomAttributeCacheStatus.Completed
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -700,9 +702,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
private static bool HasFixedAttributeMetadata(ICustomAttributeProvider provider)
|
||||||
private static object[] CopyCachedAttributes(object[] attributes)
|
=> provider.GetType().Assembly == typeof(object).Assembly && (provider switch
|
||||||
=> attributes.Length != 0 ? (object[])attributes.Clone() : attributes;
|
{
|
||||||
|
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();
|
private static object _lock = new();
|
||||||
}
|
}
|
||||||
@@ -1008,12 +1017,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
|
|
||||||
public sealed class FieldInfoPatches
|
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 sealed class GetValue_Patch : FishPatch
|
||||||
{
|
{
|
||||||
public override bool DefaultState => true;
|
public override bool DefaultState => true;
|
||||||
|
|
||||||
public override string Description { get; }
|
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()
|
//public GetValue_Patch()
|
||||||
//{
|
//{
|
||||||
@@ -1033,7 +1046,12 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static bool Prefix(FieldInfo __instance, object? obj, ref object? __result)
|
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);
|
ref var cache = ref FieldGetters.GetOrAddReference(__instance.FieldHandle);
|
||||||
|
if (!CanUseFieldFastPath(__instance, obj, ref cache.FastPath))
|
||||||
|
return true;
|
||||||
|
|
||||||
if (cache.Dirty
|
if (cache.Dirty
|
||||||
&& !FieldGetters.UpdateAsyncCache<FieldInfoGetterCache, FieldInfo, FieldGetter>(ref cache,
|
&& !FieldGetters.UpdateAsyncCache<FieldInfoGetterCache, FieldInfo, FieldGetter>(ref cache,
|
||||||
@@ -1042,43 +1060,17 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsupported fields fall back to Mono's implementation, including its exception behavior.
|
|
||||||
ValidateGetValueCall(__instance, obj);
|
|
||||||
__result = cache.Result!(obj);
|
__result = cache.Result!(obj);
|
||||||
return false;
|
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 sealed class SetValue_Patch : FishPatch
|
||||||
{
|
{
|
||||||
public override bool DefaultState => false;
|
public override bool DefaultState => true;
|
||||||
|
|
||||||
public override string Description { get; }
|
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; }
|
public override MethodBase? TargetMethodInfo { get; }
|
||||||
= AccessTools.DeclaredMethod(typeof(FieldInfo), nameof(FieldInfo.SetValue),
|
= AccessTools.DeclaredMethod(typeof(FieldInfo), nameof(FieldInfo.SetValue),
|
||||||
@@ -1087,7 +1079,13 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static bool Prefix(FieldInfo __instance, object? obj, object? value)
|
public static bool Prefix(FieldInfo __instance, object? obj, object? value)
|
||||||
{
|
{
|
||||||
|
if (__instance.GetType() != RuntimeFieldInfoType)
|
||||||
|
return true;
|
||||||
|
|
||||||
ref var cache = ref FieldSetters.GetOrAddReference(__instance.FieldHandle);
|
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))
|
if (cache.Dirty && !TryUpdate(__instance, ref cache))
|
||||||
return true;
|
return true;
|
||||||
@@ -1099,29 +1097,67 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
private static bool TryUpdate(FieldInfo __instance, ref FieldInfoSetterCache cache)
|
private static bool TryUpdate(FieldInfo __instance, ref FieldInfoSetterCache cache)
|
||||||
{
|
{
|
||||||
if (__instance.IsConst())
|
|
||||||
return ErrorForInvalidCallAttempt(__instance, ref cache);
|
|
||||||
|
|
||||||
return FieldSetters.UpdateAsyncCache<FieldInfoSetterCache, FieldInfo, FieldSetter>(ref cache,
|
return FieldSetters.UpdateAsyncCache<FieldInfoSetterCache, FieldInfo, FieldSetter>(ref cache,
|
||||||
__instance.FieldHandle, __instance);
|
__instance.FieldHandle, __instance);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
private static bool ErrorForInvalidCallAttempt(FieldInfo info, ref FieldInfoSetterCache cache)
|
private static void InitializeFieldFastPath(FieldInfo info, ref FieldFastPath state, bool forSetter)
|
||||||
{
|
{
|
||||||
if (cache.Task == _fieldSetterFailureTask)
|
var declaringType = info.DeclaringType;
|
||||||
return false;
|
var fieldType = info.FieldType;
|
||||||
|
state.DeclaringType = declaringType;
|
||||||
cache.Task = _fieldSetterFailureTask;
|
state.FieldType = fieldType;
|
||||||
Log.Error($"FieldInfo.SetValue has been called on '{
|
state.IsStatic = info.IsStatic;
|
||||||
info.FullDescription()}'. This is a const that gets copied at compile time. An attempt at "
|
state.IsValueType = fieldType.IsValueType;
|
||||||
+ $"setting a value will always fail.\n{new StackTrace(2, true)}");
|
// Constructed generic fields can share a RuntimeFieldHandle but need different delegates.
|
||||||
|
state.Unsupported = declaringType is null || declaringType.IsGenericType
|
||||||
return false;
|
|| 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;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<FieldSetter> _fieldSetterFailureTask = new(static () => null!); // sentinel only
|
|
||||||
|
|
||||||
public static FieldGetter? MakeGetterDelegate(FieldInfo info)
|
public static FieldGetter? MakeGetterDelegate(FieldInfo info)
|
||||||
{
|
{
|
||||||
@@ -1206,6 +1242,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
{
|
{
|
||||||
public FieldGetter? Result { get; set; }
|
public FieldGetter? Result { get; set; }
|
||||||
public Task<FieldGetter>? Task { get; set; }
|
public Task<FieldGetter>? Task { get; set; }
|
||||||
|
public FieldFastPath FastPath;
|
||||||
|
|
||||||
public bool Dirty => Result == null;
|
public bool Dirty => Result == null;
|
||||||
|
|
||||||
@@ -1217,6 +1254,7 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
{
|
{
|
||||||
public FieldSetter? Result { get; set; }
|
public FieldSetter? Result { get; set; }
|
||||||
public Task<FieldSetter>? Task { get; set; }
|
public Task<FieldSetter>? Task { get; set; }
|
||||||
|
public FieldFastPath FastPath;
|
||||||
|
|
||||||
public bool Dirty => Result == null;
|
public bool Dirty => Result == null;
|
||||||
|
|
||||||
@@ -1232,13 +1270,16 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
|
|
||||||
public sealed class MethodBasePatches
|
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 sealed class Invoke_Patch : FishPatch
|
||||||
{
|
{
|
||||||
public override bool DefaultState => false; // TODO: fix
|
public override bool DefaultState => true;
|
||||||
|
|
||||||
public override string Description { get; }
|
public override string Description { get; }
|
||||||
= "Optimizes the Invoke method by compiling specialized cached delegates. Disabled by default due to "
|
= "Uses cached delegates for static, parameterless methods with fixed metadata. Other calls use Mono reflection.";
|
||||||
+ "issues with various mods, like RIMMSqol and Performance Optimizer";
|
|
||||||
|
|
||||||
public override MethodBase? TargetMethodInfo { get; }
|
public override MethodBase? TargetMethodInfo { get; }
|
||||||
= AccessTools.DeclaredMethod(typeof(MethodBase), nameof(MethodBase.Invoke),
|
= AccessTools.DeclaredMethod(typeof(MethodBase), nameof(MethodBase.Invoke),
|
||||||
@@ -1247,22 +1288,21 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static bool Prefix(MethodBase __instance, object? obj, object[]? parameters, ref object? __result)
|
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
|
ref var cache = ref MethodInvokers.GetOrAddReference(__instance.MethodHandle);
|
||||||
// {
|
if (cache.Unsupported)
|
||||||
// if (OnStartup.State.Initialized)
|
return true;
|
||||||
// {
|
|
||||||
// Log.Message($"Called Invoke for {__instance.FullDescription()} with {obj.ToStringSafe()}, {
|
if (!cache.EligibilityChecked)
|
||||||
// parameters?.Length.ToStringSafe() ?? "null"} parameters");
|
{
|
||||||
// }
|
cache.EligibilityChecked = true;
|
||||||
//
|
cache.Unsupported = !CanCacheInvoke(methodInfo);
|
||||||
// if (!MethodInvokers.UpdateAsyncCache<MethodInvokerCache, MethodBase, MethodInvoker>(ref cache,
|
if (cache.Unsupported)
|
||||||
// __instance.MethodHandle, __instance))
|
return true;
|
||||||
// {
|
}
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (cache.Dirty
|
if (cache.Dirty
|
||||||
&& !MethodInvokers.UpdateAsyncCache<MethodInvokerCache, MethodBase, MethodInvoker>(ref cache,
|
&& !MethodInvokers.UpdateAsyncCache<MethodInvokerCache, MethodBase, MethodInvoker>(ref cache,
|
||||||
@@ -1271,58 +1311,31 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
__result = cache.Result!(obj, parameters);
|
__result = cache.Result!(obj, parameters);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new TargetInvocationException(ex);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Similar to HarmonyLib.MethodInvoker, but correctly handles a couple of edge cases Harmony would throw on.
|
// Restrict the delegate to a method whose reflection behavior can be reproduced cheaply.
|
||||||
[SuppressMessage("Globalization", "CA1310")]
|
|
||||||
public static MethodInvoker? MakeInvokeDelegate(MethodBase methodBase)
|
public static MethodInvoker? MakeInvokeDelegate(MethodBase methodBase)
|
||||||
{
|
{
|
||||||
if (methodBase.DeclaringType is null && methodBase.Name.StartsWith("PadMethod"))
|
if (methodBase is not MethodInfo methodInfo || !CanCacheInvoke(methodInfo))
|
||||||
return null; // padding methods generated by harmony for linux
|
return null;
|
||||||
|
|
||||||
try
|
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),
|
var dm = new DynamicMethod($"MethodInvoker_{methodBase.Name}", typeof(object),
|
||||||
[typeof(object), typeof(object[])],
|
[typeof(object), typeof(object[])],
|
||||||
GetOwnerType(methodBase), true);
|
GetOwnerType(methodBase), true);
|
||||||
var il = dm.GetILGenerator();
|
var il = dm.GetILGenerator();
|
||||||
|
EmitInvokeMethodCall(methodInfo, il);
|
||||||
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));
|
|
||||||
|
|
||||||
il.Emit(FishTranspiler.Return);
|
il.Emit(FishTranspiler.Return);
|
||||||
|
|
||||||
@@ -1337,109 +1350,45 @@ public sealed class ReflectionCaching : ClassWithFishPatches
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool CanCacheInvoke(MethodBase methodBase, MethodInfo? methodInfo,
|
private static bool CanCacheInvoke(MethodInfo methodInfo)
|
||||||
ConstructorInfo? constructorInfo, ParameterInfo[] parameters)
|
|
||||||
{
|
{
|
||||||
if (methodBase.ContainsGenericParameters
|
try
|
||||||
|| (methodBase.CallingConvention & CallingConventions.VarArgs) != 0)
|
{
|
||||||
|
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;
|
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++)
|
private static void EmitInvokeMethodCall(MethodInfo methodInfo, ILGenerator il)
|
||||||
{
|
{
|
||||||
if (!IsSupportedInvokeParameter(parameters[i].ParameterType))
|
il.Emit(FishTranspiler.Call(methodInfo));
|
||||||
return false;
|
if (methodInfo.ReturnType == typeof(void))
|
||||||
}
|
il.Emit(FishTranspiler.Null);
|
||||||
|
else if (methodInfo.ReturnType.IsValueType)
|
||||||
return true;
|
il.Emit(FishTranspiler.Box(methodInfo.ReturnType));
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
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<object>());
|
|
||||||
|
|
||||||
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<object>());
|
|
||||||
|
|
||||||
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]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record struct MethodInvokerCache : IAsyncCacheable<RuntimeMethodHandle, MethodBase, MethodInvoker>
|
public record struct MethodInvokerCache : IAsyncCacheable<RuntimeMethodHandle, MethodBase, MethodInvoker>
|
||||||
{
|
{
|
||||||
public MethodInvoker? Result { get; set; }
|
public MethodInvoker? Result { get; set; }
|
||||||
public Task<MethodInvoker>? Task { get; set; }
|
public Task<MethodInvoker>? Task { get; set; }
|
||||||
|
public bool EligibilityChecked;
|
||||||
|
public bool Unsupported;
|
||||||
public bool Dirty => Result == null;
|
public bool Dirty => Result == null;
|
||||||
|
|
||||||
public ValueTask<MethodInvoker?> MakeResultAsync(RuntimeMethodHandle key, MethodBase method)
|
public ValueTask<MethodInvoker?> MakeResultAsync(RuntimeMethodHandle key, MethodBase method)
|
||||||
|
|||||||
Reference in New Issue
Block a user