1.4.3: сползание FPS и текстура пирамиды - перенос исправлений 1.5.1 на игру 3.2

В 1.4.2 (игра 3.2) оставалась та же утечка, что чинилась в 1.5.1: шкала некромантии
fill="{% cvar('necroNecromancyProgressCVar') / 20 }" набирала по одной отслеживаемой
переменной за кадр (ошибка игры в BindingNcalcFunctions.cvar / BindingInfoNcalc.RegisterVariable;
в 3.2 эти классы совпадают с 3.3 построчно). CVarBindingLeakPatch - без изменений из 888cf1e.

Пирамида Ереси при Texture Quality ниже Full: CustomBlockPaint берёт мип под размер атласа,
бандл necroatlas - пересобранный с ignoreMipmapLimit (оба из 888cf1e).

Сборка: игра на машине уже 3.3, поэтому DLL собрана против справочной сборки 3.2,
сгенерированной из декомпиляции 3.2 (только метаданные). Проверка метода: пересборка
нетронутой 1.4.2 дала те же 1049 ссылок на внешний код, что и опубликованная DLL; в 1.4.3
добавилось 9 ссылок, все есть в DLL 1.5.1, собранной против настоящей игры.
В игре 3.2 НЕ проверено.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-09-30 10:48:20 +03:00
co-authored by Claude Opus 5.5
parent f4470b784f
commit 205b4d7071
9 changed files with 70 additions and 8 deletions
+42
View File
@@ -0,0 +1,42 @@
using HarmonyLib;
namespace NecromancerTome
{
/// <summary>
/// Утечка в самой игре, на которую наступала шкала некромантии в поясе (1.3.0+,
/// fill="{% cvar('necroNecromancyProgressCVar') / 20 }" в Config/XUi_InGame/windows.xml).
///
/// Найдено 28.09 замером (NecroProfiler): FPS 60 -> 30 за 10-15 минут в любом мире, в теле;
/// группа окон toolbelt росла 2 -> 23 мс/кадр при неизменных 573 элементах; в режиме змеи пояс
/// скрыт - и тормозов нет; перезаход в мир сбрасывает.
///
/// Механика (декомпиляция 3.3): BindingNcalcFunctions.cvar при каждом вычислении выражения
/// регистрирует НОВЫЙ VariableStateCVar, а BindingInfoNcalc.RegisterVariable добавляет его в
/// список без проверки. Новая переменная начинает с 0; если шкала не пуста, её RefreshValue в
/// следующем кадре видит «изменилось» -> выражение пересчитывается -> cvar() добавляет ещё одну.
/// Плюс одна переменная за кадр, пока CVar не 0, и каждый кадр обходится весь список.
///
/// Лечение: одну и ту же CVar в одной привязке регистрируем один раз. Значение cvar() берёт
/// прямо у игрока, так что отслеживающая переменная нужна только одна - для «изменилось».
/// </summary>
[HarmonyPatch(typeof(BindingInfoNcalc), nameof(BindingInfoNcalc.RegisterVariable))]
public static class Patch_BindingInfoNcalc_RegisterVariable_CVarLeak
{
public static bool Prefix(BindingInfoNcalc __instance, BindingInfoNcalc.VariableStateAbs _var, ref bool __result)
{
if (!(_var is BindingInfoNcalc.VariableStateCVar added))
{
return true;
}
foreach (BindingInfoNcalc.VariableStateAbs existing in __instance.variables)
{
if (existing is BindingInfoNcalc.VariableStateCVar known && known.cVarName == added.cVarName)
{
__result = true;
return false;
}
}
return true;
}
}
}
+16 -5
View File
@@ -269,7 +269,15 @@ namespace NecromancerTome
return -1;
}
if (ours.width != src.width || ours.height != src.height)
// Качество текстур ниже Full уменьшает атлас игры вдвое/вчетверо (Half: 256 вместо 512).
// Раньше тут был пропуск - и пирамида оставалась без текстуры (28.09, игра на Half).
// Наши уменьшенные копии уже лежат в мипах: при Half берём с мипа 1, при Quarter - с 2.
int mipOffset = 0;
while (mipOffset < ours.mipmapCount && (ours.width >> mipOffset) > src.width)
{
mipOffset++;
}
if ((ours.width >> mipOffset) != src.width || (ours.height >> mipOffset) != src.height)
{
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: " + label + " size mismatch, " +
"atlas is " + src.width + "x" + src.height + " but ours is " +
@@ -283,11 +291,11 @@ namespace NecromancerTome
" - skipped");
return -1;
}
if (ours.mipmapCount != src.mipmapCount)
if (ours.mipmapCount - mipOffset < src.mipmapCount)
{
Debug.LogWarning("[NecromancerTome] CustomBlockPaint: " + label + " mip mismatch, " +
"atlas has " + src.mipmapCount + " but ours has " + ours.mipmapCount +
" - skipped");
" (from mip " + mipOffset + ") - skipped");
return -1;
}
@@ -303,11 +311,14 @@ namespace NecromancerTome
// GPU-side copy: the game's arrays are non-readable, so nothing can be pulled back
// to the CPU. CopyTexture moves whole slices with their mip chains.
for (int i = 0; i < slice; i++) Graphics.CopyTexture(src, i, grown, i);
Graphics.CopyTexture(ours, 0, grown, slice);
for (int mip = 0; mip < src.mipmapCount; mip++)
{
Graphics.CopyTexture(ours, 0, mip + mipOffset, grown, slice, mip);
}
target = grown;
Debug.Log("[NecromancerTome] CustomBlockPaint: " + label + " grown from " + slice +
" to " + (slice + 1) + " slices");
" to " + (slice + 1) + " slices" + (mipOffset > 0 ? ", ours taken from mip " + mipOffset : ""));
return slice;
}