From 06e4a2e0109228db4bf27dffac3895871afebc86 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Fri, 19 Dec 2025 19:05:36 +0000 Subject: [PATCH] Reduce allocations in main game loop. Target a couple of hot paths: - In Actor, cache the renderables enumerable by tracking the last used WorldRenderer. It's slightly uncouth but in practise the WorldRenderer will be the same for the lifetime of the Actor and outlive it anyway. - In RenderSprites, cache the renderables enumerable. If we refresh the palettes directly, we don't need the WorldRenderer for anything else so can return a cached enumerable. This could result in iterating the animation list twice, so use a flag to only do it when required, which is rare. - In AutoTarget, the compiler has an unfortunate behaviour where it allocates the helper class for a capturing lambda at the top of the loop, but it's on a rare path and so this allocation is usually unused. We can discourage the compiler from allocating until it's actually needed by wrapping the call in a local function. --- OpenRA.Game/Actor.cs | 16 ++++++++-- OpenRA.Mods.Common/Traits/AutoTarget.cs | 14 +++++++-- .../Traits/Render/RenderSprites.cs | 29 +++++++++++++++---- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/OpenRA.Game/Actor.cs b/OpenRA.Game/Actor.cs index 8d1fe13d23..982b441d3c 100644 --- a/OpenRA.Game/Actor.cs +++ b/OpenRA.Game/Actor.cs @@ -123,6 +123,9 @@ namespace OpenRA readonly IEnumerable enabledTargetableWorldPositions; bool created; + IEnumerable renderables; + WorldRenderer lastWorldRenderer; + internal Actor(World world, string name, TypeDictionary initDict) { var duplicateInit = initDict.WithInterface().GroupBy(i => i.GetType()) @@ -287,11 +290,18 @@ namespace OpenRA public IEnumerable Render(WorldRenderer wr) { + if (lastWorldRenderer != wr) + { + // PERF: Cache the enumerable to reduce allocations. + lastWorldRenderer = wr; + renderables = Renderables(wr); + } + // PERF: Avoid LINQ. - var renderables = Renderables(wr); + var modifiedRenderables = renderables; foreach (var modifier in renderModifiers) - renderables = modifier.ModifyRender(this, wr, renderables); - return renderables; + modifiedRenderables = modifier.ModifyRender(this, wr, modifiedRenderables); + return modifiedRenderables; } IEnumerable Renderables(WorldRenderer wr) diff --git a/OpenRA.Mods.Common/Traits/AutoTarget.cs b/OpenRA.Mods.Common/Traits/AutoTarget.cs index 6e7d5e108c..abcbb04985 100644 --- a/OpenRA.Mods.Common/Traits/AutoTarget.cs +++ b/OpenRA.Mods.Common/Traits/AutoTarget.cs @@ -9,6 +9,7 @@ */ #endregion +using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Linq; @@ -431,9 +432,16 @@ namespace OpenRA.Mods.Common.Traits // Make sure that we can actually fire on the actor var armaments = ab.ChooseArmamentsForTarget(target, false); if (!allowMove) - armaments = armaments.Where(arm => - target.IsInRange(self.CenterPosition, arm.MaxRange()) && - !target.IsInRange(self.CenterPosition, arm.Weapon.MinRange)); + { + // PERF: This lambda captures, contain it within a local function to prevent + // the compiler allocating the helper class at the top of the loop. + static Func IsInRange(Actor self, Target target) => + arm => + target.IsInRange(self.CenterPosition, arm.MaxRange()) && + !target.IsInRange(self.CenterPosition, arm.Weapon.MinRange); + + armaments = armaments.Where(IsInRange(self, target)); + } if (!armaments.Any()) continue; diff --git a/OpenRA.Mods.Common/Traits/Render/RenderSprites.cs b/OpenRA.Mods.Common/Traits/Render/RenderSprites.cs index 630ec96688..d454730615 100644 --- a/OpenRA.Mods.Common/Traits/Render/RenderSprites.cs +++ b/OpenRA.Mods.Common/Traits/Render/RenderSprites.cs @@ -143,6 +143,8 @@ namespace OpenRA.Mods.Common.Traits.Render public readonly RenderSpritesInfo Info; readonly string faction; readonly List anims = []; + readonly IEnumerable renderables; + bool shouldRefreshPalettes; string cachedImage; public static Func MakeFacingFunc(Actor self) @@ -158,6 +160,7 @@ namespace OpenRA.Mods.Common.Traits.Render { Info = info; faction = init.GetValue(init.Self.Owner.Faction.InternalName); + renderables = RenderAnimations(anims, init.Self); } public string GetImage(Actor self) @@ -170,6 +173,7 @@ namespace OpenRA.Mods.Common.Traits.Render public void UpdatePalette() { + shouldRefreshPalettes = true; foreach (var anim in anims) anim.OwnerChanged(); } @@ -178,18 +182,30 @@ namespace OpenRA.Mods.Common.Traits.Render public void OnEffectiveOwnerChanged(Actor self, Player oldEffectiveOwner, Player newEffectiveOwner) { UpdatePalette(); } public virtual IEnumerable Render(Actor self, WorldRenderer wr) + { + if (shouldRefreshPalettes) + { + shouldRefreshPalettes = false; + foreach (var a in anims) + { + if (a.PaletteReference == null) + { + var owner = self.EffectiveOwner != null && self.EffectiveOwner.Disguised ? self.EffectiveOwner.Owner : self.Owner; + a.CachePalette(wr, owner); + } + } + } + + return renderables; + } + + static IEnumerable RenderAnimations(List anims, Actor self) { foreach (var a in anims) { if (!a.IsVisible) continue; - if (a.PaletteReference == null) - { - var owner = self.EffectiveOwner != null && self.EffectiveOwner.Disguised ? self.EffectiveOwner.Owner : self.Owner; - a.CachePalette(wr, owner); - } - foreach (var r in a.Animation.Render(self, a.PaletteReference)) yield return r; } @@ -226,6 +242,7 @@ namespace OpenRA.Mods.Common.Traits.Render isPlayerPalette = Info.Palette == null; } + shouldRefreshPalettes = true; anims.Add(new AnimationWrapper(anim, palette, isPlayerPalette)); }