perf(runtime): remove measured steady allocations
Eliminate boxed production surface-override enumeration, retain vital modifier projections until the enchantment registry mutates, and measure DAT font widths without allocating a captured delegate. Preserve exact hashes, spell stacking, and glyph advances with warmed zero-allocation tests. Validated by the focused rendering, UI, and spell suites, a zero-error Release build, and 8,409 passing Release tests with five pre-existing skips. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
574a6c454f
commit
feb80a67d9
6 changed files with 188 additions and 14 deletions
|
|
@ -848,18 +848,33 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
ulong xorHigh = 0;
|
ulong xorHigh = 0;
|
||||||
ulong sumLow = 0;
|
ulong sumLow = 0;
|
||||||
ulong sumHigh = 0;
|
ulong sumHigh = 0;
|
||||||
foreach ((uint surfaceId, uint replacementId) in overrides)
|
if (overrides is Dictionary<uint, uint> dictionary)
|
||||||
{
|
{
|
||||||
StableRenderHash128 pair = StableRenderHash128.Create();
|
// Production hydration owns Dictionary instances. Enumerating
|
||||||
pair.Add(surfaceId);
|
// through IReadOnlyDictionary boxes Dictionary.Enumerator for
|
||||||
pair.Add(replacementId);
|
// every live mesh on both projection-sync phases.
|
||||||
RenderSceneHash128 value = pair.Finish();
|
foreach (KeyValuePair<uint, uint> pair in dictionary)
|
||||||
xorLow ^= value.Low;
|
|
||||||
xorHigh ^= value.High;
|
|
||||||
unchecked
|
|
||||||
{
|
{
|
||||||
sumLow += value.Low;
|
Accumulate(
|
||||||
sumHigh += value.High;
|
pair.Key,
|
||||||
|
pair.Value,
|
||||||
|
ref xorLow,
|
||||||
|
ref xorHigh,
|
||||||
|
ref sumLow,
|
||||||
|
ref sumHigh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach ((uint surfaceId, uint replacementId) in overrides)
|
||||||
|
{
|
||||||
|
Accumulate(
|
||||||
|
surfaceId,
|
||||||
|
replacementId,
|
||||||
|
ref xorLow,
|
||||||
|
ref xorHigh,
|
||||||
|
ref sumLow,
|
||||||
|
ref sumHigh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -870,6 +885,27 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
geometry.Add(sumLow);
|
geometry.Add(sumLow);
|
||||||
geometry.Add(sumHigh);
|
geometry.Add(sumHigh);
|
||||||
return geometry.Finish();
|
return geometry.Finish();
|
||||||
|
|
||||||
|
static void Accumulate(
|
||||||
|
uint surfaceId,
|
||||||
|
uint replacementId,
|
||||||
|
ref ulong xorLow,
|
||||||
|
ref ulong xorHigh,
|
||||||
|
ref ulong sumLow,
|
||||||
|
ref ulong sumHigh)
|
||||||
|
{
|
||||||
|
StableRenderHash128 pairHash = StableRenderHash128.Create();
|
||||||
|
pairHash.Add(surfaceId);
|
||||||
|
pairHash.Add(replacementId);
|
||||||
|
RenderSceneHash128 value = pairHash.Finish();
|
||||||
|
xorLow ^= value.Low;
|
||||||
|
xorHigh ^= value.High;
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
sumLow += value.Low;
|
||||||
|
sumHigh += value.High;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static RenderSceneHash128 FingerprintSelectionGeometry(
|
internal static RenderSceneHash128 FingerprintSelectionGeometry(
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ public sealed class UiDatFont
|
||||||
|
|
||||||
private readonly Dictionary<char, FontCharDesc> _glyphs;
|
private readonly Dictionary<char, FontCharDesc> _glyphs;
|
||||||
|
|
||||||
private UiDatFont(
|
internal UiDatFont(
|
||||||
uint fgTex, int fgW, int fgH,
|
uint fgTex, int fgW, int fgH,
|
||||||
uint bgTex, int bgW, int bgH,
|
uint bgTex, int bgW, int bgH,
|
||||||
float lineHeight, float baselineOffset,
|
float lineHeight, float baselineOffset,
|
||||||
|
|
@ -131,7 +131,18 @@ public sealed class UiDatFont
|
||||||
/// glyph's retail advance. Characters not in the font contribute nothing.
|
/// glyph's retail advance. Characters not in the font contribute nothing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float MeasureWidth(string text)
|
public float MeasureWidth(string text)
|
||||||
=> MeasureWidth(text, c => _glyphs.TryGetValue(c, out var g) ? g : null);
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return 0f;
|
||||||
|
|
||||||
|
float width = 0f;
|
||||||
|
for (int index = 0; index < text.Length; index++)
|
||||||
|
{
|
||||||
|
if (_glyphs.TryGetValue(text[index], out FontCharDesc? glyph))
|
||||||
|
width += GlyphAdvance(glyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pure pen-advance summation seam: total width of <paramref name="text"/>
|
/// Pure pen-advance summation seam: total width of <paramref name="text"/>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ public sealed class Spellbook
|
||||||
private readonly Dictionary<uint, float> _learnedSpells = new();
|
private readonly Dictionary<uint, float> _learnedSpells = new();
|
||||||
private readonly Dictionary<uint, ActiveEnchantmentRecord> _activeById = new();
|
private readonly Dictionary<uint, ActiveEnchantmentRecord> _activeById = new();
|
||||||
private readonly Dictionary<uint, List<uint>> _enchantmentOrderByBucket = new();
|
private readonly Dictionary<uint, List<uint>> _enchantmentOrderByBucket = new();
|
||||||
|
private readonly Dictionary<uint, EnchantmentMath.VitalMod> _vitalModCache = new();
|
||||||
private readonly List<uint>[] _favoriteSpells = Enumerable.Range(0, 8)
|
private readonly List<uint>[] _favoriteSpells = Enumerable.Range(0, 8)
|
||||||
.Select(_ => new List<uint>()).ToArray();
|
.Select(_ => new List<uint>()).ToArray();
|
||||||
private readonly Dictionary<uint, uint> _desiredComponents = new();
|
private readonly Dictionary<uint, uint> _desiredComponents = new();
|
||||||
|
|
@ -86,8 +87,20 @@ public sealed class Spellbook
|
||||||
/// <see cref="EnchantmentMath.VitalMod.Identity"/> when no buffs
|
/// <see cref="EnchantmentMath.VitalMod.Identity"/> when no buffs
|
||||||
/// apply (or when no SpellTable was wired).
|
/// apply (or when no SpellTable was wired).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public EnchantmentMath.VitalMod GetVitalMod(uint statKey) =>
|
public EnchantmentMath.VitalMod GetVitalMod(uint statKey)
|
||||||
EnchantmentMath.GetMod(ActiveEnchantments, _table, statKey);
|
{
|
||||||
|
if (_vitalModCache.TryGetValue(
|
||||||
|
statKey,
|
||||||
|
out EnchantmentMath.VitalMod cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnchantmentMath.VitalMod calculated =
|
||||||
|
EnchantmentMath.GetMod(ActiveEnchantments, _table, statKey);
|
||||||
|
_vitalModCache.Add(statKey, calculated);
|
||||||
|
return calculated;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Fires when a spell is added to the player's spellbook.</summary>
|
/// <summary>Fires when a spell is added to the player's spellbook.</summary>
|
||||||
public event Action<uint>? SpellLearned;
|
public event Action<uint>? SpellLearned;
|
||||||
|
|
@ -342,6 +355,7 @@ public sealed class Spellbook
|
||||||
|
|
||||||
_activeById.Clear();
|
_activeById.Clear();
|
||||||
_enchantmentOrderByBucket.Clear();
|
_enchantmentOrderByBucket.Clear();
|
||||||
|
_vitalModCache.Clear();
|
||||||
foreach (ActiveEnchantmentRecord enchantment in enchantments)
|
foreach (ActiveEnchantmentRecord enchantment in enchantments)
|
||||||
UpsertManifestEnchantment(enchantment);
|
UpsertManifestEnchantment(enchantment);
|
||||||
|
|
||||||
|
|
@ -401,6 +415,7 @@ public sealed class Spellbook
|
||||||
_learnedSpells.Clear();
|
_learnedSpells.Clear();
|
||||||
_activeById.Clear();
|
_activeById.Clear();
|
||||||
_enchantmentOrderByBucket.Clear();
|
_enchantmentOrderByBucket.Clear();
|
||||||
|
_vitalModCache.Clear();
|
||||||
foreach (List<uint> tab in _favoriteSpells) tab.Clear();
|
foreach (List<uint> tab in _favoriteSpells) tab.Clear();
|
||||||
_desiredComponents.Clear();
|
_desiredComponents.Clear();
|
||||||
_spellbookFilters = 0x3FFFu;
|
_spellbookFilters = 0x3FFFu;
|
||||||
|
|
@ -418,6 +433,7 @@ public sealed class Spellbook
|
||||||
|
|
||||||
private void NotifyEnchantmentsChanged()
|
private void NotifyEnchantmentsChanged()
|
||||||
{
|
{
|
||||||
|
_vitalModCache.Clear();
|
||||||
EnchantmentsChanged?.Invoke();
|
EnchantmentsChanged?.Invoke();
|
||||||
StateChanged?.Invoke();
|
StateChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -472,6 +472,33 @@ public sealed class CurrentRenderSceneOracleTests
|
||||||
CurrentRenderSceneOracle.CreateSurfaceOverrideFingerprint(null));
|
CurrentRenderSceneOracle.CreateSurfaceOverrideFingerprint(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing()
|
||||||
|
{
|
||||||
|
var overrides = new Dictionary<uint, uint>
|
||||||
|
{
|
||||||
|
[0x0800_0001] = 0x0500_0011,
|
||||||
|
[0x0800_0002] = 0x0500_0012,
|
||||||
|
[0x0800_0003] = 0x0500_0013,
|
||||||
|
};
|
||||||
|
RenderSceneHash128 expected =
|
||||||
|
CurrentRenderSceneOracle.CreateSurfaceOverrideFingerprint(
|
||||||
|
overrides);
|
||||||
|
RenderSceneHash128 actual = default;
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
|
||||||
|
for (int iteration = 0; iteration < 10_000; iteration++)
|
||||||
|
{
|
||||||
|
actual =
|
||||||
|
CurrentRenderSceneOracle.CreateSurfaceOverrideFingerprint(
|
||||||
|
overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
Assert.Equal(0, allocated);
|
||||||
|
}
|
||||||
|
|
||||||
private static CurrentRenderProjectionFingerprint CaptureSingle(
|
private static CurrentRenderProjectionFingerprint CaptureSingle(
|
||||||
WorldEntity entity,
|
WorldEntity entity,
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
|
|
|
||||||
|
|
@ -81,4 +81,35 @@ public class UiDatFontTests
|
||||||
Assert.Equal(0f, UiDatFont.MeasureWidth("", Lookup));
|
Assert.Equal(0f, UiDatFont.MeasureWidth("", Lookup));
|
||||||
Assert.Equal(0f, UiDatFont.MeasureWidth(null, Lookup));
|
Assert.Equal(0f, UiDatFont.MeasureWidth(null, Lookup));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InstanceMeasureWidth_ReusesGlyphTableWithoutAllocating()
|
||||||
|
{
|
||||||
|
var glyphs = new Dictionary<char, FontCharDesc>
|
||||||
|
{
|
||||||
|
['A'] = Glyph('A', width: 8, before: 1, after: 2),
|
||||||
|
['B'] = Glyph('B', width: 7, before: 1, after: 1),
|
||||||
|
};
|
||||||
|
var font = new UiDatFont(
|
||||||
|
fgTex: 0,
|
||||||
|
fgW: 0,
|
||||||
|
fgH: 0,
|
||||||
|
bgTex: 0,
|
||||||
|
bgW: 0,
|
||||||
|
bgH: 0,
|
||||||
|
lineHeight: 16f,
|
||||||
|
baselineOffset: 12f,
|
||||||
|
glyphs);
|
||||||
|
const string Text = "ABBA";
|
||||||
|
float expected = font.MeasureWidth(Text);
|
||||||
|
float actual = 0f;
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
|
||||||
|
for (int iteration = 0; iteration < 10_000; iteration++)
|
||||||
|
actual = font.MeasureWidth(Text);
|
||||||
|
|
||||||
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
Assert.Equal(0, allocated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,59 @@ public sealed class SpellbookTests
|
||||||
Assert.Equal(2, book.ActiveCount);
|
Assert.Equal(2, book.ActiveCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVitalMod_ReusesCachedValueUntilEnchantmentsChange()
|
||||||
|
{
|
||||||
|
SpellTable table = SpellTable.Create(
|
||||||
|
[
|
||||||
|
TestSpell(42u) with { Family = 100u },
|
||||||
|
]);
|
||||||
|
var book = new Spellbook(table);
|
||||||
|
book.OnEnchantmentAdded(new ActiveEnchantmentRecord(
|
||||||
|
SpellId: 42u,
|
||||||
|
LayerId: 7u,
|
||||||
|
Duration: 300f,
|
||||||
|
CasterGuid: 0u,
|
||||||
|
StatModKey: EnchantmentMath.StatKey.MaxHealth,
|
||||||
|
StatModValue: 1.5f,
|
||||||
|
Bucket: 1u));
|
||||||
|
Assert.Equal(
|
||||||
|
1.5f,
|
||||||
|
book.GetVitalMod(EnchantmentMath.StatKey.MaxHealth).Multiplier);
|
||||||
|
EnchantmentMath.VitalMod actual = default;
|
||||||
|
|
||||||
|
// Cross the tiered-PGO call threshold before measuring the retained
|
||||||
|
// lookup itself. Otherwise a full parallel solution run can attribute
|
||||||
|
// runtime tier-promotion bookkeeping to this thread.
|
||||||
|
for (int iteration = 0; iteration < 10_000; iteration++)
|
||||||
|
actual = book.GetVitalMod(EnchantmentMath.StatKey.MaxHealth);
|
||||||
|
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
|
||||||
|
for (int iteration = 0; iteration < 10_000; iteration++)
|
||||||
|
{
|
||||||
|
actual =
|
||||||
|
book.GetVitalMod(EnchantmentMath.StatKey.MaxHealth);
|
||||||
|
}
|
||||||
|
|
||||||
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
Assert.Equal(1.5f, actual.Multiplier);
|
||||||
|
Assert.Equal(0, allocated);
|
||||||
|
|
||||||
|
book.OnEnchantmentAdded(new ActiveEnchantmentRecord(
|
||||||
|
SpellId: 42u,
|
||||||
|
LayerId: 7u,
|
||||||
|
Duration: 300f,
|
||||||
|
CasterGuid: 0u,
|
||||||
|
StatModKey: EnchantmentMath.StatKey.MaxHealth,
|
||||||
|
StatModValue: 1.25f,
|
||||||
|
Bucket: 1u));
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
1.25f,
|
||||||
|
book.GetVitalMod(EnchantmentMath.StatKey.MaxHealth).Multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LiveEnchantments_NewIdentityInsertsAtRetailListHead()
|
public void LiveEnchantments_NewIdentityInsertsAtRetailListHead()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue