From feb80a67d904090cb3e577685d458c88da6be393 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 25 Jul 2026 17:50:30 +0200 Subject: [PATCH] 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 --- .../Scene/CurrentRenderSceneOracle.cs | 56 +++++++++++++++---- src/AcDream.App/UI/UiDatFont.cs | 15 ++++- src/AcDream.Core/Spells/Spellbook.cs | 20 ++++++- .../CurrentRenderSceneOracleTests.cs | 27 +++++++++ tests/AcDream.App.Tests/UI/UiDatFontTests.cs | 31 ++++++++++ .../Spells/SpellbookTests.cs | 53 ++++++++++++++++++ 6 files changed, 188 insertions(+), 14 deletions(-) diff --git a/src/AcDream.App/Rendering/Scene/CurrentRenderSceneOracle.cs b/src/AcDream.App/Rendering/Scene/CurrentRenderSceneOracle.cs index aee9a1f9..ecea5872 100644 --- a/src/AcDream.App/Rendering/Scene/CurrentRenderSceneOracle.cs +++ b/src/AcDream.App/Rendering/Scene/CurrentRenderSceneOracle.cs @@ -848,18 +848,33 @@ internal sealed class CurrentRenderSceneOracle : ulong xorHigh = 0; ulong sumLow = 0; ulong sumHigh = 0; - foreach ((uint surfaceId, uint replacementId) in overrides) + if (overrides is Dictionary dictionary) { - StableRenderHash128 pair = StableRenderHash128.Create(); - pair.Add(surfaceId); - pair.Add(replacementId); - RenderSceneHash128 value = pair.Finish(); - xorLow ^= value.Low; - xorHigh ^= value.High; - unchecked + // Production hydration owns Dictionary instances. Enumerating + // through IReadOnlyDictionary boxes Dictionary.Enumerator for + // every live mesh on both projection-sync phases. + foreach (KeyValuePair pair in dictionary) { - sumLow += value.Low; - sumHigh += value.High; + Accumulate( + 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(sumHigh); 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( diff --git a/src/AcDream.App/UI/UiDatFont.cs b/src/AcDream.App/UI/UiDatFont.cs index 1e04c70e..176cb3b0 100644 --- a/src/AcDream.App/UI/UiDatFont.cs +++ b/src/AcDream.App/UI/UiDatFont.cs @@ -62,7 +62,7 @@ public sealed class UiDatFont private readonly Dictionary _glyphs; - private UiDatFont( + internal UiDatFont( uint fgTex, int fgW, int fgH, uint bgTex, int bgW, int bgH, float lineHeight, float baselineOffset, @@ -131,7 +131,18 @@ public sealed class UiDatFont /// glyph's retail advance. Characters not in the font contribute nothing. /// 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; + } /// /// Pure pen-advance summation seam: total width of diff --git a/src/AcDream.Core/Spells/Spellbook.cs b/src/AcDream.Core/Spells/Spellbook.cs index 80b00b00..754f5bec 100644 --- a/src/AcDream.Core/Spells/Spellbook.cs +++ b/src/AcDream.Core/Spells/Spellbook.cs @@ -23,6 +23,7 @@ public sealed class Spellbook private readonly Dictionary _learnedSpells = new(); private readonly Dictionary _activeById = new(); private readonly Dictionary> _enchantmentOrderByBucket = new(); + private readonly Dictionary _vitalModCache = new(); private readonly List[] _favoriteSpells = Enumerable.Range(0, 8) .Select(_ => new List()).ToArray(); private readonly Dictionary _desiredComponents = new(); @@ -86,8 +87,20 @@ public sealed class Spellbook /// when no buffs /// apply (or when no SpellTable was wired). /// - public EnchantmentMath.VitalMod GetVitalMod(uint statKey) => - EnchantmentMath.GetMod(ActiveEnchantments, _table, statKey); + public EnchantmentMath.VitalMod GetVitalMod(uint 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; + } /// Fires when a spell is added to the player's spellbook. public event Action? SpellLearned; @@ -342,6 +355,7 @@ public sealed class Spellbook _activeById.Clear(); _enchantmentOrderByBucket.Clear(); + _vitalModCache.Clear(); foreach (ActiveEnchantmentRecord enchantment in enchantments) UpsertManifestEnchantment(enchantment); @@ -401,6 +415,7 @@ public sealed class Spellbook _learnedSpells.Clear(); _activeById.Clear(); _enchantmentOrderByBucket.Clear(); + _vitalModCache.Clear(); foreach (List tab in _favoriteSpells) tab.Clear(); _desiredComponents.Clear(); _spellbookFilters = 0x3FFFu; @@ -418,6 +433,7 @@ public sealed class Spellbook private void NotifyEnchantmentsChanged() { + _vitalModCache.Clear(); EnchantmentsChanged?.Invoke(); StateChanged?.Invoke(); } diff --git a/tests/AcDream.App.Tests/Rendering/CurrentRenderSceneOracleTests.cs b/tests/AcDream.App.Tests/Rendering/CurrentRenderSceneOracleTests.cs index e52c7b0b..f30e36ba 100644 --- a/tests/AcDream.App.Tests/Rendering/CurrentRenderSceneOracleTests.cs +++ b/tests/AcDream.App.Tests/Rendering/CurrentRenderSceneOracleTests.cs @@ -472,6 +472,33 @@ public sealed class CurrentRenderSceneOracleTests CurrentRenderSceneOracle.CreateSurfaceOverrideFingerprint(null)); } + [Fact] + public void SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing() + { + var overrides = new Dictionary + { + [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( WorldEntity entity, uint landblockId, diff --git a/tests/AcDream.App.Tests/UI/UiDatFontTests.cs b/tests/AcDream.App.Tests/UI/UiDatFontTests.cs index 55a6457a..923af959 100644 --- a/tests/AcDream.App.Tests/UI/UiDatFontTests.cs +++ b/tests/AcDream.App.Tests/UI/UiDatFontTests.cs @@ -81,4 +81,35 @@ public class UiDatFontTests Assert.Equal(0f, UiDatFont.MeasureWidth("", Lookup)); Assert.Equal(0f, UiDatFont.MeasureWidth(null, Lookup)); } + + [Fact] + public void InstanceMeasureWidth_ReusesGlyphTableWithoutAllocating() + { + var glyphs = new Dictionary + { + ['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); + } } diff --git a/tests/AcDream.Core.Tests/Spells/SpellbookTests.cs b/tests/AcDream.Core.Tests/Spells/SpellbookTests.cs index 54a7d60a..86fb09f3 100644 --- a/tests/AcDream.Core.Tests/Spells/SpellbookTests.cs +++ b/tests/AcDream.Core.Tests/Spells/SpellbookTests.cs @@ -90,6 +90,59 @@ public sealed class SpellbookTests 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] public void LiveEnchantments_NewIdentityInsertsAtRetailListHead() {