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.
This commit is contained in:
RoosterDragon
2025-12-19 19:05:36 +00:00
committed by Gustas Kažukauskas
parent bcfaa44d66
commit 06e4a2e010
3 changed files with 47 additions and 12 deletions

View File

@@ -123,6 +123,9 @@ namespace OpenRA
readonly IEnumerable<WPos> enabledTargetableWorldPositions;
bool created;
IEnumerable<IRenderable> renderables;
WorldRenderer lastWorldRenderer;
internal Actor(World world, string name, TypeDictionary initDict)
{
var duplicateInit = initDict.WithInterface<ISingleInstanceInit>().GroupBy(i => i.GetType())
@@ -287,11 +290,18 @@ namespace OpenRA
public IEnumerable<IRenderable> 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<IRenderable> Renderables(WorldRenderer wr)

View File

@@ -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<Armament, bool> 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;

View File

@@ -143,6 +143,8 @@ namespace OpenRA.Mods.Common.Traits.Render
public readonly RenderSpritesInfo Info;
readonly string faction;
readonly List<AnimationWrapper> anims = [];
readonly IEnumerable<IRenderable> renderables;
bool shouldRefreshPalettes;
string cachedImage;
public static Func<WAngle> MakeFacingFunc(Actor self)
@@ -158,6 +160,7 @@ namespace OpenRA.Mods.Common.Traits.Render
{
Info = info;
faction = init.GetValue<FactionInit, string>(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<IRenderable> 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<IRenderable> RenderAnimations(List<AnimationWrapper> 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));
}