1.4.1: утечка видеопамяти от трупов «Жертвы», труп лежит 5 минут
Было: CorpseLootPatch перекрашивал труп через renderer.materials и Tint(), и ничего за собой не убирал. Копии материалов Unity с объектом не удаляет, перекрашенная текстура была несжатой RGBA32 (16-64 МБ) и читаемой (копия ещё и в RAM), кеш только рос. Лог игры: Texture.currentTextureMemory 1,2 -> 7,4 ГБ за два часа при desired ~3,5 ГБ, FPS 60 -> 11. Те же жалобы на Nexus (sanovskiy, Kriloko, Megaladon43). Стало: каждый перекрашенный труп записан (рендереры, материалы, текстуры), тик раз в секунду удаляет всё за трупом, которого нет в мире, текстура удаляется со счётчиком пользователей. Текстура не больше 1024, DXT, makeNoLongerReadable. Проверено в игре: обыск и истечение срока доводят счёт до «трупов 0, текстур 0». Необысканный труп лежит 300 с вместо 3600 (указание пользователя). buffNecroZombieKillTrackerDisplay: update_rate .1 -> 1 (отзыв zomb1); урон ножа растёт после убийства, проверено. Документы: версия 1.4.1, README (статус + несовместимость с 3.3 experimental), описания Nexus и сайта (пять минут вместо часа), NEXUS_CHANGELOG/FILE_SUMMARY_1.4.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
67dd410358
commit
77b56be9ae
@@ -54,9 +54,11 @@ namespace NecromancerTome
|
||||
/// </summary>
|
||||
public static class CorpseLoot
|
||||
{
|
||||
/// <summary>Сколько секунд лежит помеченный труп. У ванильного мешка с лутом 3600, и
|
||||
/// причин быть скромнее нет: труп и есть мешок.</summary>
|
||||
public const float CorpseSecondsAfterDeath = 3600f;
|
||||
/// <summary>Сколько секунд лежит помеченный труп. В 1.4.0 было 3600, как у ванильного мешка с
|
||||
/// лутом; 2026-09-24 уменьшено до 5 минут по указанию пользователя: «если игрок не будет
|
||||
/// убирать, у него FPS начнёт падать» - каждый лежащий труп это отрисовываемая модель со
|
||||
/// своими копиями материалов, а необобранных за час набирается много.</summary>
|
||||
public const float CorpseSecondsAfterDeath = 300f;
|
||||
|
||||
/// <summary>Во что красится труп. Тёмно-багровый: издалека видно, что этот не как все, и
|
||||
/// при этом не выглядит подсветкой из другой игры.</summary>
|
||||
@@ -81,9 +83,42 @@ namespace NecromancerTome
|
||||
/// кадре, когда сущность умирает. Добивается односекундным тиком PetFollowPatch.</summary>
|
||||
public static readonly HashSet<int> PendingTint = new HashSet<int>();
|
||||
|
||||
/// <summary>УТЕЧКА ПАМЯТИ, НАЙДЕНА 2026-09-24 (жалоба: «память течёт, лечится только
|
||||
/// перезапуском»). Версия 1.4.0 перекрашивала труп и НИЧЕГО за собой не убирала:
|
||||
///
|
||||
/// - `renderer.materials` создаёт копии материалов, и Unity НЕ удаляет их вместе с
|
||||
/// объектом - они живут до перезапуска;
|
||||
/// - перекрашенная текстура создавалась несжатой (RGBA32, 16-64 МБ на штуку) и оставалась
|
||||
/// читаемой, то есть лежала дважды - в видеопамяти и копией в оперативной;
|
||||
/// - кеш текстур только рос.
|
||||
///
|
||||
/// Лог игры (`60sec delta: ... current: ..., desired: ...` - это Texture.currentTextureMemory)
|
||||
/// показал рост видеопамяти текстур с 1,2 до 7,4 ГБ за два часа при нужных игре ~3,5 ГБ, и
|
||||
/// FPS упал с 60 до 11. За сессию было перекрашено 53 трупа по 4-10 материалов.
|
||||
///
|
||||
/// Теперь каждый перекрашенный труп записан здесь вместе с копиями материалов и текстурами,
|
||||
/// а тик раз в секунду (ReleaseGoneCorpses) убирает за теми, кого уже нет. Текстура
|
||||
/// удаляется, когда её не использует ни один труп.</summary>
|
||||
public class TintedCorpse
|
||||
{
|
||||
public readonly List<Renderer> Renderers = new List<Renderer>();
|
||||
public readonly List<Material> Materials = new List<Material>();
|
||||
public readonly List<Texture2D> Textures = new List<Texture2D>();
|
||||
}
|
||||
|
||||
public static readonly Dictionary<int, TintedCorpse> Tinted = new Dictionary<int, TintedCorpse>();
|
||||
|
||||
/// <summary>Сколько трупов сейчас носит каждую перекрашенную текстуру.</summary>
|
||||
public static readonly Dictionary<Texture2D, int> TextureUsers = new Dictionary<Texture2D, int>();
|
||||
|
||||
/// <summary>Предел стороны перекрашенной текстуры. Исходные 2048-4096; труп рассматривают
|
||||
/// вблизи, но 1024 со сжатием это ~1 МБ вместо 16-64 МБ, и разница на глаз незаметна.</summary>
|
||||
public const int MaxTintSize = 1024;
|
||||
|
||||
/// <summary>Повторная попытка перекраски. Зовётся из общего тика раз в секунду.</summary>
|
||||
public static void RetryPendingTints(World _world)
|
||||
{
|
||||
ReleaseGoneCorpses(_world);
|
||||
if (PendingTint.Count == 0 || _world == null)
|
||||
{
|
||||
return;
|
||||
@@ -103,6 +138,92 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Уборка за исчезнувшими трупами. Трупом считается исчезнувшим, когда его нет в
|
||||
/// мире И уничтожены все его рендереры: удалять материал, который ещё рисуется, нельзя.</summary>
|
||||
public static void ReleaseGoneCorpses(World _world)
|
||||
{
|
||||
if (Tinted.Count == 0 || _world == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<int> gone = null;
|
||||
foreach (KeyValuePair<int, TintedCorpse> pair in Tinted)
|
||||
{
|
||||
if (_world.GetEntity(pair.Key) != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bool rendered = false;
|
||||
foreach (Renderer renderer in pair.Value.Renderers)
|
||||
{
|
||||
if (renderer != null)
|
||||
{
|
||||
rendered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rendered)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (gone == null)
|
||||
{
|
||||
gone = new List<int>();
|
||||
}
|
||||
gone.Add(pair.Key);
|
||||
}
|
||||
if (gone == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (int id in gone)
|
||||
{
|
||||
TintedCorpse corpse = Tinted[id];
|
||||
Tinted.Remove(id);
|
||||
SearchableCorpses.Remove(id);
|
||||
foreach (Material material in corpse.Materials)
|
||||
{
|
||||
if (material != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(material);
|
||||
}
|
||||
}
|
||||
foreach (Texture2D texture in corpse.Textures)
|
||||
{
|
||||
ReleaseTexture(texture);
|
||||
}
|
||||
}
|
||||
Debug.Log("[NecromancerTome] CorpseLoot: убрано за исчезнувшими трупами: " + gone.Count +
|
||||
", осталось трупов " + Tinted.Count + ", текстур " + TextureUsers.Count);
|
||||
}
|
||||
|
||||
public static void ReleaseTexture(Texture2D _texture)
|
||||
{
|
||||
if (_texture == null || !TextureUsers.TryGetValue(_texture, out int users))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (users > 1)
|
||||
{
|
||||
TextureUsers[_texture] = users - 1;
|
||||
return;
|
||||
}
|
||||
TextureUsers.Remove(_texture);
|
||||
List<Texture> keys = new List<Texture>();
|
||||
foreach (KeyValuePair<Texture, Texture2D> pair in TintedTextures)
|
||||
{
|
||||
if (pair.Value == _texture)
|
||||
{
|
||||
keys.Add(pair.Key);
|
||||
}
|
||||
}
|
||||
foreach (Texture key in keys)
|
||||
{
|
||||
TintedTextures.Remove(key);
|
||||
}
|
||||
UnityEngine.Object.Destroy(_texture);
|
||||
}
|
||||
|
||||
public static bool IsSearchable(Entity _entity)
|
||||
{
|
||||
return _entity != null && SearchableCorpses.Contains(_entity.entityId);
|
||||
@@ -165,6 +286,11 @@ namespace NecromancerTome
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!Tinted.TryGetValue(_corpse.entityId, out TintedCorpse record))
|
||||
{
|
||||
record = new TintedCorpse();
|
||||
Tinted[_corpse.entityId] = record;
|
||||
}
|
||||
int tinted = 0;
|
||||
foreach (Renderer renderer in renderers)
|
||||
{
|
||||
@@ -177,7 +303,13 @@ namespace NecromancerTome
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Копии материалов создаются этой строкой, и все они теперь наши - записываем
|
||||
// каждую, даже ту, что перекрасить не удалось: удалять её тоже нам.
|
||||
Material[] instances = renderer.materials;
|
||||
if (!record.Renderers.Contains(renderer))
|
||||
{
|
||||
record.Renderers.Add(renderer);
|
||||
}
|
||||
for (int i = 0; i < instances.Length && i < shared.Length; i++)
|
||||
{
|
||||
Material material = instances[i];
|
||||
@@ -185,6 +317,10 @@ namespace NecromancerTome
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!record.Materials.Contains(material))
|
||||
{
|
||||
record.Materials.Add(material);
|
||||
}
|
||||
string albedo = GhostTraderPatch.FindAlbedoProperty(material);
|
||||
if (albedo == null)
|
||||
{
|
||||
@@ -196,6 +332,9 @@ namespace NecromancerTome
|
||||
continue;
|
||||
}
|
||||
material.SetTexture(albedo, repainted);
|
||||
record.Textures.Add(repainted);
|
||||
TextureUsers.TryGetValue(repainted, out int users);
|
||||
TextureUsers[repainted] = users + 1;
|
||||
tinted++;
|
||||
}
|
||||
}
|
||||
@@ -222,13 +361,19 @@ namespace NecromancerTome
|
||||
RenderTexture previous = RenderTexture.active;
|
||||
try
|
||||
{
|
||||
rt = RenderTexture.GetTemporary(_source.width, _source.height, 0,
|
||||
// Уменьшение идёт бесплатно, самим Blit в RenderTexture меньшего размера. Стороны
|
||||
// кратны четырём - иначе Compress ниже откажется сжимать.
|
||||
float scale = Mathf.Min(1f, (float)MaxTintSize / Mathf.Max(_source.width, _source.height));
|
||||
int width = Mathf.Max(4, (int)(_source.width * scale) / 4 * 4);
|
||||
int height = Mathf.Max(4, (int)(_source.height * scale) / 4 * 4);
|
||||
|
||||
rt = RenderTexture.GetTemporary(width, height, 0,
|
||||
RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||||
Graphics.Blit(_source, rt);
|
||||
RenderTexture.active = rt;
|
||||
|
||||
painted = new Texture2D(_source.width, _source.height, TextureFormat.RGBA32, false);
|
||||
painted.ReadPixels(new Rect(0f, 0f, _source.width, _source.height), 0, 0);
|
||||
painted = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
||||
painted.ReadPixels(new Rect(0f, 0f, width, height), 0, 0);
|
||||
|
||||
Color32[] pixels = painted.GetPixels32();
|
||||
for (int i = 0; i < pixels.Length; i++)
|
||||
@@ -240,12 +385,19 @@ namespace NecromancerTome
|
||||
pixels[i] = p;
|
||||
}
|
||||
painted.SetPixels32(pixels);
|
||||
painted.Apply(false, false);
|
||||
// Сжатие в DXT (вчетверо-восьмеро меньше RGBA32) и makeNoLongerReadable=true: без
|
||||
// него копия пикселей оставалась в оперативной памяти рядом с видеопамятью.
|
||||
painted.Compress(false);
|
||||
painted.Apply(false, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError("[NecromancerTome] CorpseLoot: не удалось перекрасить текстуру '" +
|
||||
_source.name + "': " + e.Message);
|
||||
if (painted != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(painted);
|
||||
}
|
||||
painted = null;
|
||||
}
|
||||
finally
|
||||
|
||||
Reference in New Issue
Block a user