Кратко: лучше вынести работу с изображениями в менеджер ресурсов (ResourceManager / TextureCache) привязанный к GPU‑контексту (или глобальный, если контексты шарят текстуры), и управлять жизненным циклом по правилам «проверка при доступе + бюджетная эвикция (LRU/TTL) + возможность закрепить (pin)». Таймер возможен, но сам по себе обычно слаб; сочетание эвикции при выделении + фоновая периодическая чистка даёт лучшие результаты.
Ниже — рекомендации и паттерны, которые можно применить в вашей архитектуре.
1) Разделение ответственности
- Window хранит ссылки на текстуры, но не саму логику загрузки/эвикции.
- Введите ResourceManager на уровень GPU‑контекста (или глобально, если контексты общие). Он отвечает за загрузку, кэширование, подсчёт размера и уничтожение текстур в правильном (рендерном) потоке.
2) Ключевые механизмы управления
- Shared/weak references: храните в ResourceManager сильные ссылки (shared_ptr или интраузивный refcount) на загруженные текстуры; элементы UI держат weak‑ссылки. При рендере берут strong (если expired — перезагружают).
- LRU / last‑used timestamp: при каждом доступе обновляйте метку использования. Поддерживайте список LRU для быстрой эвикции.
- Бюджет по памяти/количеству: задавайте лимит VRAM (в байтах и/или по числу текстур). При превышении — удаляйте наименее недавно использованные.
- Pinning: возможность «закрепить» важные ресурсы (иконки чекбоксов, скины). Такие текстуры не эвиктятся.
- Evict on allocation: при попытке загрузить новую текстуру — если бюджет превысится, вызывайте эвикцию (LRU) до нормализации. Это надёжнее, чем чистка по таймеру.
3) Таймер vs эвикция при доступе
- Таймер: подходит для фоновой очистки в неактивные моменты (например, приложение в простое). Минус — либо слишком частый (нагрузка), либо слишком редкий (накопление).
- Evict on allocation + background trim: основной механизм — эвикция при выделении памяти; дополняющий — фоновая задача, которая раз в N секунд/минут проверяет и делает мягкую чистку (TTL). Этот подход практичен и экономен.
4) Удаление текстур и потокобезопасность
- Удалять GPU‑ресурсы (glDeleteTextures, vkDestroyImage и т.п.) нужно в рендерном/контекстном потоке. Поэтому реализация должна:
- либо вызывать удаление немедленно, если вы в правильном потоке;
- либо помещать ресурсы в очередь «deferred delete», которую рендерный поток очищает в безопасный момент.
- Мьютексы/атомики: защищайте хранилище при доступе из нескольких потоков (загрузка может быть асинхронной).
5) Асинхронная загрузка и placeholders
- Lazy‑load остаётся хорошей практикой. Загружайте в фоне, в UI рисуйте placeholder (solid color или 1×1 texture) и заменяйте при готовности.
- При асинхронной загрузке аккуратно обрабатывайте повторные запросы: если один ресурс уже загружается, следующие должны ждать или присоединяться к результату.
6) Дополнительные оптимизации
- Атлас текстур (sprite atlas) для мелких UI‑иконок: свести десятки маленьких текстур в одну, сократить накладные расходы GPU и вероятность «мусора» в кэше. Минусы: сложнее обновление отдельных картинок, потребность в перестроении атласа при динамике.
- Форматы и компрессия: храните/загружайте в GPU‑дружественном формате (sRGB, compressed) чтобы экономить VRAM.
- Mipmaps, texture arrays и bindless (если API поддерживает) — для производительности и упрощения карты текстур.
7) API для разработчиков
- Предоставьте методы:
- GetTexture(path) -> shared_handle / future
- Preload(path) / PreloadAsync(path) — явно загрузить нужные ресурсы (например, для диалогов)
- Pin(path) / Unpin(path) — защитить ресурсы от эвикции
- Unload(path) / ClearUnused() — ручная очистка
- TrimToBudget() — принудительная очистка под текущий лимит
- Сигналы памяти: если платформа предоставляет уведомления о нехватке памяти, используйте их для немедленной очистки.
8) Пример алгоритма (упрощённо)
- ResourceManager:
- map[path] -> weak_ptr<TextureNode>
- LRU list упорядочен по last_used
- total_vram_bytes
- budget_bytes
- Get(path):
- если в map есть сильная ссылка -> обновить last_used и вернуть
- иначе: async/sync load -> при завершении вставить в map, увеличить total_vram, вставить в LRU
- если total_vram > budget -> вызывать EvictUntilUnderBudget()
- EvictUntilUnderBudget():
- пройти LRU с конца, пропуская pinned/в использовании, деактивировать и enqueue GPU‑удаление, уменьшить total_vram
9) Практические советы применительно к вашей проблеме с CheckBox
- Закрепите (pin) изображения для часто используемых стандартных виджетов (checkbox, radio, common icons) — они всегда нужны и не должны эвиктиться.
- Если чекбоксы создаются динамически и много различных картинок, комбинируйте с атласом или используйте LRU/TTL, чтобы редко используемые образы уезжали из кэша.
- Если тексты/пиктограммы небольшие — атлас предпочтителен.
Итого: не полагайтесь только на таймер. Реализуйте ResourceManager на уровне контекста с LRU/бюджетом, last_used/TTL, поддержкой pin/explicit preload, deferred GPU‑удалением и асинхронной загрузкой. Это даёт предсказуемую экономию памяти и гибкость для UI‑библиотеки. Могу помочь набросать конкретную структуру классов/псевдокод для вашей архитектуры, если хотите — опишите язык и графический API (GL, Vulkan, D3D).