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-03-31 17:25:20 +08:00
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
_isShuttingDown = true;
|
|
|
|
|
_manager.RemoveMaintenance(ref _maintenanceHeapIndex);
|
2026-03-31 17:25:20 +08:00
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
for (int page = 0; page < _pageCount; page++)
|
2026-03-31 17:25:20 +08:00
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
Slot[] pageSlots = _pages[page];
|
|
|
|
|
if (pageSlots == null)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-03-31 17:25:20 +08:00
|
|
|
|
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-03-31 17:25:20 +08:00
|
|
|
}
|
|
|
|
|
|
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-31 17:25:20 +08:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|