Files
Performance-Fish/Source/PerformanceFish/RoomOptimizations.cs
T
bbradson be3dae46bb Added stat caching, fixed all the bugs
Added RitualObligationTriggers analyzer category,
Fixed defs from other mods with broken market value breaking fish's storage patches,
Fixed storage caches not updating for quality, rotting and similar changes to stored things,
Fixed storage caches not updating capacity when reinstalling storage,
Fixed prison labor compatibility,
Improved multiplayer compatibility,
Fixed ideology style dominance and altar sharing patches throwing errors instead of defaulting to disabled when not having ideology,
Fixed compatibility with RIMMSqol's world pawn GC patch by disabling that patch,
Fixed rare threading issues throwing errors on startup
2024-03-10 20:05:24 +01:00

401 lines
12 KiB
C#

// Copyright (c) 2023 bradson
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
using System.Linq;
using System.Reflection.Emit;
using RoomOwnersCache
= PerformanceFish.Cache.ByInt<Verse.Room, PerformanceFish.RoomOptimizations.Owners_Patch.CacheValue>;
using RoomRoleCache
= PerformanceFish.Cache.ByInt<Verse.Room, PerformanceFish.RoomOptimizations.Role_Patch.CacheValue>;
using ContainedAndAdjacentThingsCache
= PerformanceFish.Cache.ByInt<Verse.Room,
PerformanceFish.RoomOptimizations.ContainedAndAdjacentThings_Patch.CacheValue>;
using OpCodes = System.Reflection.Emit.OpCodes;
namespace PerformanceFish;
public sealed class RoomOptimizations : ClassWithFishPatches, IHasDescription
{
public string Description { get; } = "Room stats related optimizations";
public sealed class Role_Patch : FirstPriorityFishPatch
{
public override string Description { get; }
= "Throttles room stats and role updating to happen at most once every 512 or so ticks per room, "
+ "instead of every time anything at all changes or moves";
public override MethodBase TargetMethodInfo { get; }
= AccessTools.PropertyGetter(typeof(Room), nameof(Room.Role));
public static CodeInstructions Transpiler(CodeInstructions instructions)
{
var this_statsAndRoleDirty = FishTranspiler.Field(typeof(Room), nameof(Room.statsAndRoleDirty));
try
{
return instructions.ReplaceAt((codes, i)
=> i - 1 > 0
&& codes[i - 1] == this_statsAndRoleDirty
&& codes[i].opcode == OpCodes.Brfalse_S
&& codes[i].operand is Label, static code =>
[
code,
FishTranspiler.This,
FishTranspiler.Call(ShouldUpdate),
FishTranspiler.IfFalse_Short((Label)code.operand)
]);
}
catch (Exception ex)
{
Log.Error("Performance Fish failed to patch Room.Role. This just means that specific patch "
+ $"will be doing nothing.\n{ex}");
return null!;
}
}
public static bool ShouldUpdate(Room __instance)
{
if (__instance.role is not null
|| !RoomRoleCache.GetOrAddReference(__instance.ID).Dirty)
{
return false;
}
UpdateCache(__instance);
return true;
}
private static void UpdateCache(Room __instance)
=> RoomRoleCache.GetOrAddReference(__instance.ID).SetDirty(__instance);
public record struct CacheValue()
{
private int _nextUpdateTick = -2;
public bool Dirty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => TickHelper.Past(_nextUpdateTick);
}
public void SetDirty(Room room)
=> _nextUpdateTick = TickHelper.Add(384, room.ID, 256);
}
}
public sealed class Owners_Patch : FirstPriorityFishPatch
{
public override string Description { get; }
= "Caches room owner data and throttles them to only update at most once every ~512 or so ticks. "
+ "RimWorld calculates owners by checking ownership of all items contained in a room, which can get "
+ "quite expensive";
public override MethodBase TargetMethodInfo { get; }
= AccessTools.PropertyGetter(typeof(Room), nameof(Room.Owners));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Prefix(Room __instance, ref IEnumerable<Pawn> __result, out bool __state)
{
ref var cache = ref RoomOwnersCache.GetOrAddReference(__instance.ID);
if (cache.Dirty)
{
if (CanCache(__instance))
return __state = true;
else
cache.Update(__instance, null);
}
__result = cache.Owners;
return __state = false;
}
private static bool CanCache(Room __instance)
=> __instance is { TouchesMapEdge: false, IsHuge: false }
&& (!__instance.statsAndRoleDirty || __instance.ContainedBeds.Any());
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Postfix(Room __instance, bool __state, ref IEnumerable<Pawn> __result)
{
if (!__state)
return;
UpdateCache(__instance, ref __result);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void UpdateCache(Room __instance, ref IEnumerable<Pawn> __result)
{
ref var cache = ref RoomOwnersCache.GetOrAddReference(__instance.ID);
cache.Update(__instance, __result);
__result = cache.Owners;
}
public record struct CacheValue()
{
private int _nextRefreshTick = -2;
public readonly List<Pawn> Owners = [];
public void Update(Room room, IEnumerable<Pawn>? result)
{
Owners.Clear();
if (result != null)
Owners.AddRange(result);
_nextRefreshTick = TickHelper.Add(384, room.ID, 256);
}
public bool Dirty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => TickHelper.Past(_nextRefreshTick);
}
}
}
public sealed class Regions_Patch : FirstPriorityFishPatch
{
public override string Description { get; } = "Minor optimization";
public override MethodBase TargetMethodInfo { get; }
= AccessTools.PropertyGetter(typeof(Room), nameof(Room.Regions));
public static CodeInstructions Transpiler(CodeInstructions codeInstructions)
=> Reflection.MakeReplacementCall(Regions_Replacement);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static List<Region> Regions_Replacement(Room instance)
{
var tmpRegions = instance.tmpRegions;
var districts = instance.districts;
tmpRegions.Clear();
for (var i = 0; i < districts.Count; i++)
tmpRegions.AddRangeFast(districts[i].Regions);
return tmpRegions;
}
}
public sealed class ContainedBeds_Patch : FirstPriorityFishPatch
{
public override string Description { get; }
= "So RimWorld has a list of beds and a list of all things. For some reason Ludeon chose to have "
+ "ContainedBeds retrieve its beds by looping through all things. This fixes that. Helps a lot "
+ "against spikes";
public override MethodBase TargetMethodInfo { get; }
= AccessTools.PropertyGetter(typeof(Room), nameof(Room.ContainedBeds));
public static CodeInstructions Transpiler(CodeInstructions codeInstructions, ILGenerator generator)
=> Reflection.GetCodeInstructions(ContainedBeds_Replacement, generator);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEnumerable<Building_Bed> ContainedBeds_Replacement(Room instance)
{
var uniqueContainedBeds = instance.UniqueContainedBeds();
uniqueContainedBeds.Clear();
var regions = instance.Regions;
for (var i = regions.Count; i-- > 0;)
{
var beds = regions[i].ListerThings.listsByGroup[(int)ThingRequestGroup.Bed];
if (beds is null)
continue;
for (var j = beds.Count; j-- > 0;)
uniqueContainedBeds.Add((Building_Bed)beds[j]);
}
return uniqueContainedBeds;
}
}
public sealed class ContainedAndAdjacentThings_Patch : FirstPriorityFishPatch
{
public override string Description { get; } = "Caching of room content info. Variable impact";
public override MethodBase TargetMethodInfo { get; }
= AccessTools.PropertyGetter(typeof(Room), nameof(Room.ContainedAndAdjacentThings));
public static CodeInstructions Transpiler(CodeInstructions codeInstructions)
=> Reflection.MakeReplacementCall(ContainedAndAdjacentThings_Replacement);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static List<Thing> ContainedAndAdjacentThings_Replacement(Room instance)
{
instance.uniqueContainedThings.Clear();
var regions = instance.Regions;
ref var cache = ref ContainedAndAdjacentThingsCache.GetOrAddReference(instance.ID);
if (cache.ListVersions.Count == regions.Count && !cache.Dirty)
{
var cacheIsAccurate = true;
for (var i = 0; i < regions.Count; i++)
{
var allThings = regions[i].listerThings.AllThings;
if (allThings == null)
{
if (cache.ListVersions[i] != -1)
cacheIsAccurate = false;
}
else if (cache.ListVersions[i] != allThings._version)
{
cacheIsAccurate = false;
}
}
if (cacheIsAccurate)
{
instance.uniqueContainedThings.AddRangeFast(cache.Things);
return instance.uniqueContainedThings;
}
}
return RefreshContainedAndAdjacentThings(instance, regions, ref cache);
}
private static List<Thing> RefreshContainedAndAdjacentThings(Room instance, List<Region> regions,
ref CacheValue cache)
{
var uniqueContainedThings = instance.uniqueContainedThings;
var uniqueContainedThingsSet = instance.uniqueContainedThingsSet;
uniqueContainedThingsSet.Clear();
cache.ListVersions.Clear();
for (var i = 0; i < regions.Count; i++)
{
var allThings = regions[i].ListerThings.AllThings;
if (allThings == null)
{
cache.ListVersions.Add(-1);
continue;
}
var count = allThings.Count;
for (var j = 0; j < count; j++)
uniqueContainedThingsSet.Add(allThings[j]);
cache.ListVersions.Add(allThings.Version());
}
uniqueContainedThings.AddRange(uniqueContainedThingsSet);
if (cache.Things is { } list)
list.ReplaceContentsWith(uniqueContainedThings);
else
cache.Things = [..uniqueContainedThings];
cache.SetDirty(false, instance);
uniqueContainedThingsSet.Clear();
return uniqueContainedThings;
}
/*private static List<Thing> RefreshContainedAndAdjacentThingsAlternative(Room instance, List<Region> regions,
FishThingsInfo cache)
{
var uniqueContainedThings = instance.uniqueContainedThings;
(cache.listVersions ??= new()).Clear();
var map = instance.Map;
var regionGrid = map.regionGrid.regionGrid;
var mapSizeX = map.cellIndices.mapSizeX;
var adjacentThings = new List<Thing>();
for (var i = 0; i < regions.Count; i++)
{
var region = regions[i];
var allThings = region.ListerThings.AllThings;
if (allThings == null)
{
cache.listVersions.Add(-1);
continue;
}
var count = allThings.Count;
for (var j = 0; j < count; j++)
{
if (regionGrid[allThings[j].Position.CellToIndex(mapSizeX)] == region)
uniqueContainedThings.Add(allThings[j]);
else
adjacentThings.Add(allThings[j]);
}
cache.listVersions.Add(allThings._version);
}
for (var i = 0; i < adjacentThings.Count; i++)
{
if (!uniqueContainedThings.Contains(adjacentThings[i]))
uniqueContainedThings.Add(adjacentThings[i]);
}
if (cache.things is List<Thing> list)
list.ReplaceContentsWith(uniqueContainedThings);
else
cache.things = new List<Thing>(uniqueContainedThings);
cache.ShouldRefreshNow = false;
CachedThingLists[instance] = cache;
return uniqueContainedThings;
}*/
public record struct CacheValue()
{
private int _nextRefreshTick = -2;
public List<int> ListVersions = [];
public List<Thing> Things = [];
public void SetDirty(bool value, Room room)
=> _nextRefreshTick = value ? 0 : TickHelper.Add(3072, room.ID, 2048);
public bool Dirty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => TickHelper.Past(_nextRefreshTick);
}
}
}
public sealed class RoomAt_Patch : FirstPriorityFishPatch
{
public override string? Description { get; }
= "Minor optimization through inlining ond cleaning up of duplicate calls in the method. About 1/4 "
+ "faster, which isn't much, but it likely won't break anything either.";
public override Delegate TargetMethodGroup { get; } = RegionAndRoomQuery.RoomAt;
public static CodeInstructions Transpiler(CodeInstructions codes)
=> Reflection.MakeReplacementCall(RoomAt);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Room? RoomAt(IntVec3 c, Map map, RegionType allowedRegionTypes = RegionType.Set_All)
{
var cellIndices = map.cellIndices;
if (((uint)c.x >= (uint)cellIndices.mapSizeX) | ((uint)c.z >= (uint)cellIndices.mapSizeZ))
return null;
var regionAndRoomUpdater = map.regionAndRoomUpdater;
if (regionAndRoomUpdater.Enabled)
{
regionAndRoomUpdater.TryRebuildDirtyRegionsAndRooms();
}
else if (regionAndRoomUpdater.AnythingToRebuild)
{
Verse.Log.Warning($"Trying to get valid region at {
c.ToString()} but RegionAndRoomUpdater is disabled. The result may be incorrect.");
}
var region = map.regionGrid.regionGrid[(c.z * cellIndices.mapSizeX) + c.x];
return region is null || !region.valid || (region.type & allowedRegionTypes) == 0
? null
: region.District?.Room;
}
}
}