// 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.Diagnostics; using System.Linq; using System.Reflection.Emit; using Mono.Cecil; using Mono.Cecil.Cil; using PerformanceFish.Prepatching; using RoomOwnersCache = PerformanceFish.Cache.ByInt; using RoomRoleCache = PerformanceFish.Cache.ByInt; using ContainedAndAdjacentThingsCache = PerformanceFish.Cache.ByInt; 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.DeclaredPropertyGetter(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!; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] 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.DeclaredPropertyGetter(typeof(Room), nameof(Room.Owners)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(Room __instance, ref IEnumerable __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; } [MethodImpl(MethodImplOptions.NoInlining)] public 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 __result) { if (!__state) return; UpdateCache(__instance, ref __result); } [MethodImpl(MethodImplOptions.NoInlining)] private static void UpdateCache(Room __instance, ref IEnumerable __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 Owners = []; public void Update(Room room, IEnumerable? 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 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.DeclaredPropertyGetter(typeof(Room), nameof(Room.ContainedBeds)); public static CodeInstructions Transpiler(CodeInstructions codeInstructions, ILGenerator generator) => Reflection.GetCodeInstructions(ContainedBeds_Replacement, generator); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IEnumerable 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.DeclaredPropertyGetter(typeof(Room), nameof(Room.ContainedAndAdjacentThings)); public static CodeInstructions Transpiler(CodeInstructions codeInstructions) => Reflection.MakeReplacementCall(ContainedAndAdjacentThings_Replacement); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static List 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 = regions.Count; i-- > 0;) { if (cache.ListVersions[i] != (regions[i]?.listerThings.AllThings?._version ?? -1)) cacheIsAccurate = false; } if (cacheIsAccurate) { instance.uniqueContainedThings.AddRangeFast(cache.Things); return instance.uniqueContainedThings; } } return RefreshContainedAndAdjacentThings(instance, regions, ref cache); } [MethodImpl(MethodImplOptions.NoInlining)] private static List RefreshContainedAndAdjacentThings(Room instance, List 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++) { if (regions[i]?.ListerThings.AllThings is not { } allThings) { 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); cache.Things.ReplaceContentsWith(uniqueContainedThings); cache.SetDirty(false, instance); uniqueContainedThingsSet.Clear(); return uniqueContainedThings; } /*[MethodImpl(MethodImplOptions.NoInlining)] private static List RefreshContainedAndAdjacentThingsAlternative(Room instance, List regions, ref CacheValue cache) { var uniqueContainedThings = instance.uniqueContainedThings; cache.ListVersions.Clear(); var map = instance.Map; var regionGrid = map.regionGrid.regionGrid; var mapSizeX = map.cellIndices.mapSizeX; using var pooledAdjacentThings = new Pools.PooledIList>(); var adjacentThings = pooledAdjacentThings.List; var regionCount = regions.Count; using var pooledRegionIDs = new PooledArray(regionCount); var regionIDs = pooledRegionIDs.BackingArray; for (var i = regionCount; i-- > 0;) regionIDs[i] = regions[i].id; for (var i = 0; i < regionCount; i++) { var region = regions[i]; if (region.ListerThings.AllThings is not { } allThings) { cache.ListVersions.Add(-1); continue; } var count = allThings.Count; for (var j = 0; j < count; j++) { var thing = allThings[j]; var thingRegionId = regionGrid[thing.Position.CellToIndex(mapSizeX)].id; if (thingRegionId == region.id) uniqueContainedThings.Add(thing); else if (!regionIDs.Contains(thingRegionId) && !adjacentThings.Contains(thing)) adjacentThings.Add(thing); } cache.ListVersions.Add(allThings._version); } for (var i = adjacentThings.Count; i-- > 0;) uniqueContainedThings.Add(adjacentThings[i]); cache.Things.ReplaceContentsWith(uniqueContainedThings); cache.SetDirty(false, instance); return uniqueContainedThings; }*/ public record struct CacheValue() { private int _nextRefreshTick = -2; public readonly List ListVersions = []; public readonly List 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) { if (!c.InBounds(map)) return null; var regionAndRoomUpdater = map.regionAndRoomUpdater; if (regionAndRoomUpdater.Enabled) regionAndRoomUpdater.TryRebuildDirtyRegionsAndRooms(); else if (regionAndRoomUpdater.AnythingToRebuild) LogIncorrectResultWarning(c); var region = map.regionGrid.GetValidRegionAt_NoRebuild(c); return region is null || (region.type & allowedRegionTypes) == 0 ? null : region.District?.Room; } [MethodImpl(MethodImplOptions.NoInlining)] public static void LogIncorrectResultWarning(IntVec3 c) => Log.Warning($"Trying to get valid region at { c.ToString()} but RegionAndRoomUpdater is disabled. The result may be incorrect.\nThis commonly " + $"happens when incorrectly accessing a region within a method that itself gets accessed by the " + $"RegionAndRoomUpdater when computing the region grid. The updater gets disabled to avoid a stack " + $"overflow.\n{new StackTrace(1, true)}"); } } public sealed class RoomPrepatches : ClassWithFishPrepatches { public sealed class Regions_Patch : FishPrepatch { public override string Description { get; } = "Minor optimization by simplifying instructions"; public override MethodBase TargetMethodBase { get; } = AccessTools.DeclaredPropertyGetter(typeof(Room), nameof(Room.Regions)); public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) => ilProcessor.ReplaceBodyWith(Regions_Replacement); public static List 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; } } }