com.alicizax.unity.framework/Runtime/ABase/GameObjectPool/RuntimePoolModels.cs

1791 lines
56 KiB
C#
Raw Normal View History

2026-03-26 10:49:41 +08:00
using System;
using System.Collections.Generic;
2026-04-30 17:18:17 +08:00
using System.Runtime.CompilerServices;
2026-03-26 10:49:41 +08:00
using System.Threading;
2026-04-30 17:18:17 +08:00
using AlicizaX.ObjectPool;
using Cysharp.Text;
2026-03-26 10:49:41 +08:00
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace AlicizaX
{
2026-04-17 21:01:20 +08:00
public readonly struct PoolSpawnContext
{
public readonly string AssetPath;
public readonly string Group;
public readonly Transform Parent;
public readonly uint SpawnFrame;
public PoolSpawnContext(
string assetPath,
string group,
Transform parent,
uint spawnFrame = 0)
{
AssetPath = assetPath;
Group = group;
Parent = parent;
SpawnFrame = spawnFrame;
}
public PoolSpawnContext WithParent(Transform parent)
{
2026-04-30 17:18:17 +08:00
return new PoolSpawnContext(
AssetPath,
Group,
parent,
SpawnFrame);
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
public PoolSpawnContext WithGroup(string group)
2026-04-17 21:01:20 +08:00
{
return new PoolSpawnContext(
2026-04-30 17:18:17 +08:00
AssetPath,
2026-04-17 21:01:20 +08:00
group,
2026-04-30 17:18:17 +08:00
Parent,
SpawnFrame);
}
public static PoolSpawnContext Create(string assetPath, Transform parent)
{
return new PoolSpawnContext(
assetPath,
null,
2026-04-17 21:01:20 +08:00
parent,
spawnFrame: (uint)Time.frameCount);
}
}
public interface IGameObjectPoolable
{
void OnPoolCreate();
void OnPoolGet(in PoolSpawnContext context);
void OnPoolRelease();
void OnPoolDestroy();
}
2026-04-30 17:18:17 +08:00
public interface IPoolSleepable
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
void EnterSleep();
void ExitSleep(in PoolSpawnContext context);
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
internal readonly struct PoolRecycleRuntimeContext
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
public readonly string Group;
public readonly string AssetPath;
public readonly PoolCategory Category;
public readonly int ActiveCount;
public readonly int InactiveCount;
public readonly int TotalCount;
public readonly int MinRetained;
public readonly int SoftCapacity;
public readonly int HardCapacity;
public readonly int ShortPeakRetainPercent;
public readonly int LongPeakRetainPercent;
public readonly int ShortPeakDecayFrames;
public readonly int LongPeakDecayFrames;
public readonly int TrimMultiplier;
public readonly bool RetainAllInactive;
public readonly int PeakActiveShort;
public readonly int PeakActiveLong;
public readonly int IdleFrames;
public readonly int HotFrames;
public readonly float OldestIdleSeconds;
public readonly bool LowMemory;
public PoolRecycleRuntimeContext(
string group,
string assetPath,
PoolCategory category,
int activeCount,
int inactiveCount,
int totalCount,
int minRetained,
int softCapacity,
int hardCapacity,
int shortPeakRetainPercent,
int longPeakRetainPercent,
int shortPeakDecayFrames,
int longPeakDecayFrames,
int trimMultiplier,
bool retainAllInactive,
int peakActiveShort,
int peakActiveLong,
int idleFrames,
int hotFrames,
float oldestIdleSeconds,
bool lowMemory)
{
Group = group;
AssetPath = assetPath;
Category = category;
ActiveCount = activeCount;
InactiveCount = inactiveCount;
TotalCount = totalCount;
MinRetained = minRetained;
SoftCapacity = softCapacity;
HardCapacity = hardCapacity;
ShortPeakRetainPercent = shortPeakRetainPercent;
LongPeakRetainPercent = longPeakRetainPercent;
ShortPeakDecayFrames = shortPeakDecayFrames;
LongPeakDecayFrames = longPeakDecayFrames;
TrimMultiplier = trimMultiplier;
RetainAllInactive = retainAllInactive;
PeakActiveShort = peakActiveShort;
PeakActiveLong = peakActiveLong;
IdleFrames = idleFrames;
HotFrames = hotFrames;
OldestIdleSeconds = oldestIdleSeconds;
LowMemory = lowMemory;
}
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
internal readonly struct PoolRecyclePlan
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
public readonly int RetainTarget;
public readonly int TrimBudget;
public readonly bool ForceTrim;
public readonly bool ForcePrefabUnload;
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
public PoolRecyclePlan(int retainTarget, int trimBudget, bool forceTrim, bool forcePrefabUnload)
{
RetainTarget = retainTarget;
TrimBudget = trimBudget;
ForceTrim = forceTrim;
ForcePrefabUnload = forcePrefabUnload;
}
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
public readonly struct GameObjectPoolSummarySnapshot
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
public readonly bool IsReady;
public readonly bool WaitingForBootstrap;
public readonly int PoolCount;
public readonly int LoadedPrefabCount;
public readonly int TotalInstanceCount;
public readonly int ActiveInstanceCount;
public readonly int InactiveInstanceCount;
public readonly int PendingMaintenanceCount;
public GameObjectPoolSummarySnapshot(
bool isReady,
bool waitingForBootstrap,
int poolCount,
int loadedPrefabCount,
int totalInstanceCount,
int activeInstanceCount,
int inactiveInstanceCount,
int pendingMaintenanceCount)
{
IsReady = isReady;
WaitingForBootstrap = waitingForBootstrap;
PoolCount = poolCount;
LoadedPrefabCount = loadedPrefabCount;
TotalInstanceCount = totalInstanceCount;
ActiveInstanceCount = activeInstanceCount;
InactiveInstanceCount = inactiveInstanceCount;
PendingMaintenanceCount = pendingMaintenanceCount;
}
2026-04-17 21:01:20 +08:00
}
2026-03-26 10:49:41 +08:00
[Serializable]
public sealed class GameObjectPoolInstanceSnapshot : IMemory
{
public string instanceName;
public bool isActive;
public float idleDuration;
2026-04-17 21:01:20 +08:00
public float lifeDuration;
2026-03-26 10:49:41 +08:00
public GameObject gameObject;
public void Clear()
{
instanceName = null;
isActive = false;
idleDuration = 0f;
2026-04-17 21:01:20 +08:00
lifeDuration = 0f;
2026-03-26 10:49:41 +08:00
gameObject = null;
}
}
[Serializable]
public sealed class GameObjectPoolSnapshot : IMemory
{
2026-04-17 21:01:20 +08:00
public string entryName;
2026-03-26 10:49:41 +08:00
public string group;
public string assetPath;
2026-04-30 17:18:17 +08:00
public PoolCategory category;
2026-03-26 10:49:41 +08:00
public PoolResourceLoaderType loaderType;
2026-04-17 21:01:20 +08:00
public int minRetained;
2026-04-30 17:18:17 +08:00
public int retainTarget;
2026-04-17 21:01:20 +08:00
public int softCapacity;
public int hardCapacity;
2026-04-30 17:18:17 +08:00
public int runtimeHardCapacity;
public int trimPerTick;
public int shortPeakRetainPercent;
public int longPeakRetainPercent;
public int shortPeakDecayFrames;
public int longPeakDecayFrames;
public int trimMultiplier;
public bool retainAllInactive;
2026-03-26 10:49:41 +08:00
public int totalCount;
public int activeCount;
public int inactiveCount;
public bool prefabLoaded;
public float prefabIdleDuration;
2026-04-30 17:18:17 +08:00
public int prefabWakeCount;
public float prefabWakeGap;
public float prefabUnloadDelay;
public float nextMaintenanceIn;
2026-04-17 21:01:20 +08:00
public int acquireCount;
public int releaseCount;
public int hitCount;
public int missCount;
public int expandCount;
public int destroyCount;
public int peakActive;
2026-04-30 17:18:17 +08:00
public int peakActiveShort;
public int peakActiveLong;
2026-03-26 10:49:41 +08:00
public List<GameObjectPoolInstanceSnapshot> instances = new List<GameObjectPoolInstanceSnapshot>();
public void Clear()
{
2026-04-17 21:01:20 +08:00
entryName = null;
2026-03-26 10:49:41 +08:00
group = null;
assetPath = null;
2026-04-30 17:18:17 +08:00
category = default;
2026-03-26 10:49:41 +08:00
loaderType = default;
2026-04-17 21:01:20 +08:00
minRetained = 0;
2026-04-30 17:18:17 +08:00
retainTarget = 0;
2026-04-17 21:01:20 +08:00
softCapacity = 0;
hardCapacity = 0;
2026-04-30 17:18:17 +08:00
runtimeHardCapacity = 0;
trimPerTick = 0;
shortPeakRetainPercent = 0;
longPeakRetainPercent = 0;
shortPeakDecayFrames = 0;
longPeakDecayFrames = 0;
trimMultiplier = 0;
retainAllInactive = false;
2026-03-26 10:49:41 +08:00
totalCount = 0;
activeCount = 0;
inactiveCount = 0;
prefabLoaded = false;
prefabIdleDuration = 0f;
2026-04-30 17:18:17 +08:00
prefabWakeCount = 0;
prefabWakeGap = 0f;
prefabUnloadDelay = 0f;
nextMaintenanceIn = 0f;
2026-04-17 21:01:20 +08:00
acquireCount = 0;
releaseCount = 0;
hitCount = 0;
missCount = 0;
expandCount = 0;
destroyCount = 0;
peakActive = 0;
2026-04-30 17:18:17 +08:00
peakActiveShort = 0;
peakActiveLong = 0;
2026-03-26 10:49:41 +08:00
for (int i = 0; i < instances.Count; i++)
{
MemoryPool.Release(instances[i]);
}
instances.Clear();
}
}
[DisallowMultipleComponent]
2026-04-30 17:18:17 +08:00
public sealed class GameObjectPoolHandle : MonoBehaviour
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
private RuntimeGameObjectPool _owner;
private int _slotIndex = -1;
private uint _generation;
internal int SlotIndex => _slotIndex;
internal uint Generation => _generation;
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
internal void Bind(RuntimeGameObjectPool owner, int slotIndex, uint generation)
2026-03-26 10:49:41 +08:00
{
_owner = owner;
2026-04-30 17:18:17 +08:00
_slotIndex = slotIndex;
_generation = generation;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
internal void Detach()
2026-03-26 10:49:41 +08:00
{
_owner = null;
2026-04-30 17:18:17 +08:00
_slotIndex = -1;
_generation = 0;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
internal bool TryRelease()
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
return _owner != null && _owner.ReleaseFromHandle(this);
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
private void OnDestroy()
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
if (_owner != null)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
_owner.NotifyHandleDestroyed(_slotIndex, _generation);
2026-04-17 21:01:20 +08:00
}
}
}
2026-04-30 17:18:17 +08:00
internal sealed class RuntimeGameObjectPool : IMemory
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
private enum SlotState : byte
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
Free = 0,
Inactive = 1,
Active = 2
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
private struct Slot
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
public GameObject instance;
public Transform transform;
public GameObjectPoolHandle handle;
public IGameObjectPoolable[] poolables;
public float spawnTime;
public float lastReleaseTime;
public int prevInactive;
public int nextInactive;
public uint generation;
public SlotState state;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
private const int PageBits = 7;
private const int PageSize = 1 << PageBits;
private const int PageMask = PageSize - 1;
private const byte PageAllocated = 1;
private const byte PageInFreeStack = 2;
private const byte PageInEmptyStack = 4;
private const int InitialPageCapacity = 4;
private const int WarmupCreateBatch = 8;
private const float WarmupFrameBudgetSeconds = 0.001f;
private const float IdleTrimDelaySeconds = 15f;
private const float PrefabUnloadDelayColdSeconds = 15f;
private const float PrefabUnloadDelaySparseSeconds = 30f;
private const float PrefabUnloadDelayHotSeconds = 90f;
private const float PrefabSparseWakeGapSeconds = 20f;
private const int PrefabLowWakeCountThreshold = 2;
private const int PrefabWarmWakeCountThreshold = 6;
private const int PeakRetainPercentShort = 120;
private const int PeakRetainPercentLong = 100;
private const int PeakDecayFramesShort = 180;
private const int PeakDecayFramesLong = 900;
private const int TrimBudgetCap = 16;
private static readonly Comparison<GameObjectPoolInstanceSnapshot> InstanceComparer = CompareInstanceSnapshot;
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
private GameObjectPoolManager _manager;
2026-03-26 10:49:41 +08:00
private IResourceLoader _loader;
2026-04-30 17:18:17 +08:00
private PoolCompiledRule _rule;
private int _poolIndex;
private string _assetPath;
2026-03-26 10:49:41 +08:00
private Transform _root;
private GameObject _prefab;
2026-04-30 17:18:17 +08:00
private UniTask<GameObject> _prefabLoadTask;
private bool _prefabLoading;
private bool _isShuttingDown;
2026-03-26 10:49:41 +08:00
private float _lastPrefabTouchTime;
2026-04-30 17:18:17 +08:00
private float _lastWakeTime;
private float _previousWakeTime;
private float _nextMaintenanceAt;
private int _maintenanceHeapIndex;
private Slot[][] _pages;
private int[][] _pageFreeStacks;
private int[] _pageAliveCounts;
private int[] _pageFreeTops;
private byte[] _pageFlags;
private int _pageCount;
private int[] _freePageStack;
private int _freePageTop;
private int[] _emptyPageStack;
private int _emptyPageTop;
private int _inactiveHead;
private int _inactiveTail;
private int _activeCount;
private int _inactiveCount;
private int _totalCount;
2026-04-17 21:01:20 +08:00
private int _runtimeHardCapacity;
2026-04-30 17:18:17 +08:00
private int _retainTarget;
2026-04-17 21:01:20 +08:00
private int _acquireCount;
private int _releaseCount;
private int _hitCount;
private int _missCount;
private int _expandCount;
private int _destroyCount;
private int _peakActive;
2026-04-30 17:18:17 +08:00
private int _peakActiveShort;
private int _peakActiveLong;
private int _acquireSinceMaintain;
private int _releaseSinceMaintain;
private int _wakeCountSincePrefabLoad;
private int _idleFrames;
private int _hotFrames;
private uint _generationCounter;
public int TotalCount => _totalCount;
public int ActiveCount => _activeCount;
public int InactiveCount => _inactiveCount;
public bool IsPrefabLoaded => _prefab != null;
public float NextMaintenanceAt => _nextMaintenanceAt;
public bool HasMaintenance => _maintenanceHeapIndex >= 0;
public float PrefabIdleDuration => _prefab == null ? 0f : Mathf.Max(0f, Time.time - GetPrefabReferenceTime());
2026-03-26 10:49:41 +08:00
public void Initialize(
2026-04-30 17:18:17 +08:00
GameObjectPoolManager manager,
int poolIndex,
in PoolCompiledRule rule,
2026-03-26 10:49:41 +08:00
string assetPath,
IResourceLoader loader,
2026-04-30 17:18:17 +08:00
Transform inactiveRoot)
{
_manager = manager;
_poolIndex = poolIndex;
_rule = rule;
_assetPath = assetPath;
_loader = loader;
_root = inactiveRoot;
_runtimeHardCapacity = rule.hardCapacity;
_retainTarget = GetMinimumRetained();
_lastPrefabTouchTime = -1f;
_lastWakeTime = -1f;
_previousWakeTime = -1f;
_nextMaintenanceAt = float.MaxValue;
_maintenanceHeapIndex = -1;
_inactiveHead = -1;
_inactiveTail = -1;
_pages = SlotArrayPool<Slot[]>.Rent(InitialPageCapacity);
_pageFreeStacks = SlotArrayPool<int[]>.Rent(InitialPageCapacity);
_pageAliveCounts = SlotArrayPool<int>.Rent(InitialPageCapacity);
_pageFreeTops = SlotArrayPool<int>.Rent(InitialPageCapacity);
_pageFlags = SlotArrayPool<byte>.Rent(InitialPageCapacity);
_freePageStack = SlotArrayPool<int>.Rent(InitialPageCapacity);
_emptyPageStack = SlotArrayPool<int>.Rent(InitialPageCapacity);
Array.Clear(_pages, 0, InitialPageCapacity);
Array.Clear(_pageFreeStacks, 0, InitialPageCapacity);
Array.Clear(_pageAliveCounts, 0, InitialPageCapacity);
Array.Clear(_pageFreeTops, 0, InitialPageCapacity);
Array.Clear(_pageFlags, 0, InitialPageCapacity);
Array.Clear(_freePageStack, 0, InitialPageCapacity);
Array.Clear(_emptyPageStack, 0, InitialPageCapacity);
ScheduleMaintenance(float.MaxValue);
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
public GameObject Acquire(in PoolSpawnContext context)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
if (!EnsurePrefabLoaded())
{
throw new InvalidOperationException(ZString.Format(
"Pool prefab '{0}' is loading asynchronously. Use async acquire for this path.",
_assetPath));
}
2026-04-17 21:01:20 +08:00
return AcquirePrepared(context);
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
public async UniTask<GameObject> AcquireAsync(PoolSpawnContext context, CancellationToken cancellationToken)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
bool prefabLoaded = await EnsurePrefabLoadedAsync(cancellationToken);
if (!prefabLoaded)
{
return null;
}
2026-04-17 21:01:20 +08:00
return AcquirePrepared(context);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
public async UniTask WarmupAsync(int count, CancellationToken cancellationToken)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
int warmCount = Mathf.Max(0, count);
if (warmCount <= 0)
2026-03-26 10:49:41 +08:00
{
return;
}
2026-04-30 17:18:17 +08:00
if (_inactiveCount >= warmCount)
2026-03-26 10:49:41 +08:00
{
return;
}
2026-04-30 17:18:17 +08:00
bool prefabLoaded = await EnsurePrefabLoadedAsync(cancellationToken);
if (!prefabLoaded)
2026-04-17 21:01:20 +08:00
{
return;
}
2026-04-30 17:18:17 +08:00
EnsureRuntimeHardCapacity(warmCount, "Warmup");
float frameBudget = WarmupFrameBudgetSeconds;
2026-04-17 21:01:20 +08:00
int createdThisFrame = 0;
2026-04-30 17:18:17 +08:00
float frameStart = Time.realtimeSinceStartup;
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
while (_inactiveCount < warmCount)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
EnsureRuntimeHardCapacity(_totalCount + 1, "Warmup");
int slotIndex = CreateTrackedInstance();
ParkInactive(slotIndex, true);
2026-04-17 21:01:20 +08:00
createdThisFrame++;
2026-04-30 17:18:17 +08:00
bool budgetReachedByCount = createdThisFrame >= WarmupCreateBatch;
bool budgetReachedByTime = frameBudget > 0f && Time.realtimeSinceStartup - frameStart >= frameBudget;
2026-04-17 21:01:20 +08:00
if (budgetReachedByCount || budgetReachedByTime)
{
createdThisFrame = 0;
2026-04-30 17:18:17 +08:00
frameStart = Time.realtimeSinceStartup;
2026-04-17 21:01:20 +08:00
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
}
}
2026-04-30 17:18:17 +08:00
RefreshMaintenance(false);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
public bool ReleaseFromHandle(GameObjectPoolHandle handle)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
if (handle == null)
2026-03-26 10:49:41 +08:00
{
return false;
}
2026-04-30 17:18:17 +08:00
int slotIndex = handle.SlotIndex;
if (!IsValidIndex(slotIndex))
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
return false;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
ref Slot slot = ref GetSlotRef(slotIndex);
if (slot.handle != handle || slot.generation != handle.Generation || slot.state != SlotState.Active)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
return false;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
ReleaseTrackedInstance(slotIndex);
return true;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
public void NotifyHandleDestroyed(int slotIndex, uint generation)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
if (_isShuttingDown || !IsValidIndex(slotIndex))
2026-03-26 10:49:41 +08:00
{
2026-04-17 21:01:20 +08:00
return;
}
2026-04-30 17:18:17 +08:00
ref Slot slot = ref GetSlotRef(slotIndex);
if (slot.generation != generation)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
return;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
RemoveSlot(slotIndex, false);
}
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
public void SetMaintenanceHeapIndex(int heapIndex)
{
_maintenanceHeapIndex = heapIndex;
}
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
public void ExecuteMaintenance(float now, bool lowMemory)
{
UpdateRetentionMetrics();
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
PoolRecyclePlan plan = BuildRecyclePlan(now, lowMemory);
_retainTarget = Mathf.Clamp(plan.RetainTarget, GetMinimumRetained(), _runtimeHardCapacity);
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
int destroyBudget = Mathf.Max(1, plan.TrimBudget);
while (_inactiveHead >= 0 && destroyBudget > 0 && ShouldTrimHead(now, plan))
{
DestroyTrackedInstance(_inactiveHead, true);
2026-04-17 21:01:20 +08:00
destroyBudget--;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
RestoreRuntimeHardCapacity();
if (_prefab != null && ShouldUnloadPrefab(now, plan))
2026-03-26 10:49:41 +08:00
{
_loader.UnloadAsset(_prefab);
_prefab = null;
2026-04-30 17:18:17 +08:00
_prefabLoading = false;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
RefreshMaintenance(false);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
public void Shutdown()
{
2026-04-30 17:18:17 +08:00
_isShuttingDown = true;
_manager.RemoveMaintenance(ref _maintenanceHeapIndex);
2026-04-30 17:18:17 +08:00
for (int page = 0; page < _pageCount; page++)
{
2026-04-30 17:18:17 +08:00
Slot[] pageSlots = _pages[page];
if (pageSlots == null)
{
continue;
}
2026-04-30 17:18:17 +08:00
for (int offset = 0; offset < PageSize; offset++)
{
int slotIndex = MakeIndex(page, offset);
ref Slot slot = ref pageSlots[offset];
if (slot.state == SlotState.Free && slot.instance == null)
{
continue;
}
InvokeOnPoolDestroy(ref slot);
if (slot.handle != null)
{
slot.handle.Detach();
}
if (slot.instance != null)
{
GameObject.Destroy(slot.instance);
}
ClearSlot(ref slot);
slot.state = SlotState.Free;
slot.prevInactive = -1;
slot.nextInactive = -1;
_totalCount--;
_destroyCount++;
}
}
2026-04-30 17:18:17 +08:00
_inactiveHead = -1;
_inactiveTail = -1;
_activeCount = 0;
_inactiveCount = 0;
_totalCount = 0;
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
if (_prefab != null)
{
_loader.UnloadAsset(_prefab);
_prefab = null;
}
}
2026-03-26 10:49:41 +08:00
public GameObjectPoolSnapshot CreateSnapshot()
{
2026-04-30 17:18:17 +08:00
float now = Time.time;
2026-03-26 10:49:41 +08:00
var snapshot = MemoryPool.Acquire<GameObjectPoolSnapshot>();
2026-04-30 17:18:17 +08:00
snapshot.entryName = _rule.entryName;
snapshot.group = _rule.group;
2026-03-26 10:49:41 +08:00
snapshot.assetPath = _assetPath;
2026-04-30 17:18:17 +08:00
snapshot.category = _rule.category;
snapshot.loaderType = _rule.loaderType;
snapshot.minRetained = GetMinimumRetained();
snapshot.retainTarget = _retainTarget;
snapshot.softCapacity = _rule.softCapacity;
snapshot.hardCapacity = _rule.hardCapacity;
snapshot.runtimeHardCapacity = _runtimeHardCapacity;
snapshot.trimPerTick = GetTrimBudgetBase();
snapshot.shortPeakRetainPercent = PeakRetainPercentShort;
snapshot.longPeakRetainPercent = PeakRetainPercentLong;
snapshot.shortPeakDecayFrames = PeakDecayFramesShort;
snapshot.longPeakDecayFrames = PeakDecayFramesLong;
snapshot.trimMultiplier = 1;
snapshot.retainAllInactive = false;
snapshot.totalCount = _totalCount;
2026-03-26 10:49:41 +08:00
snapshot.activeCount = _activeCount;
2026-04-30 17:18:17 +08:00
snapshot.inactiveCount = _inactiveCount;
2026-03-26 10:49:41 +08:00
snapshot.prefabLoaded = _prefab != null;
snapshot.prefabIdleDuration = PrefabIdleDuration;
2026-04-30 17:18:17 +08:00
snapshot.prefabWakeCount = _prefab == null ? 0 : _wakeCountSincePrefabLoad;
snapshot.prefabWakeGap = _prefab == null ? -1f : GetLastWakeGapDuration();
snapshot.prefabUnloadDelay = _prefab == null ? -1f : GetPrefabUnloadDelaySeconds();
snapshot.nextMaintenanceIn = _nextMaintenanceAt >= float.MaxValue
? -1f
: Mathf.Max(0f, _nextMaintenanceAt - now);
2026-04-17 21:01:20 +08:00
snapshot.acquireCount = _acquireCount;
snapshot.releaseCount = _releaseCount;
snapshot.hitCount = _hitCount;
snapshot.missCount = _missCount;
snapshot.expandCount = _expandCount;
snapshot.destroyCount = _destroyCount;
snapshot.peakActive = _peakActive;
2026-04-30 17:18:17 +08:00
snapshot.peakActiveShort = _peakActiveShort;
snapshot.peakActiveLong = _peakActiveLong;
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
for (int page = 0; page < _pageCount; page++)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
Slot[] pageSlots = _pages[page];
if (pageSlots == null)
{
continue;
}
for (int offset = 0; offset < PageSize; offset++)
{
ref Slot slot = ref pageSlots[offset];
if (slot.state == SlotState.Free && slot.instance == null)
{
continue;
}
var instanceSnapshot = MemoryPool.Acquire<GameObjectPoolInstanceSnapshot>();
instanceSnapshot.instanceName = slot.instance == null ? "<destroyed>" : slot.instance.name;
instanceSnapshot.isActive = slot.state == SlotState.Active;
instanceSnapshot.idleDuration = slot.state == SlotState.Active
? 0f
: Mathf.Max(0f, now - slot.lastReleaseTime);
instanceSnapshot.lifeDuration = Mathf.Max(0f, now - slot.spawnTime);
instanceSnapshot.gameObject = slot.instance;
snapshot.instances.Add(instanceSnapshot);
}
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
snapshot.instances.Sort(InstanceComparer);
2026-03-26 10:49:41 +08:00
return snapshot;
}
2026-04-30 17:18:17 +08:00
public void Clear()
{
ReturnAllPages();
ReturnStorageArrays();
_manager = null;
_loader = null;
_rule = default;
_poolIndex = 0;
_assetPath = null;
_root = null;
_prefab = null;
_prefabLoadTask = default;
_prefabLoading = false;
_isShuttingDown = false;
_lastPrefabTouchTime = -1f;
_lastWakeTime = -1f;
_previousWakeTime = -1f;
_nextMaintenanceAt = float.MaxValue;
_maintenanceHeapIndex = -1;
_inactiveHead = -1;
_inactiveTail = -1;
_activeCount = 0;
_inactiveCount = 0;
_totalCount = 0;
_runtimeHardCapacity = 0;
_retainTarget = 0;
_acquireCount = 0;
_releaseCount = 0;
_hitCount = 0;
_missCount = 0;
_expandCount = 0;
_destroyCount = 0;
_peakActive = 0;
_peakActiveShort = 0;
_peakActiveLong = 0;
_acquireSinceMaintain = 0;
_releaseSinceMaintain = 0;
_wakeCountSincePrefabLoad = 0;
_idleFrames = 0;
_hotFrames = 0;
_generationCounter = 0;
}
private GameObject AcquirePrepared(in PoolSpawnContext context)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
_acquireCount++;
_acquireSinceMaintain++;
int slotIndex;
if (_inactiveTail >= 0)
{
slotIndex = _inactiveTail;
RemoveFromInactive(slotIndex);
_hitCount++;
}
else
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
_missCount++;
slotIndex = AcquireOnMiss();
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
ActivateTrackedInstance(slotIndex, context);
if (_activeCount > _peakActive)
{
_peakActive = _activeCount;
}
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
if (_activeCount > _peakActiveShort)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
_peakActiveShort = _activeCount;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
if (_activeCount > _peakActiveLong)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
_peakActiveLong = _activeCount;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
RefreshMaintenance(false);
return GetSlotRef(slotIndex).instance;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
private int AcquireOnMiss()
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
EnsureRuntimeHardCapacity(_totalCount + 1, "Acquire");
return CreateTrackedInstance();
}
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
private void ActivateTrackedInstance(int slotIndex, in PoolSpawnContext context)
{
ref Slot slot = ref GetSlotRef(slotIndex);
slot.state = SlotState.Active;
_activeCount++;
RecordWake();
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
Transform transform = slot.transform;
transform.SetParent(context.Parent, false);
if (!slot.instance.activeSelf)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
slot.instance.SetActive(true);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
InvokeOnPoolGet(ref slot, context);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
private void ReleaseTrackedInstance(int slotIndex)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
ref Slot slot = ref GetSlotRef(slotIndex);
if (slot.state != SlotState.Active)
2026-03-26 10:49:41 +08:00
{
return;
}
2026-04-30 17:18:17 +08:00
_releaseCount++;
_releaseSinceMaintain++;
_activeCount = Mathf.Max(0, _activeCount - 1);
InvokeOnPoolRelease(ref slot);
if (slot.instance.activeSelf)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
slot.instance.SetActive(false);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
ParkInactive(slotIndex, false);
RefreshMaintenance(false);
}
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
private void ParkInactive(int slotIndex, bool newInstance)
{
ref Slot slot = ref GetSlotRef(slotIndex);
slot.state = SlotState.Inactive;
slot.lastReleaseTime = Time.time;
slot.transform.SetParent(_root, false);
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
AddToInactiveTail(slotIndex);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
private int CreateTrackedInstance()
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
int slotIndex = AllocSlot();
ref Slot slot = ref GetSlotRef(slotIndex);
slot.generation = ++_generationCounter;
slot.state = SlotState.Inactive;
slot.spawnTime = Time.time;
slot.lastReleaseTime = Time.time;
slot.prevInactive = -1;
slot.nextInactive = -1;
slot.instance = GameObject.Instantiate(_prefab);
slot.transform = slot.instance.transform;
slot.instance.name = ZString.Format("{0}[Pool]", _prefab.name);
slot.transform.SetParent(_root, false);
if (slot.instance.activeSelf)
{
slot.instance.SetActive(false);
}
GameObjectPoolHandle handle = slot.instance.GetComponent<GameObjectPoolHandle>();
if (handle == null)
{
handle = slot.instance.AddComponent<GameObjectPoolHandle>();
}
handle.Bind(this, slotIndex, slot.generation);
slot.handle = handle;
BuildLifecycleCache(slot.instance, ref slot);
InvokeOnPoolCreate(ref slot);
_totalCount++;
TouchPrefab();
return slotIndex;
}
private void DestroyTrackedInstance(int slotIndex, bool countDestroy)
{
ref Slot slot = ref GetSlotRef(slotIndex);
RemoveFromInactive(slotIndex);
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
if (slot.state == SlotState.Active)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
_activeCount = Mathf.Max(0, _activeCount - 1);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
InvokeOnPoolDestroy(ref slot);
if (slot.handle != null)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
slot.handle.Detach();
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
if (slot.instance != null)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
GameObject.Destroy(slot.instance);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
ClearSlot(ref slot);
slot.state = SlotState.Free;
slot.prevInactive = -1;
slot.nextInactive = -1;
FreeSlot(slotIndex);
_totalCount = Mathf.Max(0, _totalCount - 1);
if (countDestroy)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
_destroyCount++;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
}
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
private void RemoveSlot(int slotIndex, bool countDestroy)
{
ref Slot slot = ref GetSlotRef(slotIndex);
RemoveFromInactive(slotIndex);
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
if (slot.state == SlotState.Active)
{
_activeCount = Mathf.Max(0, _activeCount - 1);
}
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
ClearSlot(ref slot);
slot.state = SlotState.Free;
slot.prevInactive = -1;
slot.nextInactive = -1;
FreeSlot(slotIndex);
_totalCount = Mathf.Max(0, _totalCount - 1);
if (countDestroy)
{
_destroyCount++;
}
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
RestoreRuntimeHardCapacity();
RefreshMaintenance(false);
}
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
private void RefreshMaintenance(bool lowMemory)
{
float dueTime = ComputeNextMaintenanceAt(Time.time, lowMemory);
ScheduleMaintenance(dueTime);
}
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
private void ScheduleMaintenance(float dueTime)
{
_nextMaintenanceAt = dueTime;
_manager.ScheduleMaintenance(_poolIndex, dueTime, ref _maintenanceHeapIndex);
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
private float ComputeNextMaintenanceAt(float now, bool lowMemory)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
if (lowMemory && _inactiveHead >= 0)
{
return now;
}
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
float nextDueTime = float.MaxValue;
if (_inactiveHead >= 0)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
if (_totalCount > _rule.softCapacity)
{
return now;
}
ref Slot headSlot = ref GetSlotRef(_inactiveHead);
nextDueTime = headSlot.lastReleaseTime + IdleTrimDelaySeconds;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
if (_prefab != null && _totalCount == 0)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
float unloadDue = GetPrefabUnloadDueTime();
if (unloadDue < nextDueTime)
{
nextDueTime = unloadDue;
}
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
return nextDueTime;
}
private PoolRecyclePlan BuildRecyclePlan(float now, bool lowMemory)
{
float oldestIdle = _inactiveHead >= 0
? Mathf.Max(0f, now - GetSlotRef(_inactiveHead).lastReleaseTime)
: 0f;
var context = new PoolRecycleRuntimeContext(
_rule.group,
_assetPath,
_rule.category,
_activeCount,
_inactiveCount,
_totalCount,
GetMinimumRetained(),
_rule.softCapacity,
_runtimeHardCapacity,
PeakRetainPercentShort,
PeakRetainPercentLong,
PeakDecayFramesShort,
PeakDecayFramesLong,
1,
false,
_peakActiveShort,
_peakActiveLong,
_idleFrames,
_hotFrames,
oldestIdle,
lowMemory);
return BuildDefaultRecyclePlan(in context);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
private PoolRecyclePlan BuildDefaultRecyclePlan(in PoolRecycleRuntimeContext context)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
if (_idleFrames >= context.ShortPeakDecayFrames && _peakActiveShort > 0)
{
_peakActiveShort -= Mathf.Max(1, _peakActiveShort >> 4);
}
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
if (_idleFrames >= context.LongPeakDecayFrames && _peakActiveLong > 0)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
_peakActiveLong -= Mathf.Max(1, _peakActiveLong >> 6);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
int shortRetain = (_peakActiveShort * context.ShortPeakRetainPercent + 99) / 100;
int longRetain = (_peakActiveLong * context.LongPeakRetainPercent + 99) / 100;
int retainTarget = Mathf.Max(context.MinRetained, Mathf.Max(shortRetain, longRetain));
retainTarget = Mathf.Min(retainTarget, context.SoftCapacity);
if (context.RetainAllInactive)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
retainTarget = Mathf.Max(retainTarget, context.InactiveCount);
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
if (context.LowMemory)
{
retainTarget = context.MinRetained;
}
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
int trimBudget = GetTrimBudgetBase();
bool forceTrim = context.LowMemory;
bool forceUnload = context.LowMemory;
return new PoolRecyclePlan(retainTarget, trimBudget, forceTrim, forceUnload);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
private bool ShouldTrimHead(float now, in PoolRecyclePlan plan)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
if (_inactiveHead < 0)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
return false;
}
if (_totalCount <= plan.RetainTarget)
{
return false;
}
if (plan.ForceTrim)
{
return true;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
if (_totalCount > _rule.softCapacity)
{
return true;
}
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
ref Slot headSlot = ref GetSlotRef(_inactiveHead);
return _inactiveCount > plan.RetainTarget && now - headSlot.lastReleaseTime >= IdleTrimDelaySeconds;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
private bool ShouldUnloadPrefab(float now, in PoolRecyclePlan plan)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
if (_prefab == null || _totalCount > 0)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
return false;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
if (plan.ForcePrefabUnload)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
return true;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
return now >= GetPrefabUnloadDueTime();
}
private void UpdateRetentionMetrics()
{
bool hot = _acquireSinceMaintain > 0 || _releaseSinceMaintain > 0 || _activeCount > 0;
if (hot)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
_hotFrames++;
_idleFrames = 0;
}
else
{
_idleFrames++;
_hotFrames = 0;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
_acquireSinceMaintain = 0;
_releaseSinceMaintain = 0;
}
private void RestoreRuntimeHardCapacity()
{
_runtimeHardCapacity = Mathf.Max(_rule.hardCapacity, _totalCount);
}
private int GetMinimumRetained()
{
return 0;
}
private int GetTrimBudgetBase()
{
return Mathf.Clamp(_rule.softCapacity >> 2, 1, TrimBudgetCap);
}
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
private void EnsureRuntimeHardCapacity(int requiredCapacity, string reason)
{
if (requiredCapacity <= _runtimeHardCapacity)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
return;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
int previousRuntimeHardCapacity = _runtimeHardCapacity;
int expandedBy = requiredCapacity - previousRuntimeHardCapacity;
_runtimeHardCapacity = requiredCapacity;
_expandCount += expandedBy;
Log.Warning(ZString.Format(
"[GameObjectPool] Pool expanded ({0}). Rule:{1}, Asset:{2}, ConfigHard:{3}, RuntimeHard:{4}->{5}, ExpandCount:{6}",
reason,
_rule.entryName,
_assetPath,
_rule.hardCapacity,
previousRuntimeHardCapacity,
_runtimeHardCapacity,
_expandCount));
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
private bool EnsurePrefabLoaded()
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
if (_prefab != null)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
TouchPrefab();
return true;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
if (_prefabLoading)
{
return false;
}
2026-03-26 10:49:41 +08:00
2026-04-30 17:18:17 +08:00
_prefab = _loader.LoadPrefab(_assetPath);
_prefabLoading = false;
if (_prefab != null)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
ResetPrefabUsageWindow();
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
return _prefab != null;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
private async UniTask<bool> EnsurePrefabLoadedAsync(CancellationToken cancellationToken)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
if (_prefab != null)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
TouchPrefab();
return true;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
if (_prefabLoading)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
GameObject loadedPrefab = await _prefabLoadTask.AttachExternalCancellation(cancellationToken);
bool firstCompletion = _prefab == null;
_prefab = loadedPrefab;
_prefabLoading = false;
if (_prefab != null)
{
if (firstCompletion)
{
ResetPrefabUsageWindow();
}
else
{
TouchPrefab();
}
}
return _prefab != null;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
_prefabLoading = true;
_prefabLoadTask = _loader.LoadPrefabAsync(_assetPath, _manager.ShutdownToken);
_prefab = await _prefabLoadTask.AttachExternalCancellation(cancellationToken);
_prefabLoading = false;
if (_prefab != null)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
ResetPrefabUsageWindow();
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
return _prefab != null;
}
private void TouchPrefab()
{
_lastPrefabTouchTime = Time.time;
}
private void ResetPrefabUsageWindow()
{
_lastPrefabTouchTime = Time.time;
_lastWakeTime = -1f;
_previousWakeTime = -1f;
_wakeCountSincePrefabLoad = 0;
}
private void RecordWake()
{
float now = Time.time;
_previousWakeTime = _lastWakeTime;
_lastWakeTime = now;
_wakeCountSincePrefabLoad++;
_lastPrefabTouchTime = now;
}
private float GetPrefabReferenceTime()
{
return _lastWakeTime >= 0f ? _lastWakeTime : _lastPrefabTouchTime;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
private float GetLastWakeGapDuration()
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
if (_lastWakeTime < 0f || _previousWakeTime < 0f)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
return -1f;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
return Mathf.Max(0f, _lastWakeTime - _previousWakeTime);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
// Empty pools unload faster after sparse wake patterns, and stay loaded longer after bursty use.
private float GetPrefabUnloadDelaySeconds()
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
if (_wakeCountSincePrefabLoad <= 0)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
return PrefabUnloadDelayColdSeconds;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
float lastWakeGap = GetLastWakeGapDuration();
bool sparseWakeGap = lastWakeGap < 0f || lastWakeGap >= PrefabSparseWakeGapSeconds;
if (_wakeCountSincePrefabLoad <= PrefabLowWakeCountThreshold)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
return sparseWakeGap
? PrefabUnloadDelayColdSeconds
: PrefabUnloadDelaySparseSeconds;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
if (_wakeCountSincePrefabLoad <= PrefabWarmWakeCountThreshold && sparseWakeGap)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
return PrefabUnloadDelaySparseSeconds;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
return PrefabUnloadDelayHotSeconds;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
private float GetPrefabUnloadDueTime()
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
if (_prefab == null || _totalCount > 0)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
return float.MaxValue;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
return GetPrefabReferenceTime() + GetPrefabUnloadDelaySeconds();
}
private static void BuildLifecycleCache(GameObject gameObject, ref Slot slot)
{
MonoBehaviour[] behaviours = gameObject.GetComponentsInChildren<MonoBehaviour>(true);
int poolableCount = 0;
2026-04-17 21:01:20 +08:00
2026-04-30 17:18:17 +08:00
for (int i = 0; i < behaviours.Length; i++)
{
MonoBehaviour behaviour = behaviours[i];
if (behaviour == null)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
continue;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
if (behaviour is IGameObjectPoolable)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
poolableCount++;
2026-04-17 21:01:20 +08:00
}
}
2026-04-30 17:18:17 +08:00
slot.poolables = poolableCount == 0 ? null : new IGameObjectPoolable[poolableCount];
int poolableIndex = 0;
for (int i = 0; i < behaviours.Length; i++)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
MonoBehaviour behaviour = behaviours[i];
if (behaviour == null)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
continue;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
if (behaviour is IGameObjectPoolable poolable)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
slot.poolables[poolableIndex++] = poolable;
2026-04-17 21:01:20 +08:00
}
}
}
2026-04-30 17:18:17 +08:00
private void InvokeOnPoolCreate(ref Slot slot)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
if (slot.poolables == null)
2026-04-17 21:01:20 +08:00
{
return;
}
2026-04-30 17:18:17 +08:00
for (int i = 0; i < slot.poolables.Length; i++)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
slot.poolables[i]?.OnPoolCreate();
2026-04-17 21:01:20 +08:00
}
}
2026-04-30 17:18:17 +08:00
private void InvokeOnPoolGet(ref Slot slot, in PoolSpawnContext context)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
if (slot.poolables == null)
2026-04-17 21:01:20 +08:00
{
return;
}
2026-04-30 17:18:17 +08:00
for (int i = 0; i < slot.poolables.Length; i++)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
slot.poolables[i]?.OnPoolGet(context);
2026-04-17 21:01:20 +08:00
}
}
2026-04-30 17:18:17 +08:00
private void InvokeOnPoolRelease(ref Slot slot)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
if (slot.poolables == null)
2026-04-17 21:01:20 +08:00
{
return;
}
2026-04-30 17:18:17 +08:00
for (int i = 0; i < slot.poolables.Length; i++)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
slot.poolables[i]?.OnPoolRelease();
}
}
private void InvokeOnPoolDestroy(ref Slot slot)
{
if (slot.poolables == null)
{
return;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
for (int i = 0; i < slot.poolables.Length; i++)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
slot.poolables[i]?.OnPoolDestroy();
2026-04-17 21:01:20 +08:00
}
}
2026-04-30 17:18:17 +08:00
private static void ClearSlot(ref Slot slot)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
slot.instance = null;
slot.transform = null;
slot.handle = null;
slot.poolables = null;
slot.spawnTime = 0f;
slot.lastReleaseTime = 0f;
slot.generation++;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
private void AddToInactiveTail(int slotIndex)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
ref Slot slot = ref GetSlotRef(slotIndex);
if (_inactiveTail == slotIndex || slot.prevInactive >= 0 || slot.nextInactive >= 0)
2026-04-17 21:01:20 +08:00
{
return;
}
2026-04-30 17:18:17 +08:00
slot.prevInactive = _inactiveTail;
slot.nextInactive = -1;
if (_inactiveTail >= 0)
{
GetSlotRef(_inactiveTail).nextInactive = slotIndex;
}
else
{
_inactiveHead = slotIndex;
}
_inactiveTail = slotIndex;
_inactiveCount++;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
private void RemoveFromInactive(int slotIndex)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
if (!IsValidIndex(slotIndex))
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
return;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
ref Slot slot = ref GetSlotRef(slotIndex);
if (_inactiveHead != slotIndex && slot.prevInactive < 0 && slot.nextInactive < 0)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
return;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
int prev = slot.prevInactive;
int next = slot.nextInactive;
if (prev >= 0)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
GetSlotRef(prev).nextInactive = next;
}
else
{
_inactiveHead = next;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
if (next >= 0)
2026-04-17 21:01:20 +08:00
{
2026-04-30 17:18:17 +08:00
GetSlotRef(next).prevInactive = prev;
}
else
{
_inactiveTail = prev;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
slot.prevInactive = -1;
slot.nextInactive = -1;
_inactiveCount = Mathf.Max(0, _inactiveCount - 1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int MakeIndex(int page, int offset)
{
return (page << PageBits) | offset;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int PageOf(int index)
{
return index >> PageBits;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int OffsetOf(int index)
{
return index & PageMask;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref Slot GetSlotRef(int index)
{
return ref _pages[PageOf(index)][OffsetOf(index)];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsValidIndex(int index)
{
if (index < 0)
2026-04-17 21:01:20 +08:00
{
return false;
}
2026-04-30 17:18:17 +08:00
int page = PageOf(index);
return page < _pageCount && _pages[page] != null;
2026-04-17 21:01:20 +08:00
}
2026-04-30 17:18:17 +08:00
private int AllocSlot()
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
if (_freePageTop <= 0)
{
AllocatePage();
}
int page = _freePageStack[_freePageTop - 1];
int offset = _pageFreeStacks[page][--_pageFreeTops[page]];
if (_pageFreeTops[page] <= 0)
{
RemoveFreePage(page);
}
_pageAliveCounts[page]++;
return MakeIndex(page, offset);
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
private void FreeSlot(int index)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
int page = PageOf(index);
int offset = OffsetOf(index);
_pageFreeStacks[page][_pageFreeTops[page]++] = offset;
_pageAliveCounts[page]--;
if (_pageFreeTops[page] == 1)
{
AddFreePage(page);
}
if (_pageAliveCounts[page] == 0)
{
AddEmptyPage(page);
ReleaseEmptyPages(1);
}
}
private void AllocatePage()
{
int page = GetReusablePageIndex();
_pages[page] = SlotArrayPool<Slot>.Rent(PageSize);
_pageFreeStacks[page] = SlotArrayPool<int>.Rent(PageSize);
Array.Clear(_pages[page], 0, PageSize);
for (int i = 0; i < PageSize; i++)
{
_pageFreeStacks[page][i] = PageSize - 1 - i;
_pages[page][i].prevInactive = -1;
_pages[page][i].nextInactive = -1;
}
_pageFreeTops[page] = PageSize;
_pageAliveCounts[page] = 0;
_pageFlags[page] = PageAllocated;
AddFreePage(page);
}
private int GetReusablePageIndex()
{
if (_emptyPageTop > 0)
{
int page = _emptyPageStack[--_emptyPageTop];
_pageFlags[page] = (byte)(_pageFlags[page] & ~PageInEmptyStack);
return page;
}
if (_pageCount >= _pages.Length)
{
ExpandPageStorage(_pages.Length << 1);
}
return _pageCount++;
}
private void ExpandPageStorage(int required)
{
int newCapacity = Mathf.Max(required, _pages.Length << 1);
Slot[][] newPages = SlotArrayPool<Slot[]>.Rent(newCapacity);
int[][] newPageFreeStacks = SlotArrayPool<int[]>.Rent(newCapacity);
int[] newPageAliveCounts = SlotArrayPool<int>.Rent(newCapacity);
int[] newPageFreeTops = SlotArrayPool<int>.Rent(newCapacity);
byte[] newPageFlags = SlotArrayPool<byte>.Rent(newCapacity);
int[] newFreePageStack = SlotArrayPool<int>.Rent(newCapacity);
int[] newEmptyPageStack = SlotArrayPool<int>.Rent(newCapacity);
Array.Clear(newPages, 0, newCapacity);
Array.Clear(newPageFreeStacks, 0, newCapacity);
Array.Clear(newPageAliveCounts, 0, newCapacity);
Array.Clear(newPageFreeTops, 0, newCapacity);
Array.Clear(newPageFlags, 0, newCapacity);
Array.Clear(newFreePageStack, 0, newCapacity);
Array.Clear(newEmptyPageStack, 0, newCapacity);
Array.Copy(_pages, 0, newPages, 0, _pageCount);
Array.Copy(_pageFreeStacks, 0, newPageFreeStacks, 0, _pageCount);
Array.Copy(_pageAliveCounts, 0, newPageAliveCounts, 0, _pageCount);
Array.Copy(_pageFreeTops, 0, newPageFreeTops, 0, _pageCount);
Array.Copy(_pageFlags, 0, newPageFlags, 0, _pageCount);
Array.Copy(_freePageStack, 0, newFreePageStack, 0, _freePageTop);
Array.Copy(_emptyPageStack, 0, newEmptyPageStack, 0, _emptyPageTop);
SlotArrayPool<Slot[]>.Return(_pages, true);
SlotArrayPool<int[]>.Return(_pageFreeStacks, true);
SlotArrayPool<int>.Return(_pageAliveCounts, true);
SlotArrayPool<int>.Return(_pageFreeTops, true);
SlotArrayPool<byte>.Return(_pageFlags, true);
SlotArrayPool<int>.Return(_freePageStack, true);
SlotArrayPool<int>.Return(_emptyPageStack, true);
_pages = newPages;
_pageFreeStacks = newPageFreeStacks;
_pageAliveCounts = newPageAliveCounts;
_pageFreeTops = newPageFreeTops;
_pageFlags = newPageFlags;
_freePageStack = newFreePageStack;
_emptyPageStack = newEmptyPageStack;
}
private void AddFreePage(int page)
{
if ((_pageFlags[page] & PageInFreeStack) != 0)
{
return;
}
_pageFlags[page] = (byte)(_pageFlags[page] | PageInFreeStack);
_freePageStack[_freePageTop++] = page;
2026-03-26 10:49:41 +08:00
}
2026-04-30 17:18:17 +08:00
private void RemoveFreePage(int page)
2026-03-26 10:49:41 +08:00
{
2026-04-30 17:18:17 +08:00
if ((_pageFlags[page] & PageInFreeStack) == 0)
{
return;
}
_pageFlags[page] = (byte)(_pageFlags[page] & ~PageInFreeStack);
for (int i = 0; i < _freePageTop; i++)
{
if (_freePageStack[i] != page)
{
continue;
}
_freePageTop--;
_freePageStack[i] = _freePageStack[_freePageTop];
_freePageStack[_freePageTop] = 0;
return;
}
}
private void AddEmptyPage(int page)
{
if ((_pageFlags[page] & PageInEmptyStack) != 0)
{
return;
}
_pageFlags[page] = (byte)(_pageFlags[page] | PageInEmptyStack);
_emptyPageStack[_emptyPageTop++] = page;
}
private void ReleaseEmptyPages(int budget)
{
while (budget > 0 && _emptyPageTop > 0)
{
int page = _emptyPageStack[--_emptyPageTop];
_pageFlags[page] = (byte)(_pageFlags[page] & ~PageInEmptyStack);
if (_pages[page] == null || _pageAliveCounts[page] != 0)
{
continue;
}
SlotArrayPool<Slot>.Return(_pages[page], true);
SlotArrayPool<int>.Return(_pageFreeStacks[page], true);
_pages[page] = null;
_pageFreeStacks[page] = null;
_pageFreeTops[page] = 0;
_pageAliveCounts[page] = 0;
_pageFlags[page] = 0;
budget--;
}
}
private void ReturnAllPages()
{
if (_pages == null)
{
return;
}
for (int page = 0; page < _pageCount; page++)
{
if (_pages[page] != null)
{
SlotArrayPool<Slot>.Return(_pages[page], true);
_pages[page] = null;
}
if (_pageFreeStacks[page] != null)
{
SlotArrayPool<int>.Return(_pageFreeStacks[page], true);
_pageFreeStacks[page] = null;
}
}
}
private void ReturnStorageArrays()
{
if (_pages != null)
{
SlotArrayPool<Slot[]>.Return(_pages, true);
_pages = null;
}
if (_pageFreeStacks != null)
{
SlotArrayPool<int[]>.Return(_pageFreeStacks, true);
_pageFreeStacks = null;
}
if (_pageAliveCounts != null)
{
SlotArrayPool<int>.Return(_pageAliveCounts, true);
_pageAliveCounts = null;
}
if (_pageFreeTops != null)
{
SlotArrayPool<int>.Return(_pageFreeTops, true);
_pageFreeTops = null;
}
if (_pageFlags != null)
{
SlotArrayPool<byte>.Return(_pageFlags, true);
_pageFlags = null;
}
if (_freePageStack != null)
{
SlotArrayPool<int>.Return(_freePageStack, true);
_freePageStack = null;
}
if (_emptyPageStack != null)
{
SlotArrayPool<int>.Return(_emptyPageStack, true);
_emptyPageStack = null;
}
_pageCount = 0;
_freePageTop = 0;
_emptyPageTop = 0;
}
private static int CompareInstanceSnapshot(GameObjectPoolInstanceSnapshot left, GameObjectPoolInstanceSnapshot right)
{
if (ReferenceEquals(left, right))
{
return 0;
}
if (left == null)
{
return 1;
}
if (right == null)
{
return -1;
}
if (left.isActive != right.isActive)
{
return left.isActive ? -1 : 1;
}
return string.Compare(left.instanceName, right.instanceName, StringComparison.Ordinal);
2026-03-26 10:49:41 +08:00
}
}
}