[Opt] AppService架构 UI加载&调试 ObjectPool优化 Timer模块优化

This commit is contained in:
陈思海 2026-04-28 18:25:04 +08:00
parent 8cc5025796
commit de926116aa
44 changed files with 3344 additions and 1747 deletions

View File

@ -8,38 +8,46 @@ namespace AlicizaX.Timer.Editor
[CustomEditor(typeof(TimerComponent))] [CustomEditor(typeof(TimerComponent))]
internal sealed class TimerComponentInspector : GameFrameworkInspector internal sealed class TimerComponentInspector : GameFrameworkInspector
{ {
private const double UPDATE_INTERVAL = 0.25d; private const double UPDATE_INTERVAL = 0.02d;
private const int MAX_DISPLAY_COUNT = 20; private const int DISPLAY_COUNT = 32;
private const int MIN_INITIAL_CAPACITY = 256;
private const int MAX_INITIAL_CAPACITY = 16384;
private const int CAPACITY_STEP = 256;
private TimerDebugInfo[] _timerBuffer; private readonly TimerDebugInfo[] _timerBuffer = new TimerDebugInfo[DISPLAY_COUNT];
#if UNITY_EDITOR private readonly TimerDebugInfo[] _staleBuffer = new TimerDebugInfo[DISPLAY_COUNT];
private TimerDebugInfo[] _leakBuffer;
#endif
private double _lastUpdateTime; private double _lastUpdateTime;
private int _cachedActiveCount; private SerializedProperty _initialCapacityProperty;
private int _cachedPoolCapacity;
private int _cachedPeakActiveCount;
private int _cachedFreeCount;
private string _cachedUsageText;
public override void OnInspectorGUI() public override void OnInspectorGUI()
{ {
base.OnInspectorGUI(); base.OnInspectorGUI();
serializedObject.Update(); serializedObject.Update();
DrawConfiguration();
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
DrawRuntimeDebugInfo(); DrawRuntimeDebugInfo();
RequestRuntimeRepaint();
}
if (EditorApplication.isPlaying) private void OnEnable()
{
_initialCapacityProperty = serializedObject.FindProperty("_initialCapacity");
}
private void DrawConfiguration()
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Configuration", EditorStyles.boldLabel);
int capacity = _initialCapacityProperty.intValue;
int sliderValue = EditorGUILayout.IntSlider("Initial Capacity", capacity, MIN_INITIAL_CAPACITY, MAX_INITIAL_CAPACITY);
sliderValue = AlignCapacity(sliderValue);
if (sliderValue != capacity)
{ {
double currentTime = EditorApplication.timeSinceStartup; _initialCapacityProperty.intValue = sliderValue;
if (currentTime - _lastUpdateTime >= UPDATE_INTERVAL)
{
_lastUpdateTime = currentTime;
Repaint();
}
} }
EditorGUILayout.HelpBox(Utility.Text.Format("Rounded by {0}. Runtime allocates timer pages during Awake/prewarm.", CAPACITY_STEP), MessageType.None);
} }
private void DrawRuntimeDebugInfo() private void DrawRuntimeDebugInfo()
@ -56,30 +64,31 @@ namespace AlicizaX.Timer.Editor
return; return;
} }
if (!(timerService is ITimerDebugService timerDebugService))
{
EditorGUILayout.HelpBox("Timer debug service is not available.", MessageType.Info);
return;
}
timerService.GetStatistics(out _cachedActiveCount, out _cachedPoolCapacity, out _cachedPeakActiveCount, out _cachedFreeCount); timerDebugService.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount);
_cachedUsageText = _cachedPoolCapacity > 0
? Utility.Text.Format("{0:F1}%", (float)_cachedActiveCount / _cachedPoolCapacity * 100f)
: "0.0%";
EditorGUILayout.Space(); EditorGUILayout.Space();
EditorGUILayout.LabelField("Runtime Debug", EditorStyles.boldLabel); EditorGUILayout.LabelField("Runtime Debug", EditorStyles.boldLabel);
EditorGUILayout.LabelField("Active Timers", _cachedActiveCount.ToString()); DrawStatistic("Active Timers", activeCount);
EditorGUILayout.LabelField("Pool Capacity", _cachedPoolCapacity.ToString()); DrawStatistic("Pool Capacity", poolCapacity);
EditorGUILayout.LabelField("Peak Active Count", _cachedPeakActiveCount.ToString()); DrawStatistic("Peak Active Count", peakActiveCount);
EditorGUILayout.LabelField("Free Slots", _cachedFreeCount.ToString()); DrawStatistic("Free Slots", freeCount);
EditorGUILayout.LabelField("Pool Usage", _cachedUsageText); DrawUsageBar("Active Usage", activeCount, poolCapacity);
DrawUsageBar("Peak Usage", peakActiveCount, poolCapacity);
DrawTimerList(timerService, _cachedActiveCount); DrawTimerList(timerDebugService, activeCount);
#if UNITY_EDITOR DrawStaleTimerList(timerDebugService, activeCount);
DrawLeakDetection(timerService, _cachedActiveCount);
#endif
} }
private void DrawTimerList(ITimerService debug, int activeCount) private void DrawTimerList(ITimerDebugService timerDebugService, int activeCount)
{ {
EditorGUILayout.Space(); EditorGUILayout.Space();
EditorGUILayout.LabelField("Active Timers", EditorStyles.boldLabel); EditorGUILayout.LabelField("Active Timer Sample", EditorStyles.boldLabel);
if (activeCount <= 0) if (activeCount <= 0)
{ {
@ -87,84 +96,93 @@ namespace AlicizaX.Timer.Editor
return; return;
} }
EnsureTimerBuffer(activeCount); int timerCount = timerDebugService.GetAllTimers(_timerBuffer);
int timerCount = debug.GetAllTimers(_timerBuffer); if (activeCount > DISPLAY_COUNT)
int displayCount = Mathf.Min(timerCount, MAX_DISPLAY_COUNT);
if (displayCount < activeCount)
{ {
EditorGUILayout.HelpBox(Utility.Text.Format("Showing first {0} timers of {1}.", displayCount, activeCount), MessageType.Info); EditorGUILayout.HelpBox(Utility.Text.Format("Showing first {0} timers of {1}.", timerCount, activeCount), MessageType.Info);
} }
for (int i = 0; i < displayCount; i++) for (int i = 0; i < timerCount; i++)
{ {
TimerDebugInfo timer = _timerBuffer[i]; DrawTimerInfo(ref _timerBuffer[i]);
bool isLoop = (timer.Flags & TimerDebugFlags.Loop) != 0;
bool isRunning = (timer.Flags & TimerDebugFlags.Running) != 0;
bool isUnscaled = (timer.Flags & TimerDebugFlags.Unscaled) != 0;
string label = Utility.Text.Format(
"ID {0} | {1} | {2} | {3}",
timer.TimerHandle,
isLoop ? "Loop" : "Once",
isUnscaled ? "Unscaled" : "Scaled",
isRunning ? "Running" : "Paused");
string value = Utility.Text.Format(
"Left {0:F2}s | Duration {1:F2}s",
timer.LeftTime,
timer.Duration);
EditorGUILayout.LabelField(label, value);
} }
} }
#if UNITY_EDITOR private void DrawStaleTimerList(ITimerDebugService timerDebugService, int activeCount)
private void DrawLeakDetection(ITimerService debug, int activeCount)
{ {
if (activeCount <= 0) if (activeCount <= 0)
{ {
return; return;
} }
EnsureLeakBuffer(activeCount); if (!(timerDebugService is ITimerEditorDebugService editorDebugService))
int staleCount = debug.GetStaleOneShotTimers(_leakBuffer); {
return;
}
int staleCount = editorDebugService.GetStaleOneShotTimers(_staleBuffer);
if (staleCount <= 0) if (staleCount <= 0)
{ {
return; return;
} }
EditorGUILayout.Space(); EditorGUILayout.Space();
EditorGUILayout.LabelField(Utility.Text.Format("Stale One-Shot Timers ({0})", staleCount), EditorStyles.boldLabel); EditorGUILayout.HelpBox("Long-lived one-shot timers detected.", MessageType.Warning);
EditorGUILayout.HelpBox("Non-loop timers older than 5 minutes. This may indicate long-delay tasks or paused timers.", MessageType.Warning);
for (int i = 0; i < staleCount; i++) for (int i = 0; i < staleCount; i++)
{ {
TimerDebugInfo staleTimer = _leakBuffer[i]; TimerDebugInfo info = _staleBuffer[i];
EditorGUILayout.LabelField( EditorGUILayout.LabelField(Utility.Text.Format("ID {0}", info.TimerHandle), Utility.Text.Format("Age {0:F1}s | Left {1:F2}s", info.Age, info.LeftTime));
Utility.Text.Format("ID {0}", staleTimer.TimerHandle),
Utility.Text.Format("Created {0:F1}s ago", staleTimer.Age));
}
}
#endif
private void EnsureTimerBuffer(int count)
{
int capacity = count > 0 ? count : 1;
if (_timerBuffer == null || _timerBuffer.Length < capacity)
{
_timerBuffer = new TimerDebugInfo[capacity];
} }
} }
#if UNITY_EDITOR private static void DrawTimerInfo(ref TimerDebugInfo info)
private void EnsureLeakBuffer(int count)
{ {
int capacity = count > 0 ? count : 1; byte flags = info.Flags;
if (_leakBuffer == null || _leakBuffer.Length < capacity) string mode = (flags & TimerDebugFlags.Loop) != 0 ? "Loop" : "Once";
{ string scale = (flags & TimerDebugFlags.Unscaled) != 0 ? "Unscaled" : "Scaled";
_leakBuffer = new TimerDebugInfo[capacity]; string state = (flags & TimerDebugFlags.Running) != 0 ? "Running" : "Paused";
} EditorGUILayout.LabelField(
Utility.Text.Format("ID {0} | {1} | {2} | {3}", info.TimerHandle, mode, scale, state),
Utility.Text.Format("Left {0:F2}s | Duration {1:F2}s", info.LeftTime, info.Duration));
}
private static void DrawStatistic(string label, int value)
{
EditorGUILayout.LabelField(label, value.ToString(), EditorStyles.boldLabel);
}
private static void DrawUsageBar(string label, int value, int capacity)
{
float ratio = capacity > 0 ? (float)value / capacity : 0f;
EditorGUILayout.Slider(label, ratio, 0f, 1f);
}
private static int AlignCapacity(int value)
{
int aligned = ((value + CAPACITY_STEP - 1) / CAPACITY_STEP) * CAPACITY_STEP;
if (aligned < MIN_INITIAL_CAPACITY)
{
return MIN_INITIAL_CAPACITY;
}
return aligned > MAX_INITIAL_CAPACITY ? MAX_INITIAL_CAPACITY : aligned;
}
private void RequestRuntimeRepaint()
{
if (!EditorApplication.isPlaying)
{
return;
}
double currentTime = EditorApplication.timeSinceStartup;
if (currentTime - _lastUpdateTime < UPDATE_INTERVAL)
{
return;
}
_lastUpdateTime = currentTime;
Repaint();
} }
#endif
} }
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using AlicizaX.Editor; using AlicizaX.Editor;
using AlicizaX.UI.Runtime; using AlicizaX.UI.Runtime;
using UnityEditor; using UnityEditor;
@ -9,9 +9,26 @@ namespace AlicizaX.UI.Editor
[CustomEditor(typeof(UIComponent))] [CustomEditor(typeof(UIComponent))]
internal sealed class UIComponentInspector : GameFrameworkInspector internal sealed class UIComponentInspector : GameFrameworkInspector
{ {
private const int CacheDebugInfoCapacity = 64;
private static readonly Color SectionColor = new Color(0.18f, 0.18f, 0.18f, 1f);
private static readonly Color OkColor = new Color(0.3f, 0.75f, 0.35f, 1f);
private static readonly Color WarningColor = new Color(1f, 0.72f, 0.18f, 1f);
private static readonly Color ErrorColor = new Color(1f, 0.32f, 0.28f, 1f);
private static readonly Color MutedColor = new Color(0.62f, 0.62f, 0.62f, 1f);
private readonly UIServiceDebugInfo _serviceInfo = new UIServiceDebugInfo();
private readonly UILayerDebugInfo _layerInfo = new UILayerDebugInfo();
private readonly UIWindowDebugInfo _windowInfo = new UIWindowDebugInfo();
private readonly UIWindowDebugInfo[] _cacheInfos = new UIWindowDebugInfo[CacheDebugInfoCapacity];
private SerializedProperty uiRoot; private SerializedProperty uiRoot;
private SerializedProperty _isOrthographic; private SerializedProperty _isOrthographic;
private bool _showRuntimeDebug = true;
private bool _showReferences;
private bool _showLayers = true;
private bool _showCache = true;
private bool _showEmptyLayers;
private Vector2 _runtimeScroll;
public override void OnInspectorGUI() public override void OnInspectorGUI()
{ {
@ -27,7 +44,7 @@ namespace AlicizaX.UI.Editor
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
GameObject rootPrefab = (GameObject)EditorGUILayout.ObjectField("UI根预设", uiRoot.objectReferenceValue, typeof(GameObject), false); GameObject rootPrefab = (GameObject)EditorGUILayout.ObjectField("UI Root Prefab", uiRoot.objectReferenceValue, typeof(GameObject), false);
if (rootPrefab != uiRoot.objectReferenceValue) if (rootPrefab != uiRoot.objectReferenceValue)
{ {
@ -36,7 +53,7 @@ namespace AlicizaX.UI.Editor
if (uiRoot.objectReferenceValue == null) if (uiRoot.objectReferenceValue == null)
{ {
if (GUILayout.Button("设置默认")) if (GUILayout.Button("Set Default"))
{ {
GameObject defaultPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(UIGlobalPath.UIPrefabPath); GameObject defaultPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(UIGlobalPath.UIPrefabPath);
uiRoot.objectReferenceValue = defaultPrefab; uiRoot.objectReferenceValue = defaultPrefab;
@ -49,14 +66,283 @@ namespace AlicizaX.UI.Editor
} }
EditorGUI.EndDisabledGroup(); EditorGUI.EndDisabledGroup();
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
Repaint();
}
DrawRuntimeDebugInfo();
if (EditorApplication.isPlaying)
{
Repaint();
}
}
private void OnEnable() private void OnEnable()
{ {
uiRoot = serializedObject.FindProperty("uiRoot"); uiRoot = serializedObject.FindProperty("uiRoot");
_isOrthographic = serializedObject.FindProperty("_isOrthographic"); _isOrthographic = serializedObject.FindProperty("_isOrthographic");
for (int i = 0; i < _cacheInfos.Length; i++)
{
_cacheInfos[i] = new UIWindowDebugInfo();
}
}
private void DrawRuntimeDebugInfo()
{
EditorGUILayout.Space();
_showRuntimeDebug = EditorGUILayout.Foldout(_showRuntimeDebug, "Runtime Debug", true, EditorStyles.foldoutHeader);
if (!_showRuntimeDebug)
{
return;
}
if (!EditorApplication.isPlaying)
{
EditorGUILayout.HelpBox("Enter Play Mode to inspect runtime UI state.", MessageType.Info);
return;
}
if (!AppServices.TryGet<IUIService>(out IUIService uiService) || uiService is not IUIDebugService debugService)
{
EditorGUILayout.HelpBox("UI service is not initialized.", MessageType.Warning);
return;
}
debugService.FillServiceDebugInfo(_serviceInfo);
DrawServiceSummary();
DrawRuntimeOptions();
_runtimeScroll = EditorGUILayout.BeginScrollView(_runtimeScroll, GUILayout.MinHeight(260f), GUILayout.MaxHeight(720f));
DrawReferences();
DrawLayerDebugInfo(debugService);
DrawCacheDebugInfo(debugService);
EditorGUILayout.EndScrollView();
}
private void DrawRuntimeOptions()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
_showLayers = GUILayout.Toggle(_showLayers, "Layers", EditorStyles.toolbarButton);
_showCache = GUILayout.Toggle(_showCache, "Cache", EditorStyles.toolbarButton);
_showReferences = GUILayout.Toggle(_showReferences, "References", EditorStyles.toolbarButton);
_showEmptyLayers = GUILayout.Toggle(_showEmptyLayers, "Empty Layers", EditorStyles.toolbarButton);
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
private void DrawServiceSummary()
{
DrawSectionBegin("Service Summary");
EditorGUILayout.BeginHorizontal();
DrawCounter("Initialized", _serviceInfo.Initialized ? "Yes" : "No", _serviceInfo.Initialized ? OkColor : ErrorColor);
DrawCounter("Mode", _serviceInfo.Orthographic ? "Orthographic" : "Perspective", Color.white);
DrawCounter("Layers", _serviceInfo.LayerCount.ToString(), Color.white);
DrawCounter("Open", _serviceInfo.OpenWindowCount.ToString(), _serviceInfo.OpenWindowCount > 0 ? OkColor : MutedColor);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
DrawCounter("Cache", _serviceInfo.CacheWindowCount.ToString(), _serviceInfo.CacheWindowCount > 0 ? WarningColor : MutedColor);
DrawCounter("Update", _serviceInfo.UpdateWindowCount.ToString(), _serviceInfo.UpdateWindowCount > 0 ? OkColor : MutedColor);
DrawCounter("Block", _serviceInfo.BlockActive ? "Active" : "Inactive", _serviceInfo.BlockActive ? WarningColor : MutedColor);
DrawCounter("Block Timer", _serviceInfo.BlockTimerHandle.ToString(), _serviceInfo.BlockTimerHandle != 0UL ? WarningColor : MutedColor);
EditorGUILayout.EndHorizontal();
DrawSectionEnd();
}
private void DrawReferences()
{
if (!_showReferences)
{
return;
}
DrawSectionBegin("References");
EditorGUILayout.ObjectField("Root", _serviceInfo.Root, typeof(Transform), true);
EditorGUILayout.ObjectField("Canvas Root", _serviceInfo.CanvasRoot, typeof(Transform), true);
EditorGUILayout.ObjectField("Canvas", _serviceInfo.Canvas, typeof(Canvas), true);
EditorGUILayout.ObjectField("Camera", _serviceInfo.Camera, typeof(Camera), true);
DrawSectionEnd();
}
private void DrawLayerDebugInfo(IUIDebugService debugService)
{
if (!_showLayers)
{
return;
}
DrawSectionBegin("Open Windows");
bool hasWindow = false;
for (int layerIndex = 0; layerIndex < debugService.LayerCount; layerIndex++)
{
if (!debugService.FillLayerDebugInfo(layerIndex, _layerInfo))
{
continue;
}
if (_layerInfo.WindowCount == 0 && !_showEmptyLayers)
{
continue;
}
hasWindow |= _layerInfo.WindowCount > 0;
DrawLayerHeader(_layerInfo);
EditorGUI.indentLevel++;
for (int windowIndex = 0; windowIndex < _layerInfo.WindowCount; windowIndex++)
{
if (debugService.FillWindowDebugInfo(layerIndex, windowIndex, _windowInfo))
{
DrawWindowDebugInfo(_windowInfo, false);
}
}
EditorGUI.indentLevel--;
}
if (!hasWindow && !_showEmptyLayers)
{
DrawEmptyLabel("No open UI windows.");
}
DrawSectionEnd();
}
private void DrawCacheDebugInfo(IUIDebugService debugService)
{
if (!_showCache)
{
return;
}
DrawSectionBegin("Cached Windows");
int count = debugService.FillCacheDebugInfo(_cacheInfos, _cacheInfos.Length);
if (count == 0)
{
DrawEmptyLabel("No cached UI windows.");
}
for (int i = 0; i < count; i++)
{
DrawWindowDebugInfo(_cacheInfos[i], true);
}
if (debugService.CacheWindowCount > _cacheInfos.Length)
{
EditorGUILayout.HelpBox("Cache list is truncated by inspector buffer capacity.", MessageType.Warning);
}
DrawSectionEnd();
}
private static void DrawLayerHeader(UILayerDebugInfo info)
{
Rect rect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight + 4f);
EditorGUI.DrawRect(rect, SectionColor);
rect.x += 6f;
rect.width -= 12f;
rect.y += 2f;
EditorGUI.LabelField(
rect,
info.Layer.ToString(),
"Windows " + info.WindowCount + " | Last Fullscreen " + info.LastFullscreenIndex,
EditorStyles.boldLabel);
}
private static void DrawWindowDebugInfo(UIWindowDebugInfo info, bool cached)
{
Rect rect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight + 4f);
Color color = GetWindowColor(info, cached);
EditorGUI.DrawRect(new Rect(rect.x, rect.y + 2f, 4f, rect.height - 4f), color);
rect.x += 8f;
rect.width -= 8f;
EditorGUI.ObjectField(rect, GetWindowTitle(info, cached), info.HolderTransform, typeof(Transform), true);
EditorGUI.indentLevel++;
EditorGUILayout.LabelField("State", GetStateLine(info));
EditorGUILayout.LabelField("Flags", GetFlagLine(info));
if (cached || info.CacheTime > 0f || info.CacheTimerHandle != 0UL)
{
EditorGUILayout.LabelField("Cache", GetCacheLine(info));
}
EditorGUILayout.LabelField("Holder", string.IsNullOrEmpty(info.HolderTypeName) ? "None" : info.HolderTypeName);
EditorGUI.indentLevel--;
}
private static string GetWindowTitle(UIWindowDebugInfo info, bool cached)
{
string logicName = string.IsNullOrEmpty(info.LogicTypeName) ? "Unknown" : info.LogicTypeName;
string prefix = cached ? "[Cache] " : "";
return prefix + "#" + info.OrderIndex + " L" + info.LayerIndex + " " + logicName;
}
private static string GetStateLine(UIWindowDebugInfo info)
{
return info.State + " | Visible " + info.Visible + " | Depth " + info.Depth + " | FullScreen " + info.FullScreen;
}
private static string GetFlagLine(UIWindowDebugInfo info)
{
return "Update " + info.NeedUpdate + " | InCache " + info.InCache + " | ShowOp " + info.ShowInProgress + " | CloseOp " + info.CloseInProgress;
}
private static string GetCacheLine(UIWindowDebugInfo info)
{
return "Time " + info.CacheTime.ToString("F2") + " | Timer " + info.CacheTimerHandle;
}
private static Color GetWindowColor(UIWindowDebugInfo info, bool cached)
{
if (info.ShowInProgress || info.CloseInProgress)
{
return WarningColor;
}
if (cached || info.InCache)
{
return MutedColor;
}
if (!info.Visible)
{
return ErrorColor;
}
return info.FullScreen ? OkColor : Color.cyan;
}
private static void DrawCounter(string label, string value, Color valueColor)
{
EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.MinWidth(82f));
EditorGUILayout.LabelField(label, EditorStyles.miniLabel);
Color oldColor = GUI.color;
GUI.color = valueColor;
EditorGUILayout.LabelField(value, EditorStyles.boldLabel);
GUI.color = oldColor;
EditorGUILayout.EndVertical();
}
private static void DrawSectionBegin(string title)
{
EditorGUILayout.Space(4f);
Rect rect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight + 6f);
EditorGUI.DrawRect(rect, SectionColor);
rect.x += 6f;
rect.y += 3f;
rect.width -= 12f;
EditorGUI.LabelField(rect, title, EditorStyles.boldLabel);
EditorGUILayout.BeginVertical(GUI.skin.box);
}
private static void DrawSectionEnd()
{
EditorGUILayout.EndVertical();
}
private static void DrawEmptyLabel(string text)
{
Color oldColor = GUI.color;
GUI.color = MutedColor;
EditorGUILayout.LabelField(text, EditorStyles.miniLabel);
GUI.color = oldColor;
} }
} }
} }

View File

@ -26,4 +26,11 @@ namespace AlicizaX
{ {
public int Order { get; } public int Order { get; }
} }
internal enum ServiceScopeKind : byte
{
App = 0,
Scene = 1,
Gameplay = 2,
}
} }

View File

@ -21,8 +21,12 @@ namespace AlicizaX
return _world; return _world;
} }
public static T RegisterApp<T>(T service, params Type[] extraContracts) where T : class, IService public static T RegisterAppSelf<T>(T service) where T : class, IService
=> RequireWorld().App.Register(service, extraContracts); => RequireWorld().App.RegisterSelf(service);
public static TContract RegisterApp<TContract>(IService service)
where TContract : class, IService
=> RequireWorld().App.Register<TContract>(service);
public static bool TryGetApp<T>(out T service) where T : class, IService public static bool TryGetApp<T>(out T service) where T : class, IService
{ {

View File

@ -1,10 +1,9 @@
using System; namespace AlicizaX
namespace AlicizaX
{ {
public interface IServiceRegistry public interface IServiceRegistry
{ {
T Register<T>(T service, params Type[] extraContracts) where T : class, IService; T RegisterSelf<T>(T service) where T : class, IService;
TContract Register<TContract>(IService service) where TContract : class, IService;
bool TryGet<T>(out T service) where T : class, IService; bool TryGet<T>(out T service) where T : class, IService;
T Require<T>() where T : class, IService; T Require<T>() where T : class, IService;
} }

View File

@ -1,3 +1,5 @@
using Cysharp.Text;
namespace AlicizaX namespace AlicizaX
{ {
internal interface IServiceLifecycle internal interface IServiceLifecycle
@ -15,7 +17,7 @@ namespace AlicizaX
void IServiceLifecycle.Initialize(ServiceContext context) void IServiceLifecycle.Initialize(ServiceContext context)
{ {
if (IsInitialized) if (IsInitialized)
throw new System.InvalidOperationException($"{GetType().FullName} is already initialized."); throw new System.InvalidOperationException(ZString.Format("{0} is already initialized.", GetType().FullName));
Context = context; Context = context;
IsInitialized = true; IsInitialized = true;

View File

@ -1,78 +1,37 @@
using System; using System;
using System.Collections.Generic;
namespace AlicizaX namespace AlicizaX
{ {
internal static class ServiceContractUtility internal static class ServiceContractUtility
{ {
private static readonly HashSet<Type> ExcludedContracts = new HashSet<Type> public static ServiceContracts Create(Type serviceType)
=> new ServiceContracts(serviceType, null);
public static ServiceContracts Create(Type serviceType, Type contractType)
=> new ServiceContracts(serviceType, contractType);
}
internal readonly struct ServiceContracts
{
private readonly Type _serviceType;
private readonly Type _contractType;
public ServiceContracts(Type serviceType, Type contractType)
{ {
typeof(IService), _serviceType = serviceType;
typeof(IMonoService), _contractType = contractType;
typeof(IServiceTickable),
typeof(IServiceLateTickable),
typeof(IServiceFixedTickable),
typeof(IServiceGizmoDrawable),
typeof(IServiceOrder),
typeof(IServiceLifecycle),
};
// Cache for the common no-extraContracts path — contracts per concrete type never change.
private static readonly Dictionary<Type, List<Type>> _contractCache = new Dictionary<Type, List<Type>>();
public static List<Type> Collect(Type serviceType, IReadOnlyList<Type> extraContracts)
{
if (extraContracts == null || extraContracts.Count == 0)
{
if (_contractCache.TryGetValue(serviceType, out var cached))
return cached;
var result = BuildContracts(serviceType);
_contractCache[serviceType] = result;
return result;
}
// Extra contracts path: build fresh, validate, append extras.
var contracts = BuildContracts(serviceType);
contracts = new List<Type>(contracts); // don't mutate the cached list
var unique = new HashSet<Type>(contracts);
for (var i = 0; i < extraContracts.Count; i++)
{
var extraContract = extraContracts[i];
ValidateExtraContract(serviceType, extraContract);
if (unique.Add(extraContract)) contracts.Add(extraContract);
}
return contracts;
} }
private static List<Type> BuildContracts(Type serviceType) public int Count => _contractType == null || _contractType == _serviceType ? 1 : 2;
{
var contracts = new List<Type> { serviceType };
var unique = new HashSet<Type> { serviceType };
var interfaces = serviceType.GetInterfaces(); public Type this[int index]
for (var i = 0; i < interfaces.Length; i++) {
get
{ {
var contract = interfaces[i]; if (index == 0) return _serviceType;
if (!typeof(IService).IsAssignableFrom(contract)) continue; if (index == 1 && Count == 2) return _contractType;
if (ExcludedContracts.Contains(contract)) continue; throw new IndexOutOfRangeException();
if (unique.Add(contract)) contracts.Add(contract);
} }
return contracts;
}
private static void ValidateExtraContract(Type serviceType, Type contract)
{
if (contract == null)
throw new ArgumentNullException(nameof(contract));
if (!typeof(IService).IsAssignableFrom(contract))
throw new InvalidOperationException($"{contract.FullName} must inherit {nameof(IService)}.");
if (!contract.IsAssignableFrom(serviceType))
throw new InvalidOperationException($"{serviceType.FullName} does not implement {contract.FullName}.");
if (ExcludedContracts.Contains(contract))
throw new InvalidOperationException($"{contract.FullName} cannot be used as a service contract.");
} }
} }
} }

View File

@ -1,12 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Cysharp.Text;
namespace AlicizaX namespace AlicizaX
{ {
internal sealed class ServiceScope : IDisposable, IServiceRegistry internal sealed class ServiceScope : IDisposable, IServiceRegistry
{ {
private readonly Dictionary<Type, IService> _servicesByContract = new Dictionary<Type, IService>(); private const int MissingIndex = -1;
private readonly Dictionary<IService, List<Type>> _contractsByService = new Dictionary<IService, List<Type>>(ReferenceComparer<IService>.Instance);
private readonly Dictionary<RuntimeTypeHandle, IService> _servicesByContract = new Dictionary<RuntimeTypeHandle, IService>();
private readonly Dictionary<IService, ServiceEntry> _entriesByService = new Dictionary<IService, ServiceEntry>(ReferenceComparer<IService>.Instance);
private readonly List<IService> _registrationOrder = new List<IService>(); private readonly List<IService> _registrationOrder = new List<IService>();
private readonly List<IServiceTickable> _tickables = new List<IServiceTickable>(); private readonly List<IServiceTickable> _tickables = new List<IServiceTickable>();
@ -14,101 +17,74 @@ namespace AlicizaX
private readonly List<IServiceFixedTickable> _fixedTickables = new List<IServiceFixedTickable>(); private readonly List<IServiceFixedTickable> _fixedTickables = new List<IServiceFixedTickable>();
private readonly List<IServiceGizmoDrawable> _gizmoDrawables = new List<IServiceGizmoDrawable>(); private readonly List<IServiceGizmoDrawable> _gizmoDrawables = new List<IServiceGizmoDrawable>();
private IServiceTickable[] _tickableSnapshot = Array.Empty<IServiceTickable>(); private readonly List<PendingChange> _pendingChanges = new List<PendingChange>();
private IServiceLateTickable[] _lateTickableSnapshot = Array.Empty<IServiceLateTickable>();
private IServiceFixedTickable[] _fixedTickableSnapshot = Array.Empty<IServiceFixedTickable>();
private IServiceGizmoDrawable[] _gizmoSnapshot = Array.Empty<IServiceGizmoDrawable>();
private bool _tickablesDirty; private bool _tickablesDirty;
private bool _lateTickablesDirty; private bool _lateTickablesDirty;
private bool _fixedTickablesDirty; private bool _fixedTickablesDirty;
private bool _gizmoDrawablesDirty; private bool _gizmoDrawablesDirty;
private bool _isIterating;
internal ServiceScope(ServiceWorld world, string name, int order) internal ServiceScope(ServiceWorld world, ServiceScopeKind kind, string name, int order, int creationIndex)
{ {
World = world ?? throw new ArgumentNullException(nameof(world)); World = world ?? throw new ArgumentNullException(nameof(world));
Kind = kind;
Name = string.IsNullOrWhiteSpace(name) ? throw new ArgumentException("Scope name cannot be empty.", nameof(name)) : name; Name = string.IsNullOrWhiteSpace(name) ? throw new ArgumentException("Scope name cannot be empty.", nameof(name)) : name;
Order = order; Order = order;
CreationIndex = creationIndex;
} }
internal ServiceWorld World { get; } internal ServiceWorld World { get; }
internal ServiceScopeKind Kind { get; }
public string Name { get; } public string Name { get; }
internal int Order { get; } internal int Order { get; }
internal int CreationIndex { get; }
internal bool IsDisposed { get; private set; } internal bool IsDisposed { get; private set; }
public T Register<T>(T service, params Type[] extraContracts) public T RegisterSelf<T>(T service) where T : class, IService
where T : class, IService => RegisterInternal(service, ServiceContractUtility.Create(service != null ? service.GetType() : typeof(T)));
{
EnsureNotDisposed();
if (service == null) public TContract Register<TContract>(IService service)
throw new ArgumentNullException(nameof(service)); where TContract : class, IService
=> RegisterInternal(service, ServiceContractUtility.Create(service != null ? service.GetType() : typeof(TContract), typeof(TContract))) as TContract;
if (service is not IServiceLifecycle lifecycle)
throw new InvalidOperationException($"Service {service.GetType().FullName} must implement {nameof(IServiceLifecycle)}.");
ValidateService(service);
if (_contractsByService.ContainsKey(service))
throw new InvalidOperationException($"Service {service.GetType().FullName} is already registered in scope {Name}.");
var contracts = ServiceContractUtility.Collect(service.GetType(), extraContracts);
for (var i = 0; i < contracts.Count; i++)
{
var contract = contracts[i];
if (_servicesByContract.TryGetValue(contract, out var existing))
{
throw new InvalidOperationException(
$"Scope {Name} already contains contract {contract.FullName} bound to {existing.GetType().FullName}.");
}
}
_contractsByService.Add(service, contracts);
_registrationOrder.Add(service);
for (var i = 0; i < contracts.Count; i++)
_servicesByContract.Add(contracts[i], service);
try
{
lifecycle.Initialize(new ServiceContext(World, this));
AddToLifecycleLists(service);
}
catch
{
RemoveBindings(service);
throw;
}
return service;
}
public bool Unregister<T>() where T : class, IService public bool Unregister<T>() where T : class, IService
{ {
if (!_servicesByContract.TryGetValue(typeof(T), out var service)) if (!_servicesByContract.TryGetValue(typeof(T).TypeHandle, out var service))
return false; return false;
return Unregister(service); return Unregister(service);
} }
public bool Unregister(IService service) public bool Unregister(IService service)
{ {
if (service == null || !_contractsByService.ContainsKey(service)) if (service == null || !_entriesByService.TryGetValue(service, out var entry))
return false; return false;
if (service is not IServiceLifecycle lifecycle) if (service is not IServiceLifecycle lifecycle)
throw new InvalidOperationException($"Service {service.GetType().FullName} must implement {nameof(IServiceLifecycle)}."); throw new InvalidOperationException(ZString.Format("Service {0} must implement {1}.", service.GetType().FullName, nameof(IServiceLifecycle)));
RemoveFromLifecycleLists(service); if (_isIterating)
RemoveBindings(service); {
if (entry.PendingRemove) return true;
entry.PendingRemove = true;
_entriesByService[service] = entry;
_pendingChanges.Add(PendingChange.Remove(service));
return true;
}
RemoveEntry(service, entry, true);
lifecycle.Destroy(); lifecycle.Destroy();
return true; return true;
} }
public bool TryGet<T>(out T service) where T : class, IService public bool TryGet<T>(out T service) where T : class, IService
{ {
if (_servicesByContract.TryGetValue(typeof(T), out var raw)) if (_servicesByContract.TryGetValue(typeof(T).TypeHandle, out var raw))
{ {
service = raw as T; service = raw as T;
return service != null; return service != null;
@ -118,60 +94,180 @@ namespace AlicizaX
return false; return false;
} }
internal bool TryGet(Type contract, out IService service)
=> _servicesByContract.TryGetValue(contract.TypeHandle, out service);
public T Require<T>() where T : class, IService public T Require<T>() where T : class, IService
{ {
if (TryGet(out T service)) return service; if (TryGet(out T service)) return service;
Log.Error($"Scope {Name} does not contain service {typeof(T).FullName}."); throw new InvalidOperationException(ZString.Format("Scope {0} does not contain service {1}.", Name, typeof(T).FullName));
return default;
} }
public bool HasContract(Type contractType) public bool HasContract(Type contractType)
=> _servicesByContract.ContainsKey(contractType); => _servicesByContract.ContainsKey(contractType.TypeHandle);
internal void Tick(float deltaTime) internal void Tick(float deltaTime)
{ {
var snapshot = GetTickSnapshot(); SortTickablesIfDirty();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].Tick(deltaTime); _isIterating = true;
for (var i = 0; i < _tickables.Count; i++) _tickables[i].Tick(deltaTime);
_isIterating = false;
FlushPendingChanges();
} }
internal void LateTick(float deltaTime) internal void LateTick(float deltaTime)
{ {
var snapshot = GetLateTickSnapshot(); SortLateTickablesIfDirty();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].LateTick(deltaTime); _isIterating = true;
for (var i = 0; i < _lateTickables.Count; i++) _lateTickables[i].LateTick(deltaTime);
_isIterating = false;
FlushPendingChanges();
} }
internal void FixedTick(float fixedDeltaTime) internal void FixedTick(float fixedDeltaTime)
{ {
var snapshot = GetFixedTickSnapshot(); SortFixedTickablesIfDirty();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].FixedTick(fixedDeltaTime); _isIterating = true;
for (var i = 0; i < _fixedTickables.Count; i++) _fixedTickables[i].FixedTick(fixedDeltaTime);
_isIterating = false;
FlushPendingChanges();
} }
internal void DrawGizmos() internal void DrawGizmos()
{ {
var snapshot = GetGizmoSnapshot(); SortGizmoDrawablesIfDirty();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].DrawGizmos(); _isIterating = true;
for (var i = 0; i < _gizmoDrawables.Count; i++) _gizmoDrawables[i].DrawGizmos();
_isIterating = false;
FlushPendingChanges();
} }
public void Dispose() public void Dispose()
{ {
if (IsDisposed) return; if (IsDisposed) return;
var snapshot = _registrationOrder.ToArray(); _isIterating = false;
for (var i = snapshot.Length - 1; i >= 0; i--) _pendingChanges.Clear();
for (var i = _registrationOrder.Count - 1; i >= 0; i--)
{ {
var service = snapshot[i]; var service = _registrationOrder[i];
if (!_contractsByService.ContainsKey(service)) continue; if (service == null || !_entriesByService.TryGetValue(service, out var entry)) continue;
if (service is not IServiceLifecycle lifecycle) if (service is not IServiceLifecycle lifecycle)
throw new InvalidOperationException($"Service {service.GetType().FullName} must implement {nameof(IServiceLifecycle)}."); throw new InvalidOperationException(ZString.Format("Service {0} must implement {1}.", service.GetType().FullName, nameof(IServiceLifecycle)));
RemoveFromLifecycleLists(service);
RemoveContractBindings(service); // skip _registrationOrder.Remove — we clear below RemoveEntry(service, entry, false);
lifecycle.Destroy(); lifecycle.Destroy();
} }
_registrationOrder.Clear(); _registrationOrder.Clear();
_tickables.Clear();
_lateTickables.Clear();
_fixedTickables.Clear();
_gizmoDrawables.Clear();
IsDisposed = true; IsDisposed = true;
} }
private T RegisterInternal<T>(T service, ServiceContracts contracts) where T : class, IService
{
EnsureNotDisposed();
if (service == null)
throw new ArgumentNullException(nameof(service));
if (service is not IServiceLifecycle lifecycle)
throw new InvalidOperationException(ZString.Format("Service {0} must implement {1}.", service.GetType().FullName, nameof(IServiceLifecycle)));
ValidateService(service);
if (_entriesByService.ContainsKey(service))
throw new InvalidOperationException(ZString.Format("Service {0} is already registered in scope {1}.", service.GetType().FullName, Name));
if (_isIterating)
{
ValidateContracts(contracts);
_pendingChanges.Add(PendingChange.Add(service, contracts));
return service;
}
ValidateContracts(contracts);
lifecycle.Initialize(new ServiceContext(World, this));
AddEntry(service, contracts);
return service;
}
private void ValidateContracts(ServiceContracts contracts)
{
for (var i = 0; i < contracts.Count; i++)
{
var contract = contracts[i];
if (_servicesByContract.TryGetValue(contract.TypeHandle, out var existing))
{
throw new InvalidOperationException(
ZString.Format("Scope {0} already contains contract {1} bound to {2}.", Name, contract.FullName, existing.GetType().FullName));
}
}
for (var i = 0; i < _pendingChanges.Count; i++)
{
var change = _pendingChanges[i];
if (!change.IsAdd) continue;
for (var contractIndex = 0; contractIndex < contracts.Count; contractIndex++)
{
var contract = contracts[contractIndex];
for (var pendingContractIndex = 0; pendingContractIndex < change.Contracts.Count; pendingContractIndex++)
{
if (!change.Contracts[pendingContractIndex].TypeHandle.Equals(contract.TypeHandle)) continue;
throw new InvalidOperationException(
ZString.Format("Scope {0} already contains pending contract {1}.", Name, contract.FullName));
}
}
}
}
private void AddEntry(IService service, ServiceContracts contracts)
{
var entry = new ServiceEntry(contracts, _registrationOrder.Count);
_registrationOrder.Add(service);
for (var i = 0; i < contracts.Count; i++)
{
var contract = contracts[i];
_servicesByContract.Add(contract.TypeHandle, service);
World.AddContract(this, contract, service);
}
AddToLifecycleLists(service, ref entry);
_entriesByService.Add(service, entry);
}
private void RemoveEntry(IService service, ServiceEntry entry, bool removeRegistrationOrder)
{
_entriesByService.Remove(service);
RemoveFromLifecycleLists(service, entry);
for (var i = 0; i < entry.Contracts.Count; i++)
{
var contract = entry.Contracts[i];
_servicesByContract.Remove(contract.TypeHandle);
World.RemoveContract(this, contract, service);
}
if (!removeRegistrationOrder) return;
var lastIndex = _registrationOrder.Count - 1;
var removedIndex = entry.RegistrationIndex;
var lastService = _registrationOrder[lastIndex];
_registrationOrder[removedIndex] = lastService;
_registrationOrder.RemoveAt(lastIndex);
if (removedIndex != lastIndex && lastService != null && _entriesByService.TryGetValue(lastService, out var lastEntry))
{
lastEntry.RegistrationIndex = removedIndex;
_entriesByService[lastService] = lastEntry;
}
}
private void EnsureNotDisposed() private void EnsureNotDisposed()
{ {
if (IsDisposed) throw new ObjectDisposedException(Name); if (IsDisposed) throw new ObjectDisposedException(Name);
@ -186,115 +282,202 @@ namespace AlicizaX
service is IServiceGizmoDrawable)) service is IServiceGizmoDrawable))
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"Mono service {service.GetType().FullName} cannot implement tick lifecycle interfaces."); ZString.Format("Mono service {0} cannot implement tick lifecycle interfaces.", service.GetType().FullName));
} }
} }
private void AddToLifecycleLists(IService service) private void AddToLifecycleLists(IService service, ref ServiceEntry entry)
{ {
if (service is IServiceTickable tickable) if (service is IServiceTickable tickable)
{ {
entry.TickIndex = _tickables.Count;
_tickables.Add(tickable); _tickables.Add(tickable);
_tickablesDirty = true; _tickablesDirty = true;
} }
if (service is IServiceLateTickable late) if (service is IServiceLateTickable late)
{ {
entry.LateTickIndex = _lateTickables.Count;
_lateTickables.Add(late); _lateTickables.Add(late);
_lateTickablesDirty = true; _lateTickablesDirty = true;
} }
if (service is IServiceFixedTickable fixed_) if (service is IServiceFixedTickable fixed_)
{ {
entry.FixedTickIndex = _fixedTickables.Count;
_fixedTickables.Add(fixed_); _fixedTickables.Add(fixed_);
_fixedTickablesDirty = true; _fixedTickablesDirty = true;
} }
if (service is IServiceGizmoDrawable gizmo) if (service is IServiceGizmoDrawable gizmo)
{ {
entry.GizmoIndex = _gizmoDrawables.Count;
_gizmoDrawables.Add(gizmo); _gizmoDrawables.Add(gizmo);
_gizmoDrawablesDirty = true; _gizmoDrawablesDirty = true;
} }
} }
private void RemoveFromLifecycleLists(IService service) private void RemoveFromLifecycleLists(IService service, ServiceEntry entry)
{ {
if (service is IServiceTickable tickable && _tickables.Remove(tickable)) _tickablesDirty = true; if (entry.TickIndex != MissingIndex) RemoveTickableAt(entry.TickIndex);
if (service is IServiceLateTickable late && _lateTickables.Remove(late)) _lateTickablesDirty = true; if (entry.LateTickIndex != MissingIndex) RemoveLateTickableAt(entry.LateTickIndex);
if (service is IServiceFixedTickable fixed_ && _fixedTickables.Remove(fixed_)) _fixedTickablesDirty = true; if (entry.FixedTickIndex != MissingIndex) RemoveFixedTickableAt(entry.FixedTickIndex);
if (service is IServiceGizmoDrawable gizmo && _gizmoDrawables.Remove(gizmo)) _gizmoDrawablesDirty = true; if (entry.GizmoIndex != MissingIndex) RemoveGizmoDrawableAt(entry.GizmoIndex);
} }
private void RemoveBindings(IService service) private void RemoveTickableAt(int index)
{ {
if (_contractsByService.TryGetValue(service, out var contracts)) var lastIndex = _tickables.Count - 1;
var moved = _tickables[lastIndex];
_tickables[index] = moved;
_tickables.RemoveAt(lastIndex);
if (index != lastIndex) UpdateTickIndex(moved, index);
_tickablesDirty = true;
}
private void RemoveLateTickableAt(int index)
{
var lastIndex = _lateTickables.Count - 1;
var moved = _lateTickables[lastIndex];
_lateTickables[index] = moved;
_lateTickables.RemoveAt(lastIndex);
if (index != lastIndex) UpdateLateTickIndex(moved, index);
_lateTickablesDirty = true;
}
private void RemoveFixedTickableAt(int index)
{
var lastIndex = _fixedTickables.Count - 1;
var moved = _fixedTickables[lastIndex];
_fixedTickables[index] = moved;
_fixedTickables.RemoveAt(lastIndex);
if (index != lastIndex) UpdateFixedTickIndex(moved, index);
_fixedTickablesDirty = true;
}
private void RemoveGizmoDrawableAt(int index)
{
var lastIndex = _gizmoDrawables.Count - 1;
var moved = _gizmoDrawables[lastIndex];
_gizmoDrawables[index] = moved;
_gizmoDrawables.RemoveAt(lastIndex);
if (index != lastIndex) UpdateGizmoIndex(moved, index);
_gizmoDrawablesDirty = true;
}
private void UpdateTickIndex(IServiceTickable service, int index)
{
if (!_entriesByService.TryGetValue((IService)service, out var entry)) return;
entry.TickIndex = index;
_entriesByService[(IService)service] = entry;
}
private void UpdateLateTickIndex(IServiceLateTickable service, int index)
{
if (!_entriesByService.TryGetValue((IService)service, out var entry)) return;
entry.LateTickIndex = index;
_entriesByService[(IService)service] = entry;
}
private void UpdateFixedTickIndex(IServiceFixedTickable service, int index)
{
if (!_entriesByService.TryGetValue((IService)service, out var entry)) return;
entry.FixedTickIndex = index;
_entriesByService[(IService)service] = entry;
}
private void UpdateGizmoIndex(IServiceGizmoDrawable service, int index)
{
if (!_entriesByService.TryGetValue((IService)service, out var entry)) return;
entry.GizmoIndex = index;
_entriesByService[(IService)service] = entry;
}
private void FlushPendingChanges()
{
if (_pendingChanges.Count == 0) return;
for (var i = 0; i < _pendingChanges.Count; i++)
{ {
for (var i = 0; i < contracts.Count; i++) var change = _pendingChanges[i];
_servicesByContract.Remove(contracts[i]); if (change.IsAdd)
{
if (!_entriesByService.ContainsKey(change.Service))
{
((IServiceLifecycle)change.Service).Initialize(new ServiceContext(World, this));
AddEntry(change.Service, change.Contracts);
}
continue;
}
if (_entriesByService.TryGetValue(change.Service, out var entry))
{
RemoveEntry(change.Service, entry, true);
((IServiceLifecycle)change.Service).Destroy();
}
} }
_contractsByService.Remove(service); _pendingChanges.Clear();
_registrationOrder.Remove(service);
} }
// Used during full Dispose — skips the O(n) _registrationOrder.Remove since we clear the list afterwards. private void SortTickablesIfDirty()
private void RemoveContractBindings(IService service)
{
if (_contractsByService.TryGetValue(service, out var contracts))
{
for (var i = 0; i < contracts.Count; i++)
_servicesByContract.Remove(contracts[i]);
}
_contractsByService.Remove(service);
}
private IServiceTickable[] GetTickSnapshot()
{ {
if (_tickablesDirty) if (_tickablesDirty)
{ {
_tickables.Sort(CompareByOrder); _tickables.Sort(CompareByOrder);
_tickableSnapshot = _tickables.Count > 0 ? _tickables.ToArray() : Array.Empty<IServiceTickable>(); RebuildTickIndices();
_tickablesDirty = false; _tickablesDirty = false;
} }
return _tickableSnapshot;
} }
private IServiceLateTickable[] GetLateTickSnapshot() private void SortLateTickablesIfDirty()
{ {
if (_lateTickablesDirty) if (_lateTickablesDirty)
{ {
_lateTickables.Sort(CompareByOrder); _lateTickables.Sort(CompareByOrder);
_lateTickableSnapshot = _lateTickables.Count > 0 ? _lateTickables.ToArray() : Array.Empty<IServiceLateTickable>(); RebuildLateTickIndices();
_lateTickablesDirty = false; _lateTickablesDirty = false;
} }
return _lateTickableSnapshot;
} }
private IServiceFixedTickable[] GetFixedTickSnapshot() private void SortFixedTickablesIfDirty()
{ {
if (_fixedTickablesDirty) if (_fixedTickablesDirty)
{ {
_fixedTickables.Sort(CompareByOrder); _fixedTickables.Sort(CompareByOrder);
_fixedTickableSnapshot = _fixedTickables.Count > 0 ? _fixedTickables.ToArray() : Array.Empty<IServiceFixedTickable>(); RebuildFixedTickIndices();
_fixedTickablesDirty = false; _fixedTickablesDirty = false;
} }
return _fixedTickableSnapshot;
} }
private IServiceGizmoDrawable[] GetGizmoSnapshot() private void SortGizmoDrawablesIfDirty()
{ {
if (_gizmoDrawablesDirty) if (_gizmoDrawablesDirty)
{ {
_gizmoDrawables.Sort(CompareByOrder); _gizmoDrawables.Sort(CompareByOrder);
_gizmoSnapshot = _gizmoDrawables.Count > 0 ? _gizmoDrawables.ToArray() : Array.Empty<IServiceGizmoDrawable>(); RebuildGizmoIndices();
_gizmoDrawablesDirty = false; _gizmoDrawablesDirty = false;
} }
}
return _gizmoSnapshot; private void RebuildTickIndices()
{
for (var i = 0; i < _tickables.Count; i++) UpdateTickIndex(_tickables[i], i);
}
private void RebuildLateTickIndices()
{
for (var i = 0; i < _lateTickables.Count; i++) UpdateLateTickIndex(_lateTickables[i], i);
}
private void RebuildFixedTickIndices()
{
for (var i = 0; i < _fixedTickables.Count; i++) UpdateFixedTickIndex(_fixedTickables[i], i);
}
private void RebuildGizmoIndices()
{
for (var i = 0; i < _gizmoDrawables.Count; i++) UpdateGizmoIndex(_gizmoDrawables[i], i);
} }
private static int CompareByOrder<T>(T a, T b) private static int CompareByOrder<T>(T a, T b)
@ -303,5 +486,47 @@ namespace AlicizaX
var right = b is IServiceOrder ob ? ob.Order : 0; var right = b is IServiceOrder ob ? ob.Order : 0;
return left.CompareTo(right); return left.CompareTo(right);
} }
private struct ServiceEntry
{
public readonly ServiceContracts Contracts;
public int RegistrationIndex;
public int TickIndex;
public int LateTickIndex;
public int FixedTickIndex;
public int GizmoIndex;
public bool PendingRemove;
public ServiceEntry(ServiceContracts contracts, int registrationIndex)
{
Contracts = contracts;
RegistrationIndex = registrationIndex;
TickIndex = MissingIndex;
LateTickIndex = MissingIndex;
FixedTickIndex = MissingIndex;
GizmoIndex = MissingIndex;
PendingRemove = false;
}
}
private readonly struct PendingChange
{
public readonly bool IsAdd;
public readonly IService Service;
public readonly ServiceContracts Contracts;
private PendingChange(bool isAdd, IService service, ServiceContracts contracts)
{
IsAdd = isAdd;
Service = service;
Contracts = contracts;
}
public static PendingChange Add(IService service, ServiceContracts contracts)
=> new PendingChange(true, service, contracts);
public static PendingChange Remove(IService service)
=> new PendingChange(false, service, default);
}
} }
} }

View File

@ -1,28 +1,31 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Cysharp.Text;
namespace AlicizaX namespace AlicizaX
{ {
internal sealed class ServiceWorld : IDisposable internal sealed class ServiceWorld : IDisposable
{ {
private readonly List<ServiceScope> _scopes = new List<ServiceScope>(); private const int ScopeSlotCount = 3;
private readonly Dictionary<Type, ServiceScope> _scopesByType = new Dictionary<Type, ServiceScope>();
private ServiceScope[] _scopeSnapshot = Array.Empty<ServiceScope>(); private readonly ServiceScope[] _scopesByKind = new ServiceScope[ScopeSlotCount];
private readonly ServiceScope[] _activeScopes = new ServiceScope[ScopeSlotCount];
private readonly Dictionary<RuntimeTypeHandle, ContractBindings> _servicesByContract = new Dictionary<RuntimeTypeHandle, ContractBindings>();
private int _activeScopeCount;
private int _nextScopeCreationIndex;
private bool _scopesDirty; private bool _scopesDirty;
private ServiceScope _sceneScope;
private ServiceScope _gameplayScope;
internal ServiceWorld(int appScopeOrder = ServiceDomainOrder.App) internal ServiceWorld(int appScopeOrder = ServiceDomainOrder.App)
{ {
App = CreateScopeInternal(typeof(AppScope), nameof(App), appScopeOrder); App = CreateScopeInternal(ServiceScopeKind.App, nameof(App), appScopeOrder);
} }
internal ServiceScope App { get; } internal ServiceScope App { get; }
internal bool HasScene => _sceneScope != null && !_sceneScope.IsDisposed; internal bool HasScene => IsAlive(ServiceScopeKind.Scene);
internal bool HasGameplay => _gameplayScope != null && !_gameplayScope.IsDisposed; internal bool HasGameplay => IsAlive(ServiceScopeKind.Gameplay);
internal ServiceScope Scene internal ServiceScope Scene
{ {
@ -30,7 +33,7 @@ namespace AlicizaX
{ {
if (!HasScene) if (!HasScene)
throw new InvalidOperationException("Scene scope has not been created yet."); throw new InvalidOperationException("Scene scope has not been created yet.");
return _sceneScope; return _scopesByKind[(int)ServiceScopeKind.Scene];
} }
} }
@ -40,18 +43,18 @@ namespace AlicizaX
{ {
if (!HasGameplay) if (!HasGameplay)
throw new InvalidOperationException("Gameplay scope has not been created yet."); throw new InvalidOperationException("Gameplay scope has not been created yet.");
return _gameplayScope; return _scopesByKind[(int)ServiceScopeKind.Gameplay];
} }
} }
internal ServiceScope EnsureScene(int order = ServiceDomainOrder.Scene) internal ServiceScope EnsureScene(int order = ServiceDomainOrder.Scene)
=> _sceneScope is { IsDisposed: false } => IsAlive(ServiceScopeKind.Scene)
? _sceneScope ? _scopesByKind[(int)ServiceScopeKind.Scene]
: _sceneScope = CreateScopeInternal(typeof(SceneScope), nameof(SceneScope), order); : CreateScopeInternal(ServiceScopeKind.Scene, nameof(SceneScope), order);
internal bool TryGetScene(out ServiceScope scope) internal bool TryGetScene(out ServiceScope scope)
{ {
scope = HasScene ? _sceneScope : null; scope = IsAlive(ServiceScopeKind.Scene) ? _scopesByKind[(int)ServiceScopeKind.Scene] : null;
return scope != null; return scope != null;
} }
@ -62,35 +65,21 @@ namespace AlicizaX
} }
internal bool DestroyScene() internal bool DestroyScene()
{ => DestroyScope(ServiceScopeKind.Scene);
if (!HasScene)
return false;
var scope = _sceneScope;
_sceneScope = null;
return DestroyScope(scope, typeof(SceneScope));
}
internal ServiceScope EnsureGameplay(int order = ServiceDomainOrder.Gameplay) internal ServiceScope EnsureGameplay(int order = ServiceDomainOrder.Gameplay)
=> _gameplayScope is { IsDisposed: false } => IsAlive(ServiceScopeKind.Gameplay)
? _gameplayScope ? _scopesByKind[(int)ServiceScopeKind.Gameplay]
: _gameplayScope = CreateScopeInternal(typeof(GameplayScope), nameof(GameplayScope), order); : CreateScopeInternal(ServiceScopeKind.Gameplay, nameof(GameplayScope), order);
internal bool TryGetGameplay(out ServiceScope scope) internal bool TryGetGameplay(out ServiceScope scope)
{ {
scope = HasGameplay ? _gameplayScope : null; scope = IsAlive(ServiceScopeKind.Gameplay) ? _scopesByKind[(int)ServiceScopeKind.Gameplay] : null;
return scope != null; return scope != null;
} }
internal bool DestroyGameplay() internal bool DestroyGameplay()
{ => DestroyScope(ServiceScopeKind.Gameplay);
if (!HasGameplay)
return false;
var scope = _gameplayScope;
_gameplayScope = null;
return DestroyScope(scope, typeof(GameplayScope));
}
internal bool TryGet<T>(out T service) where T : class, IService internal bool TryGet<T>(out T service) where T : class, IService
=> TryGet(null, out service); => TryGet(null, out service);
@ -100,12 +89,10 @@ namespace AlicizaX
if (preferredScope != null && !preferredScope.IsDisposed && preferredScope.TryGet(out service)) if (preferredScope != null && !preferredScope.IsDisposed && preferredScope.TryGet(out service))
return true; return true;
var snapshot = GetScopeSnapshot(); if (_servicesByContract.TryGetValue(typeof(T).TypeHandle, out var bindings) && bindings.TryGetBest(out var raw))
for (var i = snapshot.Length - 1; i >= 0; i--)
{ {
var scope = snapshot[i]; service = raw as T;
if (ReferenceEquals(scope, preferredScope)) continue; return service != null;
if (scope.TryGet(out service)) return true;
} }
service = null; service = null;
@ -117,82 +104,209 @@ namespace AlicizaX
internal T Require<T>(ServiceScope preferredScope) where T : class, IService internal T Require<T>(ServiceScope preferredScope) where T : class, IService
{ {
if (TryGet(preferredScope, out T service)) return service; if (TryGet(preferredScope, out T service)) return service;
throw new InvalidOperationException($"Service {typeof(T).FullName} was not found in any active scope."); throw new InvalidOperationException(ZString.Format("Service {0} was not found in any active scope.", typeof(T).FullName));
} }
internal void Tick(float deltaTime) internal void Tick(float deltaTime)
{ {
var snapshot = GetScopeSnapshot(); SortScopesIfDirty();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].Tick(deltaTime); for (var i = 0; i < _activeScopeCount; i++) _activeScopes[i].Tick(deltaTime);
} }
internal void LateTick(float deltaTime) internal void LateTick(float deltaTime)
{ {
var snapshot = GetScopeSnapshot(); SortScopesIfDirty();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].LateTick(deltaTime); for (var i = 0; i < _activeScopeCount; i++) _activeScopes[i].LateTick(deltaTime);
} }
internal void FixedTick(float fixedDeltaTime) internal void FixedTick(float fixedDeltaTime)
{ {
var snapshot = GetScopeSnapshot(); SortScopesIfDirty();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].FixedTick(fixedDeltaTime); for (var i = 0; i < _activeScopeCount; i++) _activeScopes[i].FixedTick(fixedDeltaTime);
} }
internal void DrawGizmos() internal void DrawGizmos()
{ {
var snapshot = GetScopeSnapshot(); SortScopesIfDirty();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].DrawGizmos(); for (var i = 0; i < _activeScopeCount; i++) _activeScopes[i].DrawGizmos();
} }
public void Dispose() public void Dispose()
{ {
var snapshot = _scopes.ToArray(); for (var i = _activeScopeCount - 1; i >= 0; i--)
for (var i = snapshot.Length - 1; i >= 0; i--) _activeScopes[i].Dispose();
snapshot[i].Dispose();
_sceneScope = null; for (var i = 0; i < ScopeSlotCount; i++)
_gameplayScope = null; _scopesByKind[i] = null;
_scopes.Clear();
_scopesByType.Clear(); _activeScopeCount = 0;
_scopeSnapshot = Array.Empty<ServiceScope>(); _servicesByContract.Clear();
} }
private bool DestroyScope(ServiceScope scope, Type scopeType) internal void AddContract(ServiceScope scope, Type contract, IService service)
{ {
if (scope == null) var contractHandle = contract.TypeHandle;
return false; if (!_servicesByContract.TryGetValue(contractHandle, out var bindings))
{
bindings = default;
_servicesByContract.Add(contractHandle, bindings);
}
_scopesByType.Remove(scopeType); bindings.Set(scope.Kind, scope, service);
_scopes.Remove(scope); _servicesByContract[contractHandle] = bindings;
_scopesDirty = true; }
internal void RemoveContract(ServiceScope scope, Type contract, IService service)
{
var contractHandle = contract.TypeHandle;
if (!_servicesByContract.TryGetValue(contractHandle, out var bindings)) return;
bindings.Clear(scope.Kind, service);
if (bindings.IsEmpty)
_servicesByContract.Remove(contractHandle);
else
_servicesByContract[contractHandle] = bindings;
}
private bool IsAlive(ServiceScopeKind kind)
{
var scope = _scopesByKind[(int)kind];
return scope != null && !scope.IsDisposed;
}
private bool DestroyScope(ServiceScopeKind kind)
{
if (!IsAlive(kind)) return false;
var scope = _scopesByKind[(int)kind];
_scopesByKind[(int)kind] = null;
RemoveActiveScope(scope);
scope.Dispose(); scope.Dispose();
return true; return true;
} }
private ServiceScope CreateScopeInternal(Type scopeType, string scopeName, int order) private ServiceScope CreateScopeInternal(ServiceScopeKind kind, string scopeName, int order)
{ {
if (_scopesByType.ContainsKey(scopeType)) if (IsAlive(kind))
throw new InvalidOperationException($"Scope {scopeType.Name} already exists."); throw new InvalidOperationException(ZString.Format("Scope {0} already exists.", scopeName));
var scope = new ServiceScope(this, scopeName, order); var scope = new ServiceScope(this, kind, scopeName, order, _nextScopeCreationIndex++);
_scopes.Add(scope); _scopesByKind[(int)kind] = scope;
_scopesByType.Add(scopeType, scope); _activeScopes[_activeScopeCount++] = scope;
_scopes.Sort(CompareScopeOrder);
_scopesDirty = true; _scopesDirty = true;
return scope; return scope;
} }
private ServiceScope[] GetScopeSnapshot() private void RemoveActiveScope(ServiceScope scope)
{ {
if (_scopesDirty) for (var i = 0; i < _activeScopeCount; i++)
{ {
_scopeSnapshot = _scopes.Count > 0 ? _scopes.ToArray() : Array.Empty<ServiceScope>(); if (!ReferenceEquals(_activeScopes[i], scope)) continue;
_scopesDirty = false;
_activeScopeCount--;
_activeScopes[i] = _activeScopes[_activeScopeCount];
_activeScopes[_activeScopeCount] = null;
_scopesDirty = true;
return;
} }
return _scopeSnapshot;
} }
private static int CompareScopeOrder(ServiceScope left, ServiceScope right) private void SortScopesIfDirty()
=> left.Order.CompareTo(right.Order); {
if (!_scopesDirty) return;
for (var i = 1; i < _activeScopeCount; i++)
{
var scope = _activeScopes[i];
var index = i - 1;
while (index >= 0 && HasHigherPriority(_activeScopes[index], scope))
{
_activeScopes[index + 1] = _activeScopes[index];
index--;
}
_activeScopes[index + 1] = scope;
}
_scopesDirty = false;
}
private static bool HasHigherPriority(ServiceScope left, ServiceScope right)
=> left.Order > right.Order || left.Order == right.Order && left.CreationIndex > right.CreationIndex;
private struct ContractBindings
{
private ServiceBinding _app;
private ServiceBinding _scene;
private ServiceBinding _gameplay;
public bool IsEmpty => !_app.HasValue && !_scene.HasValue && !_gameplay.HasValue;
public void Set(ServiceScopeKind kind, ServiceScope scope, IService service)
{
var binding = new ServiceBinding(scope, service);
if (kind == ServiceScopeKind.App)
_app = binding;
else if (kind == ServiceScopeKind.Scene)
_scene = binding;
else
_gameplay = binding;
}
public void Clear(ServiceScopeKind kind, IService service)
{
if (kind == ServiceScopeKind.App)
{
if (_app.HasValue && ReferenceEquals(_app.Service, service)) _app = default;
return;
}
if (kind == ServiceScopeKind.Scene)
{
if (_scene.HasValue && ReferenceEquals(_scene.Service, service)) _scene = default;
return;
}
if (_gameplay.HasValue && ReferenceEquals(_gameplay.Service, service)) _gameplay = default;
}
public bool TryGetBest(out IService service)
{
if (_gameplay.HasValue)
{
service = _gameplay.Service;
return true;
}
if (_scene.HasValue)
{
service = _scene.Service;
return true;
}
if (_app.HasValue)
{
service = _app.Service;
return true;
}
service = null;
return false;
}
}
private struct ServiceBinding
{
public ServiceScope Scope;
public IService Service;
public bool HasValue;
public ServiceBinding(ServiceScope scope, IService service)
{
Scope = scope;
Service = service;
HasValue = true;
}
}
} }
} }

View File

@ -1,11 +1,10 @@
using Cysharp.Threading.Tasks;
using UnityEngine; using UnityEngine;
namespace AlicizaX namespace AlicizaX
{ {
[DefaultExecutionOrder(-32000)] [DefaultExecutionOrder(-32000)]
[DisallowMultipleComponent] [DisallowMultipleComponent]
public class AppServiceRoot : MonoBehaviour public abstract class AppServiceRoot : MonoBehaviour
{ {
private static AppServiceRoot s_activeRoot; private static AppServiceRoot s_activeRoot;
@ -55,14 +54,13 @@ namespace AlicizaX
if (AppServices.HasWorld) AppServices.RequireWorld().DrawGizmos(); if (AppServices.HasWorld) AppServices.RequireWorld().DrawGizmos();
} }
protected virtual async void OnDestroy() protected virtual void OnDestroy()
{ {
if (s_activeRoot == this) if (s_activeRoot == this)
s_activeRoot = null; s_activeRoot = null;
if (_ownsWorld && AppServices.HasWorld) if (_ownsWorld && AppServices.HasWorld)
{ {
await UniTask.Yield();
AppServices.Shutdown(); AppServices.Shutdown();
} }

View File

@ -1,3 +1,4 @@
using Cysharp.Text;
using UnityEngine; using UnityEngine;
namespace AlicizaX namespace AlicizaX
@ -11,7 +12,7 @@ namespace AlicizaX
void IServiceLifecycle.Initialize(ServiceContext context) void IServiceLifecycle.Initialize(ServiceContext context)
{ {
if (IsInitialized) if (IsInitialized)
throw new System.InvalidOperationException($"{GetType().FullName} is already initialized."); throw new System.InvalidOperationException(ZString.Format("{0} is already initialized.", GetType().FullName));
Context = context; Context = context;
IsInitialized = true; IsInitialized = true;
@ -55,7 +56,7 @@ namespace AlicizaX
if (_dontDestroyOnLoad) if (_dontDestroyOnLoad)
DontDestroyOnLoad(gameObject); DontDestroyOnLoad(gameObject);
scope.Register(this); scope.RegisterSelf(this);
} }
private void OnDestroy() private void OnDestroy()
@ -69,36 +70,41 @@ namespace AlicizaX
private static ServiceScope ResolveOrCreateScope() private static ServiceScope ResolveOrCreateScope()
{ {
if (typeof(TScope) == typeof(AppScope)) var kind = ScopeKindCache<TScope>.Kind;
return AppServices.RequireWorld().App; if (kind == ServiceScopeKind.App) return AppServices.RequireWorld().App;
if (kind == ServiceScopeKind.Scene) return AppServices.EnsureScene();
if (typeof(TScope) == typeof(SceneScope)) return AppServices.EnsureGameplay();
return AppServices.EnsureScene();
if (typeof(TScope) == typeof(GameplayScope))
return AppServices.EnsureGameplay();
throw new System.InvalidOperationException($"Unsupported service scope: {typeof(TScope).FullName}.");
} }
private static bool TryResolveScope(out ServiceScope scope) private static bool TryResolveScope(out ServiceScope scope)
{ {
if (typeof(TScope) == typeof(AppScope)) var kind = ScopeKindCache<TScope>.Kind;
if (kind == ServiceScopeKind.App)
{ {
scope = AppServices.RequireWorld().App; scope = AppServices.RequireWorld().App;
return true; return true;
} }
if (typeof(TScope) == typeof(SceneScope)) if (kind == ServiceScopeKind.Scene)
return AppServices.TryGetScene(out scope); return AppServices.TryGetScene(out scope);
if (typeof(TScope) == typeof(GameplayScope)) return AppServices.TryGetGameplay(out scope);
return AppServices.TryGetGameplay(out scope);
scope = null;
return false;
} }
protected virtual void OnAwake() { } protected virtual void OnAwake() { }
} }
internal static class ScopeKindCache<TScope>
where TScope : IScope
{
public static readonly ServiceScopeKind Kind = Resolve();
private static ServiceScopeKind Resolve()
{
if (typeof(TScope) == typeof(AppScope)) return ServiceScopeKind.App;
if (typeof(TScope) == typeof(SceneScope)) return ServiceScopeKind.Scene;
if (typeof(TScope) == typeof(GameplayScope)) return ServiceScopeKind.Gameplay;
throw new System.InvalidOperationException(ZString.Format("Unsupported service scope: {0}.", typeof(TScope).FullName));
}
}
} }

View File

@ -286,7 +286,7 @@ namespace AlicizaX
return; return;
} }
_loopService = AppServices.RegisterApp(new UtilityLoopService()); _loopService = AppServices.RegisterAppSelf(new UtilityLoopService());
} }
private static UtilityLoopService EnsureLoopService() private static UtilityLoopService EnsureLoopService()

View File

@ -16,7 +16,7 @@ namespace AlicizaX.Audio.Runtime
private void Awake() private void Awake()
{ {
_audioService = AppServices.RegisterApp(new AudioService()); _audioService = AppServices.RegisterApp<IAudioService>(new AudioService());
} }
private void Start() private void Start()

View File

@ -8,83 +8,89 @@ namespace AlicizaX.Debugger.Runtime
{ {
private sealed class TimerInformationWindow : ScrollableDebuggerWindowBase private sealed class TimerInformationWindow : ScrollableDebuggerWindowBase
{ {
private const int MAX_DISPLAY_COUNT = 50; private const int MAX_DISPLAY_COUNT = 64;
private const float REFRESH_INTERVAL = 0.25f; private const float REFRESH_INTERVAL = 0.25f;
private const string OVERFLOW_NOTE = "Showing first 50 active timers."; private const string OVERFLOW_NOTE = "Active timer count exceeds visible sample.";
private const string EMPTY_NOTE = "No active timers."; private const string EMPTY_NOTE = "No active timers.";
private const string SAMPLE_NOTE = "Zero-allocation runtime sample view."; private const string SAMPLE_NOTE = "Runtime view uses fixed buffers and refresh-only style updates.";
private struct UsageView
{
public VisualElement Fill;
}
private struct RowView private struct RowView
{ {
public VisualElement Root; public VisualElement Root;
public VisualElement Fill;
public VisualElement LoopIndicator; public VisualElement LoopIndicator;
public VisualElement ScaleIndicator; public VisualElement ScaleIndicator;
public VisualElement StateIndicator; public VisualElement StateIndicator;
public VisualElement Fill;
} }
private ITimerService _mTimerDebug; private ITimerService _timerService;
private readonly TimerDebugInfo[] m_TimerInfos = new TimerDebugInfo[MAX_DISPLAY_COUNT]; private ITimerDebugService _timerDebugService;
private readonly RowView[] m_TimerRows = new RowView[MAX_DISPLAY_COUNT]; private readonly TimerDebugInfo[] _timerInfos = new TimerDebugInfo[MAX_DISPLAY_COUNT];
private VisualElement m_ActiveUsageFill; private readonly RowView[] _timerRows = new RowView[MAX_DISPLAY_COUNT];
private VisualElement m_PeakUsageFill; private UsageView _activeUsage;
private VisualElement m_FreeUsageFill; private UsageView _peakUsage;
private VisualElement m_OverflowNote; private UsageView _freeUsage;
private VisualElement m_EmptyNote; private VisualElement _overflowNote;
private float m_RefreshCountdown; private VisualElement _emptyNote;
private float _refreshCountdown;
private int _visibleRowCount;
public override void Initialize(params object[] args) public override void Initialize(params object[] args)
{ {
_mTimerDebug = AppServices.Require<ITimerService>(); if (AppServices.TryGet(out _timerService))
{
_timerDebugService = _timerService as ITimerDebugService;
}
} }
public override void OnEnter() public override void OnEnter()
{ {
m_RefreshCountdown = 0f; _refreshCountdown = 0f;
RefreshContent(); RefreshContent();
} }
public override void OnUpdate(float elapseSeconds, float realElapseSeconds) public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{ {
m_RefreshCountdown -= realElapseSeconds; _refreshCountdown -= realElapseSeconds;
if (m_RefreshCountdown > 0f) if (_refreshCountdown > 0f)
{ {
return; return;
} }
m_RefreshCountdown = REFRESH_INTERVAL; _refreshCountdown = REFRESH_INTERVAL;
RefreshContent(); RefreshContent();
} }
protected override void BuildWindow(VisualElement root) protected override void BuildWindow(VisualElement root)
{ {
if (_mTimerDebug == null)
{
return;
}
root.Add(CreateActionButton("Refresh", RefreshContent, DebuggerTheme.ButtonSurfaceActive, DebuggerTheme.PrimaryText)); root.Add(CreateActionButton("Refresh", RefreshContent, DebuggerTheme.ButtonSurfaceActive, DebuggerTheme.PrimaryText));
VisualElement overview = CreateSection("Timer Pool Overview", out VisualElement overviewCard); VisualElement overview = CreateSection("Timer Pool", out VisualElement overviewCard);
overviewCard.Add(CreateUsageRow("Active Usage", DebuggerTheme.Accent, out m_ActiveUsageFill)); overviewCard.Add(CreateUsageRow("Active", DebuggerTheme.Accent, out _activeUsage));
overviewCard.Add(CreateUsageRow("Peak Usage", DebuggerTheme.Warning, out m_PeakUsageFill)); overviewCard.Add(CreateUsageRow("Peak", DebuggerTheme.Warning, out _peakUsage));
overviewCard.Add(CreateUsageRow("Free Capacity", DebuggerTheme.Positive, out m_FreeUsageFill)); overviewCard.Add(CreateUsageRow("Free", DebuggerTheme.Positive, out _freeUsage));
root.Add(overview); root.Add(overview);
VisualElement sample = CreateSection("Timer Sample", out VisualElement sampleCard); VisualElement sample = CreateSection("Timer Sample", out VisualElement sampleCard);
sampleCard.Add(CreateNoteLabel(SAMPLE_NOTE, DebuggerTheme.SecondaryText)); sampleCard.Add(CreateNoteLabel(SAMPLE_NOTE, DebuggerTheme.SecondaryText));
m_OverflowNote = CreateNoteLabel(OVERFLOW_NOTE, new Color(1f, 0.5f, 0f)); _overflowNote = CreateNoteLabel(OVERFLOW_NOTE, DebuggerTheme.Warning);
m_OverflowNote.style.display = DisplayStyle.None; _overflowNote.style.display = DisplayStyle.None;
sampleCard.Add(m_OverflowNote); sampleCard.Add(_overflowNote);
m_EmptyNote = CreateNoteLabel(EMPTY_NOTE, DebuggerTheme.SecondaryText); _emptyNote = CreateNoteLabel(EMPTY_NOTE, DebuggerTheme.SecondaryText);
m_EmptyNote.style.display = DisplayStyle.None; _emptyNote.style.display = DisplayStyle.None;
sampleCard.Add(m_EmptyNote); sampleCard.Add(_emptyNote);
_visibleRowCount = 0;
for (int i = 0; i < MAX_DISPLAY_COUNT; i++) for (int i = 0; i < MAX_DISPLAY_COUNT; i++)
{ {
m_TimerRows[i] = CreateTimerRow(sampleCard); _timerRows[i] = CreateTimerRow(sampleCard);
m_TimerRows[i].Root.style.display = DisplayStyle.None; _timerRows[i].Root.style.display = DisplayStyle.None;
} }
root.Add(sample); root.Add(sample);
@ -93,50 +99,76 @@ namespace AlicizaX.Debugger.Runtime
private void RefreshContent() private void RefreshContent()
{ {
if (_mTimerDebug == null || m_ActiveUsageFill == null) if (_emptyNote == null || _overflowNote == null)
{ {
return; return;
} }
_mTimerDebug.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount); if (_timerService == null)
{
if (AppServices.TryGet(out _timerService))
{
_timerDebugService = _timerService as ITimerDebugService;
}
if (_timerDebugService == null)
{
SetVisibleRows(0);
return;
}
}
_timerDebugService.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount);
float capacity = poolCapacity > 0 ? poolCapacity : 1f; float capacity = poolCapacity > 0 ? poolCapacity : 1f;
SetFillRatio(m_ActiveUsageFill, activeCount / capacity); SetFillRatio(_activeUsage.Fill, activeCount / capacity);
SetFillRatio(m_PeakUsageFill, peakActiveCount / capacity); SetFillRatio(_peakUsage.Fill, peakActiveCount / capacity);
SetFillRatio(m_FreeUsageFill, freeCount / capacity); SetFillRatio(_freeUsage.Fill, freeCount / capacity);
if (activeCount <= 0) if (activeCount <= 0)
{ {
m_EmptyNote.style.display = DisplayStyle.Flex; _emptyNote.style.display = DisplayStyle.Flex;
m_OverflowNote.style.display = DisplayStyle.None; _overflowNote.style.display = DisplayStyle.None;
SetTimerRowsVisible(0); SetVisibleRows(0);
return; return;
} }
m_EmptyNote.style.display = DisplayStyle.None; _emptyNote.style.display = DisplayStyle.None;
_overflowNote.style.display = activeCount > MAX_DISPLAY_COUNT ? DisplayStyle.Flex : DisplayStyle.None;
int timerCount = _mTimerDebug.GetAllTimers(m_TimerInfos);
int displayCount = timerCount > MAX_DISPLAY_COUNT ? MAX_DISPLAY_COUNT : timerCount;
m_OverflowNote.style.display = activeCount > MAX_DISPLAY_COUNT ? DisplayStyle.Flex : DisplayStyle.None;
int timerCount = _timerDebugService.GetAllTimers(_timerInfos);
int displayCount = timerCount < MAX_DISPLAY_COUNT ? timerCount : MAX_DISPLAY_COUNT;
for (int i = 0; i < displayCount; i++) for (int i = 0; i < displayCount; i++)
{ {
UpdateTimerRow(ref m_TimerRows[i], ref m_TimerInfos[i]); UpdateTimerRow(ref _timerRows[i], ref _timerInfos[i]);
} }
SetTimerRowsVisible(displayCount); SetVisibleRows(displayCount);
} }
private void SetTimerRowsVisible(int visibleCount) private void SetVisibleRows(int visibleCount)
{ {
for (int i = 0; i < MAX_DISPLAY_COUNT; i++) if (_visibleRowCount == visibleCount)
{ {
m_TimerRows[i].Root.style.display = i < visibleCount ? DisplayStyle.Flex : DisplayStyle.None; return;
} }
int minCount = _visibleRowCount < visibleCount ? _visibleRowCount : visibleCount;
for (int i = minCount; i < visibleCount; i++)
{
_timerRows[i].Root.style.display = DisplayStyle.Flex;
}
for (int i = visibleCount; i < _visibleRowCount; i++)
{
_timerRows[i].Root.style.display = DisplayStyle.None;
}
_visibleRowCount = visibleCount;
} }
private static VisualElement CreateUsageRow(string title, Color fillColor, out VisualElement fill) private static VisualElement CreateUsageRow(string title, Color fillColor, out UsageView usageView)
{ {
float scale = DebuggerComponent.Instance != null ? DebuggerComponent.Instance.GetUiScale() : 1f; float scale = GetScale();
VisualElement row = new VisualElement(); VisualElement row = new VisualElement();
row.style.flexDirection = FlexDirection.Column; row.style.flexDirection = FlexDirection.Column;
row.style.marginBottom = 8f * scale; row.style.marginBottom = 8f * scale;
@ -148,80 +180,72 @@ namespace AlicizaX.Debugger.Runtime
titleLabel.style.marginBottom = 4f * scale; titleLabel.style.marginBottom = 4f * scale;
row.Add(titleLabel); row.Add(titleLabel);
VisualElement track = new VisualElement(); VisualElement track = CreateTrack(14f * scale);
track.style.height = 14f * scale; VisualElement fill = new VisualElement();
track.style.backgroundColor = DebuggerTheme.PanelSurfaceStrong;
track.style.borderTopLeftRadius = 4f * scale;
track.style.borderTopRightRadius = 4f * scale;
track.style.borderBottomLeftRadius = 4f * scale;
track.style.borderBottomRightRadius = 4f * scale;
track.style.overflow = Overflow.Hidden;
fill = new VisualElement();
fill.style.height = Length.Percent(100f); fill.style.height = Length.Percent(100f);
fill.style.width = Length.Percent(0f); fill.style.width = Length.Percent(0f);
fill.style.backgroundColor = fillColor; fill.style.backgroundColor = fillColor;
track.Add(fill); track.Add(fill);
row.Add(track); row.Add(track);
usageView.Fill = fill;
return row; return row;
} }
private static VisualElement CreateNoteLabel(string text, Color color)
{
float scale = DebuggerComponent.Instance != null ? DebuggerComponent.Instance.GetUiScale() : 1f;
Label label = new Label(text);
label.style.color = color;
label.style.fontSize = 15f * scale;
label.style.marginBottom = 6f * scale;
label.style.whiteSpace = WhiteSpace.Normal;
return label;
}
private static RowView CreateTimerRow(VisualElement parent) private static RowView CreateTimerRow(VisualElement parent)
{ {
float scale = DebuggerComponent.Instance != null ? DebuggerComponent.Instance.GetUiScale() : 1f; float scale = GetScale();
VisualElement row = new VisualElement(); VisualElement row = new VisualElement();
row.style.flexDirection = FlexDirection.Row; row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center; row.style.alignItems = Align.Center;
row.style.height = 20f * scale; row.style.height = 20f * scale;
row.style.marginBottom = 4f * scale; row.style.marginBottom = 4f * scale;
VisualElement loopIndicator = CreateIndicator(6f * scale, DebuggerTheme.Accent);
VisualElement scaleIndicator = CreateIndicator(6f * scale, DebuggerTheme.Warning);
VisualElement stateIndicator = CreateIndicator(6f * scale, DebuggerTheme.Positive);
row.Add(loopIndicator);
row.Add(scaleIndicator);
row.Add(stateIndicator);
VisualElement track = new VisualElement();
track.style.flexGrow = 1f;
track.style.height = 14f * scale;
track.style.backgroundColor = DebuggerTheme.PanelSurfaceStrong;
track.style.borderTopLeftRadius = 4f * scale;
track.style.borderTopRightRadius = 4f * scale;
track.style.borderBottomLeftRadius = 4f * scale;
track.style.borderBottomRightRadius = 4f * scale;
track.style.overflow = Overflow.Hidden;
VisualElement fill = new VisualElement();
fill.style.height = Length.Percent(100f);
fill.style.width = Length.Percent(0f);
fill.style.backgroundColor = DebuggerTheme.Positive;
track.Add(fill);
row.Add(track);
parent.Add(row);
RowView view; RowView view;
view.Root = row; view.Root = row;
view.LoopIndicator = loopIndicator; view.LoopIndicator = CreateIndicator(6f * scale, DebuggerTheme.Accent);
view.ScaleIndicator = scaleIndicator; view.ScaleIndicator = CreateIndicator(6f * scale, DebuggerTheme.Warning);
view.StateIndicator = stateIndicator; view.StateIndicator = CreateIndicator(6f * scale, DebuggerTheme.Positive);
view.Fill = fill; row.Add(view.LoopIndicator);
row.Add(view.ScaleIndicator);
row.Add(view.StateIndicator);
VisualElement track = CreateTrack(14f * scale);
view.Fill = new VisualElement();
view.Fill.style.height = Length.Percent(100f);
view.Fill.style.width = Length.Percent(0f);
view.Fill.style.backgroundColor = DebuggerTheme.Positive;
track.Add(view.Fill);
row.Add(track);
parent.Add(row);
return view; return view;
} }
private static VisualElement CreateTrack(float height)
{
VisualElement track = new VisualElement();
track.style.flexGrow = 1f;
track.style.height = height;
track.style.backgroundColor = DebuggerTheme.PanelSurfaceStrong;
track.style.borderTopLeftRadius = 4f;
track.style.borderTopRightRadius = 4f;
track.style.borderBottomLeftRadius = 4f;
track.style.borderBottomRightRadius = 4f;
track.style.overflow = Overflow.Hidden;
return track;
}
private static Label CreateNoteLabel(string text, Color color)
{
float scale = GetScale();
Label label = new Label(text);
label.style.color = color;
label.style.fontSize = 15f * scale;
label.style.marginBottom = 6f * scale;
label.style.whiteSpace = WhiteSpace.Normal;
return label;
}
private static VisualElement CreateIndicator(float size, Color color) private static VisualElement CreateIndicator(float size, Color color)
{ {
VisualElement indicator = new VisualElement(); VisualElement indicator = new VisualElement();
@ -239,31 +263,24 @@ namespace AlicizaX.Debugger.Runtime
bool isRunning = (flags & TimerDebugFlags.Running) != 0; bool isRunning = (flags & TimerDebugFlags.Running) != 0;
bool isLoop = (flags & TimerDebugFlags.Loop) != 0; bool isLoop = (flags & TimerDebugFlags.Loop) != 0;
bool isUnscaled = (flags & TimerDebugFlags.Unscaled) != 0; bool isUnscaled = (flags & TimerDebugFlags.Unscaled) != 0;
float duration = info.Duration; float ratio = info.Duration > 0f ? info.LeftTime / info.Duration : 0f;
float ratio = duration > 0f ? info.LeftTime / duration : 0f;
if (ratio < 0f)
{
ratio = 0f;
}
else if (ratio > 1f)
{
ratio = 1f;
}
row.Root.style.display = DisplayStyle.Flex;
row.LoopIndicator.style.opacity = isLoop ? 1f : 0.2f; row.LoopIndicator.style.opacity = isLoop ? 1f : 0.2f;
row.ScaleIndicator.style.opacity = isUnscaled ? 1f : 0.35f; row.ScaleIndicator.style.opacity = isUnscaled ? 1f : 0.35f;
row.ScaleIndicator.style.backgroundColor = isUnscaled ? DebuggerTheme.Warning : DebuggerTheme.Accent; row.ScaleIndicator.style.backgroundColor = isUnscaled ? DebuggerTheme.Warning : DebuggerTheme.Accent;
row.StateIndicator.style.opacity = isRunning ? 1f : 0.45f; row.StateIndicator.style.opacity = isRunning ? 1f : 0.45f;
row.StateIndicator.style.backgroundColor = isRunning ? DebuggerTheme.Positive : DebuggerTheme.Warning; row.StateIndicator.style.backgroundColor = isRunning ? DebuggerTheme.Positive : DebuggerTheme.Warning;
row.Fill.style.width = Length.Percent(ratio * 100f); row.Fill.style.backgroundColor = isRunning ? ratio <= 0.2f ? DebuggerTheme.Warning : DebuggerTheme.Positive : DebuggerTheme.SecondaryText;
row.Fill.style.backgroundColor = isRunning SetFillRatio(row.Fill, ratio);
? ratio <= 0.2f ? DebuggerTheme.Warning : DebuggerTheme.Positive
: DebuggerTheme.SecondaryText;
} }
private static void SetFillRatio(VisualElement fill, float ratio) private static void SetFillRatio(VisualElement fill, float ratio)
{ {
if (fill == null)
{
return;
}
if (ratio < 0f) if (ratio < 0f)
{ {
ratio = 0f; ratio = 0f;
@ -275,6 +292,11 @@ namespace AlicizaX.Debugger.Runtime
fill.style.width = Length.Percent(ratio * 100f); fill.style.width = Length.Percent(ratio * 100f);
} }
private static float GetScale()
{
return Instance != null ? Instance.GetUiScale() : 1f;
}
} }
} }
} }

View File

@ -263,7 +263,7 @@ namespace AlicizaX.Debugger.Runtime
if (!AppServices.TryGetApp<IDebuggerService>(out _mDebuggerService)) if (!AppServices.TryGetApp<IDebuggerService>(out _mDebuggerService))
{ {
_mDebuggerService = AppServices.RegisterApp(new DebuggerService()); _mDebuggerService = AppServices.RegisterApp<IDebuggerService>(new DebuggerService());
} }
if (_mDebuggerService == null) if (_mDebuggerService == null)

View File

@ -55,7 +55,7 @@ namespace AlicizaX.Localization.Runtime
{ {
if (!AppServices.TryGetApp<ILocalizationService>(out _mLocalizationService)) if (!AppServices.TryGetApp<ILocalizationService>(out _mLocalizationService))
{ {
_mLocalizationService = AppServices.RegisterApp(new LocalizationService()); _mLocalizationService = AppServices.RegisterApp<ILocalizationService>(new LocalizationService());
} }
if (_mLocalizationService == null) if (_mLocalizationService == null)

View File

@ -615,13 +615,11 @@ namespace AlicizaX
{ {
MemoryPool<BenchmarkMemory>.ClearAll(); MemoryPool<BenchmarkMemory>.ClearAll();
MemoryPool<BenchmarkMemory>.Prewarm(objectCount); MemoryPool<BenchmarkMemory>.Prewarm(objectCount);
Type type = typeof(BenchmarkMemory);
RestartCaseMeasure(); RestartCaseMeasure();
for (int i = 0; i < loopCount; i++) for (int i = 0; i < loopCount; i++)
{ {
IMemory item = MemoryPool.Acquire(type); BenchmarkMemory item = MemoryPool.Acquire<BenchmarkMemory>();
MemoryPool.Release(item); MemoryPool.Release<BenchmarkMemory>(item);
} }
StopCaseMeasure(); StopCaseMeasure();

View File

@ -14,7 +14,7 @@ namespace AlicizaX
private void Awake() private void Awake()
{ {
_mObjectPoolService = AppServices.RegisterApp(new ObjectPoolService()); _mObjectPoolService = AppServices.RegisterApp<IObjectPoolService>(new ObjectPoolService());
Application.lowMemory += OnLowMemory; Application.lowMemory += OnLowMemory;
} }

View File

@ -32,16 +32,25 @@ namespace AlicizaX.ObjectPool
} }
} }
private ObjectSlot[] m_Slots; private ObjectSlot[][] m_Pages;
private int m_SlotCount; private int[][] m_PageFreeStacks;
private int[] m_FreeStack; private int[] m_PageAliveCounts;
private int m_FreeTop; private int[] m_PageFreeTops;
private byte[] m_PageFlags;
private int m_PageCount;
private int[] m_FreePageStack;
private int m_FreePageTop;
private int[] m_EmptyPageStack;
private int m_EmptyPageTop;
private int[] m_ReleasedPageStack;
private int m_ReleasedPageTop;
private ReferenceOpenHashMap m_TargetMap; private ReferenceOpenHashMap m_TargetMap;
private StringOpenHashMap m_AllNameHeadMap; private StringOpenHashMap m_AllNameHeadMap;
private StringOpenHashMap m_NameCursorMap; private StringOpenHashMap m_NameCursorMap;
private readonly bool m_AllowMultiSpawn; private readonly bool m_AllowMultiSpawn;
private readonly MemoryPoolHandle m_ObjectMemoryPoolHandle;
private float m_AutoReleaseInterval; private float m_AutoReleaseInterval;
private int m_Capacity; private int m_Capacity;
private float m_ExpireTime; private float m_ExpireTime;
@ -59,18 +68,37 @@ namespace AlicizaX.ObjectPool
private const int DefaultReleasePerFrame = 8; private const int DefaultReleasePerFrame = 8;
private const int InitSlotCapacity = 16; private const int InitSlotCapacity = 16;
private const int InitPageCapacity = 4;
private const int PageBits = 8;
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 EmptyPageReleaseBudget = 1;
public ObjectPool(string name, bool allowMultiSpawn, public ObjectPool(string name, bool allowMultiSpawn,
float autoReleaseInterval, int capacity, float expireTime, int priority) float autoReleaseInterval, int capacity, float expireTime, int priority)
: base(name) : base(name)
{ {
int initCap = Math.Min(Math.Max(capacity, 1), InitSlotCapacity); int initCap = Math.Min(Math.Max(capacity, 1), InitSlotCapacity);
m_Slots = SlotArrayPool<ObjectSlot>.Rent(initCap); m_Pages = SlotArrayPool<ObjectSlot[]>.Rent(InitPageCapacity);
m_FreeStack = SlotArrayPool<int>.Rent(initCap); m_PageFreeStacks = SlotArrayPool<int[]>.Rent(InitPageCapacity);
m_PageAliveCounts = SlotArrayPool<int>.Rent(InitPageCapacity);
m_PageFreeTops = SlotArrayPool<int>.Rent(InitPageCapacity);
m_PageFlags = SlotArrayPool<byte>.Rent(InitPageCapacity);
m_FreePageStack = SlotArrayPool<int>.Rent(InitPageCapacity);
m_EmptyPageStack = SlotArrayPool<int>.Rent(InitPageCapacity);
m_ReleasedPageStack = SlotArrayPool<int>.Rent(InitPageCapacity);
m_TargetMap = new ReferenceOpenHashMap(initCap); m_TargetMap = new ReferenceOpenHashMap(initCap);
m_AllNameHeadMap = new StringOpenHashMap(initCap); m_AllNameHeadMap = new StringOpenHashMap(initCap);
m_NameCursorMap = new StringOpenHashMap(initCap); m_NameCursorMap = new StringOpenHashMap(initCap);
m_PageCount = 0;
m_FreePageTop = 0;
m_EmptyPageTop = 0;
m_ReleasedPageTop = 0;
m_AllowMultiSpawn = allowMultiSpawn; m_AllowMultiSpawn = allowMultiSpawn;
m_ObjectMemoryPoolHandle = MemoryPool.GetHandle(typeof(T));
m_AutoReleaseInterval = autoReleaseInterval; m_AutoReleaseInterval = autoReleaseInterval;
m_Capacity = capacity; m_Capacity = capacity;
m_ExpireTime = expireTime; m_ExpireTime = expireTime;
@ -155,7 +183,7 @@ namespace AlicizaX.ObjectPool
if (obj == null) return; if (obj == null) return;
if (obj.Target == null) return; if (obj.Target == null) return;
if (m_TargetMap.TryGetValue(obj.Target, out int existingIdx) && m_Slots[existingIdx].IsAlive()) if (m_TargetMap.TryGetValue(obj.Target, out int existingIdx) && GetSlotRef(existingIdx).IsAlive())
{ {
#if UNITY_EDITOR #if UNITY_EDITOR
UnityEngine.Debug.LogError($"Target '{obj.Target.GetType().FullName}' is already registered in pool '{FullName}'."); UnityEngine.Debug.LogError($"Target '{obj.Target.GetType().FullName}' is already registered in pool '{FullName}'.");
@ -175,7 +203,7 @@ namespace AlicizaX.ObjectPool
if (idx < 0) if (idx < 0)
return; return;
ref var slot = ref m_Slots[idx]; ref var slot = ref GetSlotRef(idx);
slot.Obj = obj; slot.Obj = obj;
slot.SpawnCount = spawned ? 1 : 0; slot.SpawnCount = spawned ? 1 : 0;
slot.LastUseTime = Time.realtimeSinceStartup; slot.LastUseTime = Time.realtimeSinceStartup;
@ -190,7 +218,7 @@ namespace AlicizaX.ObjectPool
string objectName = obj.Name ?? string.Empty; string objectName = obj.Name ?? string.Empty;
if (m_AllNameHeadMap.TryGetValue(objectName, out int existingHead)) if (m_AllNameHeadMap.TryGetValue(objectName, out int existingHead))
{ {
m_Slots[existingHead].PrevByName = idx; GetSlotRef(existingHead).PrevByName = idx;
slot.NextByName = existingHead; slot.NextByName = existingHead;
} }
else else
@ -221,7 +249,7 @@ namespace AlicizaX.ObjectPool
int head = FindAvailableByName(name); int head = FindAvailableByName(name);
if (head < 0) return null; if (head < 0) return null;
ref var slot = ref m_Slots[head]; ref var slot = ref GetSlotRef(head);
if (!slot.IsAlive() || slot.SpawnCount != 0 || !string.Equals(slot.Obj.Name, name, StringComparison.Ordinal)) if (!slot.IsAlive() || slot.SpawnCount != 0 || !string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
{ {
#if UNITY_EDITOR #if UNITY_EDITOR
@ -253,7 +281,7 @@ namespace AlicizaX.ObjectPool
if (!m_AllNameHeadMap.TryGetValue(name, out int head)) if (!m_AllNameHeadMap.TryGetValue(name, out int head))
return -1; return -1;
ref var headSlot = ref m_Slots[head]; ref var headSlot = ref GetSlotRef(head);
if (headSlot.IsAlive() if (headSlot.IsAlive()
&& headSlot.SpawnCount == 0 && headSlot.SpawnCount == 0
&& string.Equals(headSlot.Obj.Name, name, StringComparison.Ordinal)) && string.Equals(headSlot.Obj.Name, name, StringComparison.Ordinal))
@ -269,7 +297,7 @@ namespace AlicizaX.ObjectPool
do do
{ {
ref var slot = ref m_Slots[current]; ref var slot = ref GetSlotRef(current);
if (slot.IsAlive() if (slot.IsAlive()
&& slot.SpawnCount == 0 && slot.SpawnCount == 0
&& string.Equals(slot.Obj.Name, name, StringComparison.Ordinal)) && string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
@ -288,11 +316,9 @@ namespace AlicizaX.ObjectPool
private int GetValidNameCursor(string name, int head) private int GetValidNameCursor(string name, int head)
{ {
if (m_NameCursorMap.TryGetValue(name, out int cursor) if (m_NameCursorMap.TryGetValue(name, out int cursor) && IsValidIndex(cursor))
&& cursor >= 0
&& cursor < m_SlotCount)
{ {
ref var slot = ref m_Slots[cursor]; ref var slot = ref GetSlotRef(cursor);
if (slot.IsAlive() && string.Equals(slot.Obj.Name, name, StringComparison.Ordinal)) if (slot.IsAlive() && string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
return cursor; return cursor;
} }
@ -339,8 +365,8 @@ namespace AlicizaX.ObjectPool
int current = m_UnusedHead; int current = m_UnusedHead;
while (current >= 0) while (current >= 0)
{ {
int next = m_Slots[current].NextUnused; int next = GetSlotRef(current).NextUnused;
ref var slot = ref m_Slots[current]; ref var slot = ref GetSlotRef(current);
if (CanReleaseSlot(ref slot)) if (CanReleaseSlot(ref slot))
{ {
ReleaseSlot(current, false); ReleaseSlot(current, false);
@ -352,8 +378,7 @@ namespace AlicizaX.ObjectPool
if (released > 0) if (released > 0)
{ {
m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released); m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released);
TrimSlotCountTail(); ReleaseEmptyPages(EmptyPageReleaseBudget);
ShrinkStorageIfEmpty();
UpdateActiveState(); UpdateActiveState();
ValidateState(); ValidateState();
} }
@ -400,66 +425,39 @@ namespace AlicizaX.ObjectPool
return; return;
m_ShrinkCounter = 0; m_ShrinkCounter = 0;
ReleaseEmptyPages(EmptyPageReleaseBudget);
TrimSlotCountTail();
int slotArrayLen = m_Slots.Length;
int aliveCount = m_TargetMap.Count;
if (aliveCount == 0 || slotArrayLen <= InitSlotCapacity)
return;
float usageRatio = (float)aliveCount / slotArrayLen;
if (usageRatio < 0.25f)
{
int targetCapacity = Math.Max(NextPowerOf2(Math.Max(m_SlotCount, aliveCount)), InitSlotCapacity);
if (targetCapacity < slotArrayLen && m_SlotCount <= targetCapacity)
{
var newSlots = SlotArrayPool<ObjectSlot>.Rent(targetCapacity);
var newFreeStack = SlotArrayPool<int>.Rent(targetCapacity);
Array.Copy(m_Slots, 0, newSlots, 0, m_SlotCount);
int newFreeTop = 0;
for (int i = 0; i < m_FreeTop; i++)
{
if (m_FreeStack[i] < targetCapacity)
newFreeStack[newFreeTop++] = m_FreeStack[i];
}
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_Slots = newSlots;
m_FreeStack = newFreeStack;
m_FreeTop = newFreeTop;
}
}
} }
internal override void Shutdown() internal override void Shutdown()
{ {
m_IsShuttingDown = true; m_IsShuttingDown = true;
for (int i = 0; i < m_SlotCount; i++) for (int page = 0; page < m_PageCount; page++)
{ {
ref var slot = ref m_Slots[i]; ObjectSlot[] slots = m_Pages[page];
if (!slot.IsAlive()) continue; if (slots == null) continue;
slot.Obj.Release(true);
MemoryPool.Release(slot.Obj); for (int offset = 0; offset < PageSize; offset++)
slot.Obj = null; {
slot.SetAlive(false); ref var slot = ref slots[offset];
if (!slot.IsAlive()) continue;
slot.Obj.Release(true);
m_ObjectMemoryPoolHandle.Release(slot.Obj);
slot.Obj = null;
slot.SetAlive(false);
}
} }
m_TargetMap.Clear(); m_TargetMap.Clear();
m_AllNameHeadMap.Clear(); m_AllNameHeadMap.Clear();
m_NameCursorMap.Clear(); m_NameCursorMap.Clear();
SlotArrayPool<ObjectSlot>.Return(m_Slots, true); ReleaseAllPages();
SlotArrayPool<int>.Return(m_FreeStack, true); ReturnPageStorage();
m_Slots = null;
m_FreeStack = null;
m_SlotCount = 0; m_PageCount = 0;
m_FreeTop = 0; m_FreePageTop = 0;
m_EmptyPageTop = 0;
m_ReleasedPageTop = 0;
m_PendingReleaseCount = 0; m_PendingReleaseCount = 0;
m_UnusedHead = -1; m_UnusedHead = -1;
m_UnusedTail = -1; m_UnusedTail = -1;
@ -480,19 +478,25 @@ namespace AlicizaX.ObjectPool
int write = 0; int write = 0;
int capacity = results.Length; int capacity = results.Length;
for (int i = 0; i < m_SlotCount; i++) for (int page = 0; page < m_PageCount; page++)
{ {
ref var slot = ref m_Slots[i]; ObjectSlot[] slots = m_Pages[page];
if (!slot.IsAlive()) continue; if (slots == null) continue;
if (write < capacity) for (int offset = 0; offset < PageSize; offset++)
{ {
results[write] = new ObjectInfo(slot.Obj.Name, slot.Obj.Locked, ref var slot = ref slots[offset];
slot.Obj.CustomCanReleaseFlag, if (!slot.IsAlive()) continue;
slot.Obj.LastUseTime, slot.SpawnCount);
}
write++; if (write < capacity)
{
results[write] = new ObjectInfo(slot.Obj.Name, slot.Obj.Locked,
slot.Obj.CustomCanReleaseFlag,
slot.Obj.LastUseTime, slot.SpawnCount);
}
write++;
}
} }
return write; return write;
@ -506,7 +510,7 @@ namespace AlicizaX.ObjectPool
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SpawnSlot(int idx, float now) private void SpawnSlot(int idx, float now)
{ {
ref var slot = ref m_Slots[idx]; ref var slot = ref GetSlotRef(idx);
if (slot.SpawnCount == 0) if (slot.SpawnCount == 0)
MarkSlotUnavailable(idx); MarkSlotUnavailable(idx);
@ -519,7 +523,7 @@ namespace AlicizaX.ObjectPool
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UnspawnSlot(int idx) private void UnspawnSlot(int idx)
{ {
ref var slot = ref m_Slots[idx]; ref var slot = ref GetSlotRef(idx);
float now = Time.realtimeSinceStartup; float now = Time.realtimeSinceStartup;
slot.LastUseTime = now; slot.LastUseTime = now;
slot.Obj.LastUseTime = now; slot.Obj.LastUseTime = now;
@ -548,43 +552,65 @@ namespace AlicizaX.ObjectPool
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private int AllocSlot() private int AllocSlot()
{ {
if (m_FreeTop > 0) int page = GetWritablePage();
return m_FreeStack[--m_FreeTop]; if (page < 0)
return -1;
if (m_SlotCount >= m_Slots.Length) int freeTop = m_PageFreeTops[page] - 1;
{ int offset = m_PageFreeStacks[page][freeTop];
GrowSlots(); m_PageFreeTops[page] = freeTop;
if (m_SlotCount >= m_Slots.Length) m_PageAliveCounts[page]++;
return -1;
}
return m_SlotCount++; if (freeTop == 0)
RemoveFreePage(page);
return MakeIndex(page, offset);
} }
private void GrowSlots() private int GetWritablePage()
{ {
int currentCap = m_Slots.Length; while (m_FreePageTop > 0)
int maxCap = m_Capacity == int.MaxValue ? int.MaxValue : Math.Max(m_Capacity, InitSlotCapacity); {
int newCap = Math.Min(Math.Max(currentCap * 2, InitSlotCapacity), maxCap); int page = m_FreePageStack[m_FreePageTop - 1];
if (newCap <= currentCap) if (IsAllocatedPage(page) && m_PageFreeTops[page] > 0)
return; return page;
var newSlots = SlotArrayPool<ObjectSlot>.Rent(newCap); m_FreePageTop--;
var newFreeStack = SlotArrayPool<int>.Rent(newCap); if (page >= 0 && page < m_PageFlags.Length)
m_PageFlags[page] = (byte)(m_PageFlags[page] & ~PageInFreeStack);
}
Array.Copy(m_Slots, 0, newSlots, 0, m_SlotCount); return AllocatePage();
Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop); }
SlotArrayPool<ObjectSlot>.Return(m_Slots, true); private int AllocatePage()
SlotArrayPool<int>.Return(m_FreeStack, true); {
int page;
if (m_ReleasedPageTop > 0)
{
page = m_ReleasedPageStack[--m_ReleasedPageTop];
}
else
{
page = m_PageCount++;
EnsurePageStorageCapacity(m_PageCount);
}
m_Slots = newSlots; m_Pages[page] = SlotArrayPool<ObjectSlot>.Rent(PageSize);
m_FreeStack = newFreeStack; m_PageFreeStacks[page] = SlotArrayPool<int>.Rent(PageSize);
for (int offset = 0; offset < PageSize; offset++)
m_PageFreeStacks[page][offset] = offset;
m_PageAliveCounts[page] = 0;
m_PageFreeTops[page] = PageSize;
m_PageFlags[page] = PageAllocated;
AddFreePage(page);
return page;
} }
private void ReleaseSlot(int idx, bool compactStorage = true) private void ReleaseSlot(int idx, bool compactStorage = true)
{ {
ref var slot = ref m_Slots[idx]; ref var slot = ref GetSlotRef(idx);
if (!slot.IsAlive()) return; if (!slot.IsAlive()) return;
if (slot.SpawnCount > 0) return; if (slot.SpawnCount > 0) return;
@ -595,7 +621,7 @@ namespace AlicizaX.ObjectPool
m_TargetMap.Remove(obj.Target); m_TargetMap.Remove(obj.Target);
obj.Release(false); obj.Release(false);
MemoryPool.Release(obj); m_ObjectMemoryPoolHandle.Release(obj);
slot.Obj = null; slot.Obj = null;
slot.SetAlive(false); slot.SetAlive(false);
@ -605,21 +631,19 @@ namespace AlicizaX.ObjectPool
slot.PrevUnused = -1; slot.PrevUnused = -1;
slot.NextUnused = -1; slot.NextUnused = -1;
if (m_FreeTop >= m_FreeStack.Length) int page = PageOf(idx);
{ int offset = OffsetOf(idx);
int newCap = m_FreeStack.Length * 2; m_PageFreeStacks[page][m_PageFreeTops[page]++] = offset;
var newFreeStack = SlotArrayPool<int>.Rent(newCap); m_PageAliveCounts[page]--;
Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop);
SlotArrayPool<int>.Return(m_FreeStack, true); if (m_PageFreeTops[page] == 1)
m_FreeStack = newFreeStack; AddFreePage(page);
}
m_FreeStack[m_FreeTop++] = idx; if (m_PageAliveCounts[page] == 0)
AddEmptyPageCandidate(page);
if (compactStorage) if (compactStorage)
{ ReleaseEmptyPages(EmptyPageReleaseBudget);
TrimSlotCountTail();
ShrinkStorageIfEmpty();
}
} }
private bool EnsureRegisterCapacity() private bool EnsureRegisterCapacity()
@ -637,36 +661,194 @@ namespace AlicizaX.ObjectPool
return false; return false;
} }
private void TrimSlotCountTail() [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int MakeIndex(int page, int offset)
{ {
while (m_SlotCount > 0 && !m_Slots[m_SlotCount - 1].IsAlive()) return (page << PageBits) | offset;
m_SlotCount--;
int write = 0;
for (int i = 0; i < m_FreeTop; i++)
{
int freeIndex = m_FreeStack[i];
if (freeIndex < m_SlotCount)
m_FreeStack[write++] = freeIndex;
}
m_FreeTop = write;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int NextPowerOf2(int value) private static int PageOf(int index)
{ {
value--; return index >> PageBits;
value |= value >> 1; }
value |= value >> 2;
value |= value >> 4; [MethodImpl(MethodImplOptions.AggressiveInlining)]
value |= value >> 8; private static int OffsetOf(int index)
value |= value >> 16; {
return value + 1; return index & PageMask;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref ObjectSlot GetSlotRef(int index)
{
return ref m_Pages[index >> PageBits][index & PageMask];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsAllocatedPage(int page)
{
return page >= 0
&& page < m_PageCount
&& (m_PageFlags[page] & PageAllocated) != 0
&& m_Pages[page] != null;
}
private void EnsurePageStorageCapacity(int required)
{
if (required <= m_Pages.Length)
return;
int newCap = Math.Max(required, m_Pages.Length * 2);
var newPages = SlotArrayPool<ObjectSlot[]>.Rent(newCap);
var newPageFreeStacks = SlotArrayPool<int[]>.Rent(newCap);
var newPageAliveCounts = SlotArrayPool<int>.Rent(newCap);
var newPageFreeTops = SlotArrayPool<int>.Rent(newCap);
var newPageFlags = SlotArrayPool<byte>.Rent(newCap);
var newFreePageStack = SlotArrayPool<int>.Rent(newCap);
var newEmptyPageStack = SlotArrayPool<int>.Rent(newCap);
var newReleasedPageStack = SlotArrayPool<int>.Rent(newCap);
Array.Copy(m_Pages, 0, newPages, 0, m_PageCount);
Array.Copy(m_PageFreeStacks, 0, newPageFreeStacks, 0, m_PageCount);
Array.Copy(m_PageAliveCounts, 0, newPageAliveCounts, 0, m_PageCount);
Array.Copy(m_PageFreeTops, 0, newPageFreeTops, 0, m_PageCount);
Array.Copy(m_PageFlags, 0, newPageFlags, 0, m_PageCount);
Array.Copy(m_FreePageStack, 0, newFreePageStack, 0, m_FreePageTop);
Array.Copy(m_EmptyPageStack, 0, newEmptyPageStack, 0, m_EmptyPageTop);
Array.Copy(m_ReleasedPageStack, 0, newReleasedPageStack, 0, m_ReleasedPageTop);
SlotArrayPool<ObjectSlot[]>.Return(m_Pages, true);
SlotArrayPool<int[]>.Return(m_PageFreeStacks, true);
SlotArrayPool<int>.Return(m_PageAliveCounts, true);
SlotArrayPool<int>.Return(m_PageFreeTops, true);
SlotArrayPool<byte>.Return(m_PageFlags, true);
SlotArrayPool<int>.Return(m_FreePageStack, true);
SlotArrayPool<int>.Return(m_EmptyPageStack, true);
SlotArrayPool<int>.Return(m_ReleasedPageStack, true);
m_Pages = newPages;
m_PageFreeStacks = newPageFreeStacks;
m_PageAliveCounts = newPageAliveCounts;
m_PageFreeTops = newPageFreeTops;
m_PageFlags = newPageFlags;
m_FreePageStack = newFreePageStack;
m_EmptyPageStack = newEmptyPageStack;
m_ReleasedPageStack = newReleasedPageStack;
}
private static void EnsurePageIndexStackCapacity(ref int[] stack, int required)
{
if (required <= stack.Length)
return;
int newCap = Math.Max(required, stack.Length * 2);
var newStack = SlotArrayPool<int>.Rent(newCap);
Array.Copy(stack, 0, newStack, 0, stack.Length);
SlotArrayPool<int>.Return(stack, true);
stack = newStack;
}
private void AddFreePage(int page)
{
if ((m_PageFlags[page] & PageInFreeStack) != 0)
return;
m_PageFlags[page] = (byte)(m_PageFlags[page] | PageInFreeStack);
EnsurePageIndexStackCapacity(ref m_FreePageStack, m_FreePageTop + 1);
m_FreePageStack[m_FreePageTop++] = page;
}
private void RemoveFreePage(int page)
{
if ((m_PageFlags[page] & PageInFreeStack) == 0)
return;
m_PageFlags[page] = (byte)(m_PageFlags[page] & ~PageInFreeStack);
}
private void AddEmptyPageCandidate(int page)
{
if ((m_PageFlags[page] & PageInEmptyStack) != 0)
return;
m_PageFlags[page] = (byte)(m_PageFlags[page] | PageInEmptyStack);
EnsurePageIndexStackCapacity(ref m_EmptyPageStack, m_EmptyPageTop + 1);
m_EmptyPageStack[m_EmptyPageTop++] = page;
}
private void ReleaseEmptyPages(int budget)
{
while (budget > 0 && m_EmptyPageTop > 0)
{
int page = m_EmptyPageStack[--m_EmptyPageTop];
m_PageFlags[page] = (byte)(m_PageFlags[page] & ~PageInEmptyStack);
if (!IsAllocatedPage(page) || m_PageAliveCounts[page] != 0)
continue;
ReleasePage(page);
AddReleasedPage(page);
budget--;
}
}
private void AddReleasedPage(int page)
{
EnsurePageIndexStackCapacity(ref m_ReleasedPageStack, m_ReleasedPageTop + 1);
m_ReleasedPageStack[m_ReleasedPageTop++] = page;
}
private void ReleasePage(int page)
{
SlotArrayPool<ObjectSlot>.Return(m_Pages[page], true);
SlotArrayPool<int>.Return(m_PageFreeStacks[page], true);
m_Pages[page] = null;
m_PageFreeStacks[page] = null;
m_PageAliveCounts[page] = 0;
m_PageFreeTops[page] = 0;
m_PageFlags[page] = 0;
}
private void ReleaseAllPages()
{
for (int page = 0; page < m_PageCount; page++)
{
if (m_Pages[page] != null)
SlotArrayPool<ObjectSlot>.Return(m_Pages[page], true);
if (m_PageFreeStacks[page] != null)
SlotArrayPool<int>.Return(m_PageFreeStacks[page], true);
m_Pages[page] = null;
m_PageFreeStacks[page] = null;
m_PageAliveCounts[page] = 0;
m_PageFreeTops[page] = 0;
m_PageFlags[page] = 0;
}
}
private void ReturnPageStorage()
{
SlotArrayPool<ObjectSlot[]>.Return(m_Pages, true);
SlotArrayPool<int[]>.Return(m_PageFreeStacks, true);
SlotArrayPool<int>.Return(m_PageAliveCounts, true);
SlotArrayPool<int>.Return(m_PageFreeTops, true);
SlotArrayPool<byte>.Return(m_PageFlags, true);
SlotArrayPool<int>.Return(m_FreePageStack, true);
SlotArrayPool<int>.Return(m_EmptyPageStack, true);
SlotArrayPool<int>.Return(m_ReleasedPageStack, true);
m_Pages = null;
m_PageFreeStacks = null;
m_PageAliveCounts = null;
m_PageFreeTops = null;
m_PageFlags = null;
m_FreePageStack = null;
m_EmptyPageStack = null;
m_ReleasedPageStack = null;
} }
private void RemoveFromAllNameChain(int idx) private void RemoveFromAllNameChain(int idx)
{ {
ref var slot = ref m_Slots[idx]; ref var slot = ref GetSlotRef(idx);
string objectName = slot.Obj.Name ?? string.Empty; string objectName = slot.Obj.Name ?? string.Empty;
if (!m_AllNameHeadMap.TryGetValue(objectName, out int head)) if (!m_AllNameHeadMap.TryGetValue(objectName, out int head))
return; return;
@ -675,7 +857,7 @@ namespace AlicizaX.ObjectPool
int next = slot.NextByName; int next = slot.NextByName;
if (prev >= 0) if (prev >= 0)
{ {
m_Slots[prev].NextByName = next; GetSlotRef(prev).NextByName = next;
} }
else else
{ {
@ -701,7 +883,7 @@ namespace AlicizaX.ObjectPool
} }
if (next >= 0) if (next >= 0)
m_Slots[next].PrevByName = prev; GetSlotRef(next).PrevByName = prev;
slot.PrevByName = -1; slot.PrevByName = -1;
slot.NextByName = -1; slot.NextByName = -1;
@ -714,7 +896,7 @@ namespace AlicizaX.ObjectPool
while (current >= 0 && released < maxReleaseCount) while (current >= 0 && released < maxReleaseCount)
{ {
ref var slot = ref m_Slots[current]; ref var slot = ref GetSlotRef(current);
int next = slot.NextUnused; int next = slot.NextUnused;
if (requireExpired && slot.LastUseTime > expireThreshold) if (requireExpired && slot.LastUseTime > expireThreshold)
@ -738,10 +920,7 @@ namespace AlicizaX.ObjectPool
} }
if (released > 0) if (released > 0)
{ ReleaseEmptyPages(EmptyPageReleaseBudget);
TrimSlotCountTail();
ShrinkStorageIfEmpty();
}
return released; return released;
} }
@ -756,7 +935,7 @@ namespace AlicizaX.ObjectPool
{ {
if (m_LastBudgetScanStart >= 0) if (m_LastBudgetScanStart >= 0)
{ {
ref var slot = ref m_Slots[m_LastBudgetScanStart]; ref var slot = ref GetSlotRef(m_LastBudgetScanStart);
if (slot.IsAlive() && slot.SpawnCount == 0) if (slot.IsAlive() && slot.SpawnCount == 0)
{ {
return m_LastBudgetScanStart; return m_LastBudgetScanStart;
@ -775,23 +954,12 @@ namespace AlicizaX.ObjectPool
&& slot.Obj.CustomCanReleaseFlag; && slot.Obj.CustomCanReleaseFlag;
} }
private void ShrinkStorageIfEmpty() [MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsValidIndex(int index)
{ {
if (m_TargetMap.Count > 0 || m_Slots.Length <= InitSlotCapacity) int page = index >> PageBits;
return; int offset = index & PageMask;
return offset < PageSize && IsAllocatedPage(page);
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_Slots = SlotArrayPool<ObjectSlot>.Rent(InitSlotCapacity);
m_FreeStack = SlotArrayPool<int>.Rent(InitSlotCapacity);
m_AllNameHeadMap.Clear();
m_NameCursorMap.Clear();
m_SlotCount = 0;
m_FreeTop = 0;
m_UnusedHead = -1;
m_UnusedTail = -1;
m_LastBudgetScanStart = -1;
} }
[Conditional("UNITY_EDITOR")] [Conditional("UNITY_EDITOR")]
@ -800,53 +968,60 @@ namespace AlicizaX.ObjectPool
#if UNITY_EDITOR && ENABLE_OBJECTPOOL_VALIDATION #if UNITY_EDITOR && ENABLE_OBJECTPOOL_VALIDATION
int aliveCount = 0; int aliveCount = 0;
int unusedCount = 0; int unusedCount = 0;
for (int i = 0; i < m_SlotCount; i++) for (int page = 0; page < m_PageCount; page++)
{ {
ref var slot = ref m_Slots[i]; ObjectSlot[] slots = m_Pages[page];
if (!slot.IsAlive()) if (slots == null) continue;
continue;
aliveCount++; for (int offset = 0; offset < PageSize; offset++)
object target = slot.Obj.Target;
if (!m_TargetMap.TryGetValue(target, out int mappedIdx) || mappedIdx != i)
{ {
UnityEngine.Debug.LogError($"Object pool '{FullName}' target index map is inconsistent."); int idx = MakeIndex(page, offset);
continue; ref var slot = ref slots[offset];
} if (!slot.IsAlive())
continue;
string objectName = slot.Obj.Name ?? string.Empty; aliveCount++;
if (!m_AllNameHeadMap.TryGetValue(objectName, out int head))
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name head is missing.");
continue;
}
if (slot.PrevByName < 0 && head != i) object target = slot.Obj.Target;
{ if (!m_TargetMap.TryGetValue(target, out int mappedIdx) || mappedIdx != idx)
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain head is inconsistent.");
}
if (slot.NextByName >= 0 && m_Slots[slot.NextByName].PrevByName != i)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain link is inconsistent.");
}
bool inUnusedList = m_UnusedHead == i || slot.PrevUnused >= 0 || slot.NextUnused >= 0;
if (slot.SpawnCount == 0)
{
unusedCount++;
if (!inUnusedList)
{ {
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused list is inconsistent."); UnityEngine.Debug.LogError($"Object pool '{FullName}' target index map is inconsistent.");
continue;
} }
}
else string objectName = slot.Obj.Name ?? string.Empty;
{ if (!m_AllNameHeadMap.TryGetValue(objectName, out int head))
if (inUnusedList)
{ {
UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in unused list."); UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name head is missing.");
continue;
}
if (slot.PrevByName < 0 && head != idx)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain head is inconsistent.");
}
if (slot.NextByName >= 0 && GetSlotRef(slot.NextByName).PrevByName != idx)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain link is inconsistent.");
}
bool inUnusedList = m_UnusedHead == idx || slot.PrevUnused >= 0 || slot.NextUnused >= 0;
if (slot.SpawnCount == 0)
{
unusedCount++;
if (!inUnusedList)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused list is inconsistent.");
}
}
else
{
if (inUnusedList)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in unused list.");
}
} }
} }
} }
@ -861,7 +1036,7 @@ namespace AlicizaX.ObjectPool
int prevUnused = -1; int prevUnused = -1;
while (current >= 0) while (current >= 0)
{ {
ref var slot = ref m_Slots[current]; ref var slot = ref GetSlotRef(current);
if (!slot.IsAlive() || slot.SpawnCount != 0) if (!slot.IsAlive() || slot.SpawnCount != 0)
{ {
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain contains invalid slot."); UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain contains invalid slot.");
@ -886,7 +1061,7 @@ namespace AlicizaX.ObjectPool
private void MarkSlotAvailable(int idx) private void MarkSlotAvailable(int idx)
{ {
AddToUnusedListTail(idx); AddToUnusedListTail(idx);
ref var slot = ref m_Slots[idx]; ref var slot = ref GetSlotRef(idx);
if (slot.IsAlive()) if (slot.IsAlive())
m_NameCursorMap.AddOrUpdate(slot.Obj.Name ?? string.Empty, idx); m_NameCursorMap.AddOrUpdate(slot.Obj.Name ?? string.Empty, idx);
} }
@ -898,7 +1073,7 @@ namespace AlicizaX.ObjectPool
private void AddToUnusedListTail(int idx) private void AddToUnusedListTail(int idx)
{ {
ref var slot = ref m_Slots[idx]; ref var slot = ref GetSlotRef(idx);
if (m_UnusedHead == idx || slot.PrevUnused >= 0 || slot.NextUnused >= 0) if (m_UnusedHead == idx || slot.PrevUnused >= 0 || slot.NextUnused >= 0)
return; return;
@ -906,7 +1081,7 @@ namespace AlicizaX.ObjectPool
slot.NextUnused = -1; slot.NextUnused = -1;
if (m_UnusedTail >= 0) if (m_UnusedTail >= 0)
m_Slots[m_UnusedTail].NextUnused = idx; GetSlotRef(m_UnusedTail).NextUnused = idx;
else else
m_UnusedHead = idx; m_UnusedHead = idx;
@ -915,7 +1090,7 @@ namespace AlicizaX.ObjectPool
private void RemoveFromUnusedList(int idx) private void RemoveFromUnusedList(int idx)
{ {
ref var slot = ref m_Slots[idx]; ref var slot = ref GetSlotRef(idx);
if (m_UnusedHead != idx && slot.PrevUnused < 0 && slot.NextUnused < 0) if (m_UnusedHead != idx && slot.PrevUnused < 0 && slot.NextUnused < 0)
return; return;
@ -923,12 +1098,12 @@ namespace AlicizaX.ObjectPool
int next = slot.NextUnused; int next = slot.NextUnused;
if (prev >= 0) if (prev >= 0)
m_Slots[prev].NextUnused = next; GetSlotRef(prev).NextUnused = next;
else else
m_UnusedHead = next; m_UnusedHead = next;
if (next >= 0) if (next >= 0)
m_Slots[next].PrevUnused = prev; GetSlotRef(next).PrevUnused = prev;
else else
m_UnusedTail = prev; m_UnusedTail = prev;
@ -945,7 +1120,7 @@ namespace AlicizaX.ObjectPool
return null; return null;
float now = Time.realtimeSinceStartup; float now = Time.realtimeSinceStartup;
ref var slot = ref m_Slots[head]; ref var slot = ref GetSlotRef(head);
if (!slot.IsAlive() || !string.Equals(slot.Obj.Name, name, StringComparison.Ordinal)) if (!slot.IsAlive() || !string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
return null; return null;

View File

@ -151,7 +151,7 @@ namespace AlicizaX.Resource.Runtime
private void Awake() private void Awake()
{ {
_resourceService = AppServices.RegisterApp(new ResourceService()); _resourceService = AppServices.RegisterApp<IResourceService>(new ResourceService());
Application.lowMemory += OnLowMemory; Application.lowMemory += OnLowMemory;
} }

View File

@ -11,7 +11,7 @@ namespace AlicizaX.Scene.Runtime
{ {
if (!AppServices.TryGetApp<ISceneService>(out _)) if (!AppServices.TryGetApp<ISceneService>(out _))
{ {
AppServices.RegisterApp(new SceneService()); AppServices.RegisterApp<ISceneService>(new SceneService());
} }
AppServices.EnsureScene(); AppServices.EnsureScene();

View File

@ -292,7 +292,7 @@ namespace AlicizaX.Scene.Runtime
var sceneScope = Context.EnsureScene(); var sceneScope = Context.EnsureScene();
if (!sceneScope.TryGet<SceneDomainStateService>(out var sceneState)) if (!sceneScope.TryGet<SceneDomainStateService>(out var sceneState))
{ {
sceneState = sceneScope.Register(new SceneDomainStateService()); sceneState = (SceneDomainStateService)sceneScope.Register<ISceneStateService>(new SceneDomainStateService());
} }
return sceneState; return sceneState;
@ -301,7 +301,7 @@ namespace AlicizaX.Scene.Runtime
private SceneDomainStateService PrepareSceneStateForMainSceneLoad(string location) private SceneDomainStateService PrepareSceneStateForMainSceneLoad(string location)
{ {
var sceneScope = Context.ResetScene(); var sceneScope = Context.ResetScene();
var sceneState = sceneScope.Register(new SceneDomainStateService()); var sceneState = (SceneDomainStateService)sceneScope.Register<ISceneStateService>(new SceneDomainStateService());
sceneState.MarkMainSceneLoading(location); sceneState.MarkMainSceneLoading(location);
return sceneState; return sceneState;
} }

View File

@ -29,11 +29,27 @@ namespace AlicizaX.Timer.Runtime
float GetLeftTime(ulong timerHandle); float GetLeftTime(ulong timerHandle);
void Restart(ulong timerHandle); void Restart(ulong timerHandle);
void RemoveTimer(ulong timerHandle); void RemoveTimer(ulong timerHandle);
}
[UnityEngine.Scripting.Preserve]
public interface ITimerCapacityService
{
void Prewarm(int capacity);
}
[UnityEngine.Scripting.Preserve]
public interface ITimerDebugService
{
int GetAllTimers(TimerDebugInfo[] results); int GetAllTimers(TimerDebugInfo[] results);
void GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount); void GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount);
}
#if UNITY_EDITOR
[UnityEngine.Scripting.Preserve]
public interface ITimerEditorDebugService
{
int GetStaleOneShotTimers(TimerDebugInfo[] results); int GetStaleOneShotTimers(TimerDebugInfo[] results);
} }
#endif
} }

View File

@ -8,6 +8,12 @@ namespace AlicizaX.Timer.Runtime
[UnityEngine.Scripting.Preserve] [UnityEngine.Scripting.Preserve]
public sealed class TimerComponent : MonoBehaviour public sealed class TimerComponent : MonoBehaviour
{ {
private const int MIN_INITIAL_CAPACITY = 256;
[SerializeField]
[Min(MIN_INITIAL_CAPACITY)]
private int _initialCapacity = 1024;
private void Awake() private void Awake()
{ {
if (AppServices.TryGet<ITimerService>(out _)) if (AppServices.TryGet<ITimerService>(out _))
@ -15,7 +21,15 @@ namespace AlicizaX.Timer.Runtime
return; return;
} }
AppServices.RegisterApp(new TimerService()); AppServices.RegisterApp<ITimerService>(new TimerService(_initialCapacity));
}
private void OnValidate()
{
if (_initialCapacity < MIN_INITIAL_CAPACITY)
{
_initialCapacity = MIN_INITIAL_CAPACITY;
}
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq.Expressions; using System.Linq.Expressions;
using Cysharp.Text;
public static class InstanceFactory public static class InstanceFactory
{ {
@ -10,19 +11,19 @@ public static class InstanceFactory
{ {
if (!_constructorCache.TryGetValue(type, out var constructor)) if (!_constructorCache.TryGetValue(type, out var constructor))
{ {
// 验证是否存在公共无参构造函数 // 楠岃瘉鏄惁瀛樺湪鍏叡鏃犲弬鏋勯€犲嚱鏁?
var ctor = type.GetConstructor(Type.EmptyTypes); var ctor = type.GetConstructor(Type.EmptyTypes);
if (ctor == null) if (ctor == null)
{ {
throw new MissingMethodException($"类型 {type.Name} 缺少公共无参构造函数"); throw new MissingMethodException(ZString.Format("Type {0} missing public parameterless constructor", type.Name));
} }
// 构建表达式树new T() // 鏋勫缓琛ㄨ揪寮忔爲锛歯ew T()
var newExpr = Expression.New(ctor); var newExpr = Expression.New(ctor);
var lambda = Expression.Lambda<Func<object>>(newExpr); var lambda = Expression.Lambda<Func<object>>(newExpr);
constructor = lambda.Compile(); constructor = lambda.Compile();
// 缓存委托 // 缂撳瓨濮旀墭
_constructorCache[type] = constructor; _constructorCache[type] = constructor;
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using AlicizaX.Resource.Runtime; using AlicizaX.Resource.Runtime;
using AlicizaX; using AlicizaX;
using Cysharp.Text;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using UnityEngine; using UnityEngine;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
@ -10,7 +11,6 @@ namespace AlicizaX.UI.Runtime
public static class UIHolderFactory public static class UIHolderFactory
{ {
private static IResourceService ResourceService => AppServices.Require<IResourceService>(); private static IResourceService ResourceService => AppServices.Require<IResourceService>();
private static bool AllowLegacyResourcesFallback => UnityEngine.Application.isEditor || UnityEngine.Debug.isDebugBuild;
public static async UniTask<T> CreateUIHolderAsync<T>(Transform parent) where T : UIHolderObjectBase public static async UniTask<T> CreateUIHolderAsync<T>(Transform parent) where T : UIHolderObjectBase
{ {
@ -90,65 +90,23 @@ namespace AlicizaX.UI.Runtime
private static async UniTask<GameObject> LoadResourceWithFallbackAsync(string location, Transform parent) private static async UniTask<GameObject> LoadResourceWithFallbackAsync(string location, Transform parent)
{ {
try
{
var managedObject = await ResourceService.LoadGameObjectAsync(location, parent);
if (managedObject != null)
{
return managedObject;
}
}
catch
{
if (!AllowLegacyResourcesFallback)
{
throw;
}
}
if (!AllowLegacyResourcesFallback)
{
throw new NullReferenceException($"UI resource load failed via IResourceService: {location}");
}
return await InstantiateResourceAsync(location, parent); return await InstantiateResourceAsync(location, parent);
} }
private static GameObject LoadResourceWithFallbackSync(string location, Transform parent) private static GameObject LoadResourceWithFallbackSync(string location, Transform parent)
{ {
try
{
var managedObject = ResourceService.LoadGameObject(location, parent);
if (managedObject != null)
{
return managedObject;
}
}
catch
{
if (!AllowLegacyResourcesFallback)
{
throw;
}
}
if (!AllowLegacyResourcesFallback)
{
throw new NullReferenceException($"UI resource load failed via IResourceService: {location}");
}
return InstantiateResourceSync(location, parent); return InstantiateResourceSync(location, parent);
} }
private static void ValidateAndBind(UIMetadata meta, GameObject holderObject, UIBase owner) private static void ValidateAndBind(UIMetadata meta, GameObject holderObject, UIBase owner)
{ {
if (!holderObject) throw new NullReferenceException($"UI resource load failed: {meta.ResInfo.Location}"); if (!holderObject) throw new NullReferenceException(ZString.Format("UI resource load failed: {0}", meta.ResInfo.Location));
var holder = (UIHolderObjectBase)holderObject.GetComponent(meta.View.UIHolderType); var holder = (UIHolderObjectBase)holderObject.GetComponent(meta.View.UIHolderType);
if (holder == null) if (holder == null)
{ {
throw new InvalidCastException($"资源{holderObject.name}上不存在{meta.View.UIHolderType.FullName}"); throw new InvalidCastException(ZString.Format("UI resource {0} missing holder component {1}", holderObject.name, meta.View.UIHolderType.FullName));
} }
meta.View?.BindUIHolder(holder, owner); meta.View?.BindUIHolder(holder, owner);

View File

@ -15,8 +15,9 @@ namespace AlicizaX.UI.Runtime
public readonly bool FullScreen; public readonly bool FullScreen;
public readonly int CacheTime; public readonly int CacheTime;
public readonly bool NeedUpdate; public readonly bool NeedUpdate;
public readonly int TypeId;
public UIMetaInfo(RuntimeTypeHandle runtimeTypeHandle, RuntimeTypeHandle holderRuntimeTypeHandle, UILayer windowLayer, bool fullScreen, int cacheTime, bool needUpdate) public UIMetaInfo(RuntimeTypeHandle runtimeTypeHandle, RuntimeTypeHandle holderRuntimeTypeHandle, UILayer windowLayer, bool fullScreen, int cacheTime, bool needUpdate, int typeId)
{ {
RuntimeTypeHandle = runtimeTypeHandle; RuntimeTypeHandle = runtimeTypeHandle;
HolderRuntimeTypeHandle = holderRuntimeTypeHandle; HolderRuntimeTypeHandle = holderRuntimeTypeHandle;
@ -24,18 +25,22 @@ namespace AlicizaX.UI.Runtime
FullScreen = fullScreen; FullScreen = fullScreen;
CacheTime = cacheTime; CacheTime = cacheTime;
NeedUpdate = needUpdate; NeedUpdate = needUpdate;
TypeId = typeId;
} }
} }
private static readonly Dictionary<RuntimeTypeHandle, UIMetaInfo> _typeHandleMap = new(); private static readonly Dictionary<RuntimeTypeHandle, UIMetaInfo> _typeHandleMap = new();
private static readonly Dictionary<string, RuntimeTypeHandle> _stringHandleMap = new(); private static readonly Dictionary<string, RuntimeTypeHandle> _stringHandleMap = new();
private static int _nextTypeId;
public static int TypeCount => _nextTypeId;
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Register(Type uiType, Type holderType, UILayer layer = UILayer.UI, bool fullScreen = false, int cacheTime = 0, bool needUpdate = false) public static void Register(Type uiType, Type holderType, UILayer layer = UILayer.UI, bool fullScreen = false, int cacheTime = 0, bool needUpdate = false)
{ {
RuntimeTypeHandle holderHandle = holderType.TypeHandle; RuntimeTypeHandle holderHandle = holderType.TypeHandle;
RuntimeTypeHandle uiHandle = uiType.TypeHandle; RuntimeTypeHandle uiHandle = uiType.TypeHandle;
_typeHandleMap[uiHandle] = new UIMetaInfo(uiHandle, holderHandle, layer, fullScreen, cacheTime, needUpdate); int typeId = _typeHandleMap.TryGetValue(uiHandle, out UIMetaInfo oldInfo) ? oldInfo.TypeId : _nextTypeId++;
_typeHandleMap[uiHandle] = new UIMetaInfo(uiHandle, holderHandle, layer, fullScreen, cacheTime, needUpdate, typeId);
_stringHandleMap[uiType.Name] = uiHandle; _stringHandleMap[uiType.Name] = uiHandle;
} }

View File

@ -2,6 +2,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using AlicizaX; using AlicizaX;
using Cysharp.Text;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
namespace AlicizaX.UI.Runtime namespace AlicizaX.UI.Runtime
@ -12,10 +13,21 @@ namespace AlicizaX.UI.Runtime
public readonly UIMetaRegistry.UIMetaInfo MetaInfo; public readonly UIMetaRegistry.UIMetaInfo MetaInfo;
public readonly UIResRegistry.UIResInfo ResInfo; public readonly UIResRegistry.UIResInfo ResInfo;
public readonly Type UILogicType; public readonly Type UILogicType;
public readonly string UILogicTypeName;
public readonly string UIHolderTypeName;
public bool InCache = false; public bool InCache = false;
private CancellationTokenSource _cancellationTokenSource; private CancellationTokenSource _cancellationTokenSource;
public CancellationToken CancellationToken => _cancellationTokenSource?.Token ?? CancellationToken.None; public CancellationToken CancellationToken => _cancellationTokenSource?.Token ?? CancellationToken.None;
private int _operationVersion;
private bool _cancelRequested;
private bool _showInProgress;
private bool _closeInProgress;
public int OperationVersion => _operationVersion;
public bool CancelRequested => _cancelRequested;
public bool ShowInProgress => _showInProgress;
public bool CloseInProgress => _closeInProgress;
public UIState State public UIState State
{ {
@ -48,8 +60,64 @@ namespace AlicizaX.UI.Runtime
} }
} }
public bool BeginShowOperation()
{
if (_showInProgress)
{
return false;
}
RequestCancelCurrentOperation();
EnsureCancellationToken();
_operationVersion++;
_cancelRequested = false;
_showInProgress = true;
_closeInProgress = false;
return true;
}
public bool BeginCloseOperation()
{
if (_closeInProgress)
{
return false;
}
RequestCancelCurrentOperation();
EnsureCancellationToken();
_operationVersion++;
_cancelRequested = false;
_closeInProgress = true;
_showInProgress = false;
return true;
}
public void EndShowOperation(int operationVersion)
{
if (_operationVersion == operationVersion)
{
_showInProgress = false;
}
}
public void EndCloseOperation(int operationVersion)
{
if (_operationVersion == operationVersion)
{
_closeInProgress = false;
}
}
public void CancelAsyncOperations() public void CancelAsyncOperations()
{ {
RequestCancelCurrentOperation();
_showInProgress = false;
_closeInProgress = false;
}
public void RequestCancelCurrentOperation()
{
_cancelRequested = true;
_cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose(); _cancellationTokenSource?.Dispose();
_cancellationTokenSource = null; _cancellationTokenSource = null;
@ -90,16 +158,19 @@ namespace AlicizaX.UI.Runtime
} }
UILogicType = uiType; UILogicType = uiType;
UILogicTypeName = uiType.Name;
if (!UIMetaRegistry.TryGet(UILogicType.TypeHandle, out MetaInfo)) if (!UIMetaRegistry.TryGet(UILogicType.TypeHandle, out MetaInfo))
{ {
throw new InvalidOperationException($"[UI] Metadata not registered for {UILogicType.FullName}"); throw new InvalidOperationException(ZString.Format("[UI] Metadata not registered for {0}", UILogicType.FullName));
} }
if (!UIResRegistry.TryGet(MetaInfo.HolderRuntimeTypeHandle, out ResInfo)) if (!UIResRegistry.TryGet(MetaInfo.HolderRuntimeTypeHandle, out ResInfo))
{ {
throw new InvalidOperationException($"[UI] Resource metadata not registered for holder of {UILogicType.FullName}"); throw new InvalidOperationException(ZString.Format("[UI] Resource metadata not registered for holder of {0}", UILogicType.FullName));
} }
UIHolderTypeName = Type.GetTypeFromHandle(MetaInfo.HolderRuntimeTypeHandle)?.Name;
} }
} }
} }

View File

@ -12,14 +12,6 @@ namespace AlicizaX.UI.Runtime
return obj; return obj;
} }
public static UIMetadataObject Create(UIMetadata target, RuntimeTypeHandle handle)
{
UIMetadataObject obj = MemoryPool.Acquire<UIMetadataObject>();
obj.Initialize(handle.GetHashCode().ToString(), target);
return obj;
}
protected internal override void Release(bool isShutdown) protected internal override void Release(bool isShutdown)
{ {
UIMetadata metadata = Target; UIMetadata metadata = Target;

View File

@ -0,0 +1,105 @@
using System;
using UnityEngine;
namespace AlicizaX.UI.Runtime
{
internal interface IUIDebugService
{
int LayerCount { get; }
int CacheWindowCount { get; }
void FillServiceDebugInfo(UIServiceDebugInfo info);
bool FillLayerDebugInfo(int layerIndex, UILayerDebugInfo info);
bool FillWindowDebugInfo(int layerIndex, int windowIndex, UIWindowDebugInfo info);
int FillCacheDebugInfo(UIWindowDebugInfo[] infos, int capacity);
}
internal sealed class UIServiceDebugInfo
{
public bool Initialized;
public bool Orthographic;
public int LayerCount;
public int OpenWindowCount;
public int CacheWindowCount;
public int UpdateWindowCount;
public ulong BlockTimerHandle;
public bool BlockActive;
public Camera Camera;
public Canvas Canvas;
public Transform Root;
public Transform CanvasRoot;
public void Clear()
{
Initialized = false;
Orthographic = false;
LayerCount = 0;
OpenWindowCount = 0;
CacheWindowCount = 0;
UpdateWindowCount = 0;
BlockTimerHandle = 0UL;
BlockActive = false;
Camera = null;
Canvas = null;
Root = null;
CanvasRoot = null;
}
}
internal sealed class UILayerDebugInfo
{
public int LayerIndex;
public UILayer Layer;
public int WindowCount;
public int LastFullscreenIndex;
public RectTransform RectTransform;
public void Clear()
{
LayerIndex = 0;
Layer = UILayer.Background;
WindowCount = 0;
LastFullscreenIndex = -1;
RectTransform = null;
}
}
internal sealed class UIWindowDebugInfo
{
public int LayerIndex;
public int OrderIndex;
public RuntimeTypeHandle RuntimeTypeHandle;
public string LogicTypeName;
public string HolderTypeName;
public UIState State;
public bool Visible;
public bool FullScreen;
public bool InCache;
public bool NeedUpdate;
public bool ShowInProgress;
public bool CloseInProgress;
public int Depth;
public float CacheTime;
public ulong CacheTimerHandle;
public Transform HolderTransform;
public void Clear()
{
LayerIndex = 0;
OrderIndex = 0;
RuntimeTypeHandle = default;
LogicTypeName = null;
HolderTypeName = null;
State = UIState.Uninitialized;
Visible = false;
FullScreen = false;
InCache = false;
NeedUpdate = false;
ShowInProgress = false;
CloseInProgress = false;
Depth = 0;
CacheTime = 0f;
CacheTimerHandle = 0UL;
HolderTransform = null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bdaac10881036e041859876d3852c452
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic;
using AlicizaX; using AlicizaX;
using AlicizaX.Timer.Runtime; using AlicizaX.Timer.Runtime;
using Cysharp.Text;
namespace AlicizaX.UI.Runtime namespace AlicizaX.UI.Runtime
{ {
@ -19,7 +19,9 @@ namespace AlicizaX.UI.Runtime
} }
} }
private readonly Dictionary<RuntimeTypeHandle, CacheEntry> m_CacheWindow = new(); private CacheEntry[] m_CacheWindow = new CacheEntry[8];
private int[] m_CacheTypeIdToIndex = CreateCacheIndexArray(8);
private int m_CacheWindowCount;
private void CacheWindow(UIMetadata uiMetadata, bool force) private void CacheWindow(UIMetadata uiMetadata, bool force)
{ {
@ -35,7 +37,7 @@ namespace AlicizaX.UI.Runtime
return; return;
} }
RemoveFromCache(uiMetadata.MetaInfo.RuntimeTypeHandle); RemoveFromCache(uiMetadata.MetaInfo.TypeId);
ulong timerHandle = 0UL; ulong timerHandle = 0UL;
uiMetadata.View.Holder.transform.SetParent(UICacheLayer); uiMetadata.View.Holder.transform.SetParent(UICacheLayer);
@ -51,36 +53,115 @@ namespace AlicizaX.UI.Runtime
if (timerHandle == 0UL) if (timerHandle == 0UL)
{ {
Log.Warning($"Failed to create cache timer for {uiMetadata.UILogicType.Name}"); Log.Warning(ZString.Format("Failed to create cache timer for {0}", uiMetadata.UILogicType.Name));
} }
} }
uiMetadata.InCache = true; uiMetadata.InCache = true;
m_CacheWindow.Add(uiMetadata.MetaInfo.RuntimeTypeHandle, new CacheEntry(uiMetadata, timerHandle)); AddToCache(uiMetadata, timerHandle);
} }
private void OnTimerDisposeWindow(UIMetadata meta) private void OnTimerDisposeWindow(UIMetadata meta)
{ {
if (meta != null) if (meta != null)
{ {
RemoveFromCache(meta.MetaInfo.RuntimeTypeHandle); RemoveFromCache(meta.MetaInfo.TypeId);
meta.DisposeImmediate(); meta.DisposeImmediate();
} }
} }
private void RemoveFromCache(RuntimeTypeHandle typeHandle) private void RemoveFromCache(RuntimeTypeHandle typeHandle)
{ {
if (m_CacheWindow.TryGetValue(typeHandle, out var entry)) if (UIMetaRegistry.TryGet(typeHandle, out UIMetaRegistry.UIMetaInfo metaInfo))
{ {
m_CacheWindow.Remove(typeHandle); RemoveFromCache(metaInfo.TypeId);
entry.Metadata.InCache = false;
if (entry.TimerHandle != 0UL && _timerService != null)
{
_timerService.RemoveTimer(entry.TimerHandle);
}
} }
} }
private void RemoveFromCache(int typeId)
{
if ((uint)typeId >= (uint)m_CacheTypeIdToIndex.Length)
{
return;
}
int index = m_CacheTypeIdToIndex[typeId];
if (index < 0 || index >= m_CacheWindowCount)
{
return;
}
CacheEntry entry = m_CacheWindow[index];
int lastIndex = m_CacheWindowCount - 1;
CacheEntry last = m_CacheWindow[lastIndex];
m_CacheWindow[index] = last;
m_CacheWindow[lastIndex] = default;
m_CacheWindowCount = lastIndex;
m_CacheTypeIdToIndex[typeId] = -1;
if (index != lastIndex && last.Metadata != null)
{
m_CacheTypeIdToIndex[last.Metadata.MetaInfo.TypeId] = index;
}
entry.Metadata.InCache = false;
if (entry.TimerHandle != 0UL && _timerService != null)
{
_timerService.RemoveTimer(entry.TimerHandle);
}
}
private void AddToCache(UIMetadata metadata, ulong timerHandle)
{
int typeId = metadata.MetaInfo.TypeId;
EnsureCacheIndexCapacity(typeId);
EnsureCacheCapacity();
int index = m_CacheWindowCount++;
m_CacheWindow[index] = new CacheEntry(metadata, timerHandle);
m_CacheTypeIdToIndex[typeId] = index;
}
private void EnsureCacheCapacity()
{
if (m_CacheWindowCount < m_CacheWindow.Length)
{
return;
}
Array.Resize(ref m_CacheWindow, m_CacheWindow.Length << 1);
}
private void EnsureCacheIndexCapacity(int typeId)
{
if ((uint)typeId < (uint)m_CacheTypeIdToIndex.Length)
{
return;
}
int oldLength = m_CacheTypeIdToIndex.Length;
int newLength = oldLength;
while (newLength <= typeId)
{
newLength <<= 1;
}
Array.Resize(ref m_CacheTypeIdToIndex, newLength);
for (int i = oldLength; i < newLength; i++)
{
m_CacheTypeIdToIndex[i] = -1;
}
}
private static int[] CreateCacheIndexArray(int capacity)
{
int[] values = new int[capacity];
for (int i = 0; i < values.Length; i++)
{
values[i] = -1;
}
return values;
}
private ITimerService GetTimerService() private ITimerService GetTimerService()
{ {
if (_timerService != null) if (_timerService != null)

View File

@ -0,0 +1,149 @@
namespace AlicizaX.UI.Runtime
{
internal sealed partial class UIService
{
int IUIDebugService.LayerCount => _openUI.Length;
int IUIDebugService.CacheWindowCount => m_CacheWindowCount;
void IUIDebugService.FillServiceDebugInfo(UIServiceDebugInfo info)
{
if (info == null)
{
return;
}
int openWindowCount = 0;
int updateWindowCount = 0;
for (int i = 0; i < _openUI.Length; i++)
{
LayerData layer = _openUI[i];
if (layer == null)
{
continue;
}
int count = layer.Count;
openWindowCount += count;
for (int j = 0; j < count; j++)
{
UIMetadata metadata = layer.Items[j];
if (metadata != null && metadata.MetaInfo.NeedUpdate)
{
updateWindowCount++;
}
}
}
info.Initialized = UIRoot != null && UICanvas != null;
info.Orthographic = _isOrthographic;
info.LayerCount = _openUI.Length;
info.OpenWindowCount = openWindowCount;
info.CacheWindowCount = m_CacheWindowCount;
info.UpdateWindowCount = updateWindowCount;
info.BlockTimerHandle = m_LastCountDownHandle;
info.BlockActive = m_LayerBlock != null && m_LayerBlock.activeSelf;
info.Camera = UICamera;
info.Canvas = UICanvas;
info.Root = UIRoot;
info.CanvasRoot = UICanvasRoot;
}
bool IUIDebugService.FillLayerDebugInfo(int layerIndex, UILayerDebugInfo info)
{
if (info == null || (uint)layerIndex >= (uint)_openUI.Length)
{
return false;
}
LayerData layer = _openUI[layerIndex];
if (layer == null)
{
info.Clear();
info.LayerIndex = layerIndex;
info.Layer = (UILayer)layerIndex;
return false;
}
info.LayerIndex = layerIndex;
info.Layer = (UILayer)layerIndex;
info.WindowCount = layer.Count;
info.LastFullscreenIndex = layer.LastFullscreenIndex;
info.RectTransform = m_AllWindowLayer[layerIndex];
return true;
}
bool IUIDebugService.FillWindowDebugInfo(int layerIndex, int windowIndex, UIWindowDebugInfo info)
{
if (info == null || (uint)layerIndex >= (uint)_openUI.Length)
{
return false;
}
LayerData layer = _openUI[layerIndex];
if (layer == null || (uint)windowIndex >= (uint)layer.Count)
{
info?.Clear();
return false;
}
FillWindowDebugInfo(layer.Items[windowIndex], layerIndex, windowIndex, 0UL, info);
return true;
}
int IUIDebugService.FillCacheDebugInfo(UIWindowDebugInfo[] infos, int capacity)
{
if (infos == null || capacity <= 0)
{
return 0;
}
int index = 0;
for (int i = 0; i < m_CacheWindowCount; i++)
{
if (index >= capacity || index >= infos.Length)
{
break;
}
UIWindowDebugInfo info = infos[index];
if (info != null)
{
CacheEntry entry = m_CacheWindow[i];
FillWindowDebugInfo(entry.Metadata, entry.Metadata.MetaInfo.UILayer, index, entry.TimerHandle, info);
}
index++;
}
return index;
}
private static void FillWindowDebugInfo(UIMetadata metadata, int layerIndex, int orderIndex, ulong timerHandle, UIWindowDebugInfo info)
{
if (metadata == null)
{
info.Clear();
return;
}
UIBase view = metadata.View;
UIHolderObjectBase holder = view?.Holder;
info.LayerIndex = layerIndex;
info.OrderIndex = orderIndex;
info.RuntimeTypeHandle = metadata.MetaInfo.RuntimeTypeHandle;
info.LogicTypeName = metadata.UILogicTypeName;
info.HolderTypeName = metadata.UIHolderTypeName;
info.State = metadata.State;
info.Visible = view != null && view.Visible;
info.FullScreen = metadata.MetaInfo.FullScreen;
info.InCache = metadata.InCache;
info.NeedUpdate = metadata.MetaInfo.NeedUpdate;
info.ShowInProgress = metadata.ShowInProgress;
info.CloseInProgress = metadata.CloseInProgress;
info.Depth = view != null ? view.Depth : 0;
info.CacheTime = metadata.MetaInfo.CacheTime;
info.CacheTimerHandle = timerHandle;
info.HolderTransform = holder != null ? holder.transform : null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7e3f116e63c64ff4d82fa01efb769894
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using Cysharp.Text;
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace AlicizaX.UI.Runtime namespace AlicizaX.UI.Runtime
@ -57,7 +58,7 @@ namespace AlicizaX.UI.Runtime
private void AddLayer(int layer) private void AddLayer(int layer)
{ {
var layerObject = new GameObject($"Layer{layer}-{(UILayer)layer}"); var layerObject = new GameObject(ZString.Format("Layer{0}-{1}", layer, (UILayer)layer));
var rect = layerObject.AddComponent<RectTransform>(); var rect = layerObject.AddComponent<RectTransform>();
rect.SetParent(UICanvasRoot); rect.SetParent(UICanvasRoot);
rect.localScale = Vector3.one; rect.localScale = Vector3.one;

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using AlicizaX; using AlicizaX;
@ -9,16 +8,54 @@ namespace AlicizaX.UI.Runtime
{ {
sealed class LayerData sealed class LayerData
{ {
public readonly List<UIMetadata> OrderList; public UIMetadata[] Items;
public readonly Dictionary<RuntimeTypeHandle, int> IndexMap; public int[] TypeIdToIndex;
public int Count;
public int LastFullscreenIndex; public int LastFullscreenIndex;
public LayerData(int initialCapacity) public LayerData(int initialCapacity)
{ {
OrderList = new List<UIMetadata>(initialCapacity); Items = new UIMetadata[initialCapacity];
IndexMap = new Dictionary<RuntimeTypeHandle, int>(initialCapacity); TypeIdToIndex = new int[initialCapacity];
for (int i = 0; i < TypeIdToIndex.Length; i++)
{
TypeIdToIndex[i] = -1;
}
Count = 0;
LastFullscreenIndex = -1; LastFullscreenIndex = -1;
} }
public void EnsureTypeCapacity(int typeId)
{
if ((uint)typeId < (uint)TypeIdToIndex.Length)
{
return;
}
int oldLength = TypeIdToIndex.Length;
int newLength = oldLength;
while (newLength <= typeId)
{
newLength <<= 1;
}
Array.Resize(ref TypeIdToIndex, newLength);
for (int i = oldLength; i < newLength; i++)
{
TypeIdToIndex[i] = -1;
}
}
public void EnsureItemCapacity()
{
if (Count < Items.Length)
{
return;
}
Array.Resize(ref Items, Items.Length << 1);
}
} }
internal sealed partial class UIService internal sealed partial class UIService
@ -28,30 +65,51 @@ namespace AlicizaX.UI.Runtime
private async UniTask<UIBase> ShowUIImplAsync(UIMetadata metaInfo, params object[] userDatas) private async UniTask<UIBase> ShowUIImplAsync(UIMetadata metaInfo, params object[] userDatas)
{ {
CreateMetaUI(metaInfo); CreateMetaUI(metaInfo);
EnsureMetaCanOpen(metaInfo); if (!metaInfo.BeginShowOperation())
await UIHolderFactory.CreateUIResourceAsync(metaInfo, UICacheLayer);
if (metaInfo.View == null || metaInfo.State == UIState.Uninitialized || metaInfo.State == UIState.Destroyed)
{ {
metaInfo.View?.RefreshParams(userDatas);
return metaInfo.View;
}
int operationVersion = metaInfo.OperationVersion;
await UIHolderFactory.CreateUIResourceAsync(metaInfo, UICacheLayer);
if (operationVersion != metaInfo.OperationVersion || metaInfo.View == null || metaInfo.State == UIState.Uninitialized || metaInfo.State == UIState.Destroyed)
{
metaInfo.EndShowOperation(operationVersion);
return null; return null;
} }
FinalizeShow(metaInfo, userDatas); FinalizeShow(metaInfo, userDatas);
await UpdateVisualState(metaInfo, metaInfo.CancellationToken); bool showResult = await UpdateVisualState(metaInfo, metaInfo.CancellationToken);
return metaInfo.View; metaInfo.EndShowOperation(operationVersion);
return showResult ? metaInfo.View : null;
} }
private UIBase ShowUIImplSync(UIMetadata metaInfo, params object[] userDatas) private UIBase ShowUIImplSync(UIMetadata metaInfo, params object[] userDatas)
{ {
CreateMetaUI(metaInfo); CreateMetaUI(metaInfo);
EnsureMetaCanOpen(metaInfo); if (!metaInfo.BeginShowOperation())
UIHolderFactory.CreateUIResourceSync(metaInfo, UICacheLayer);
if (metaInfo.View == null || metaInfo.State == UIState.Uninitialized || metaInfo.State == UIState.Destroyed)
{ {
metaInfo.View?.RefreshParams(userDatas);
return metaInfo.View;
}
int operationVersion = metaInfo.OperationVersion;
UIHolderFactory.CreateUIResourceSync(metaInfo, UICacheLayer);
if (operationVersion != metaInfo.OperationVersion || metaInfo.View == null || metaInfo.State == UIState.Uninitialized || metaInfo.State == UIState.Destroyed)
{
metaInfo.EndShowOperation(operationVersion);
return null; return null;
} }
FinalizeShow(metaInfo, userDatas); FinalizeShow(metaInfo, userDatas);
UpdateVisualState(metaInfo).Forget(); bool showResult = UpdateVisualStateSync(metaInfo);
metaInfo.EndShowOperation(operationVersion);
if (!showResult)
{
return null;
}
return metaInfo.View; return metaInfo.View;
} }
@ -62,29 +120,35 @@ namespace AlicizaX.UI.Runtime
return; return;
} }
if (!meta.BeginCloseOperation())
{
return;
}
int operationVersion = meta.OperationVersion;
if (meta.State == UIState.CreatedUI) if (meta.State == UIState.CreatedUI)
{ {
meta.CancelAsyncOperations();
await meta.DisposeAsync(); await meta.DisposeAsync();
meta.EndCloseOperation(operationVersion);
return; return;
} }
if (meta.State == UIState.Loaded || meta.State == UIState.Initialized) if (meta.State == UIState.Loaded || meta.State == UIState.Initialized)
{ {
meta.CancelAsyncOperations();
var popResult = Pop(meta); var popResult = Pop(meta);
SortWindowVisible(meta.MetaInfo.UILayer, popResult.previousFullscreenIndex); SortWindowVisible(meta.MetaInfo.UILayer, popResult.previousFullscreenIndex);
SortWindowDepth(meta.MetaInfo.UILayer, popResult.removedIndex >= 0 ? popResult.removedIndex : 0); SortWindowDepth(meta.MetaInfo.UILayer, popResult.removedIndex >= 0 ? popResult.removedIndex : 0);
meta.View.Visible = false; meta.View.Visible = false;
CacheWindow(meta, force); CacheWindow(meta, force);
meta.EndCloseOperation(operationVersion);
return; return;
} }
meta.CancelAsyncOperations(); bool closeResult = await meta.View.InternalClose(meta.CancellationToken);
meta.EnsureCancellationToken(); if (!closeResult || meta.State != UIState.Closed)
await meta.View.InternalClose();
if (meta.State != UIState.Closed)
{ {
meta.EndCloseOperation(operationVersion);
return; return;
} }
@ -92,6 +156,7 @@ namespace AlicizaX.UI.Runtime
SortWindowVisible(meta.MetaInfo.UILayer, closedPopResult.previousFullscreenIndex); SortWindowVisible(meta.MetaInfo.UILayer, closedPopResult.previousFullscreenIndex);
SortWindowDepth(meta.MetaInfo.UILayer, closedPopResult.removedIndex >= 0 ? closedPopResult.removedIndex : 0); SortWindowDepth(meta.MetaInfo.UILayer, closedPopResult.removedIndex >= 0 ? closedPopResult.removedIndex : 0);
CacheWindow(meta, force); CacheWindow(meta, force);
meta.EndCloseOperation(operationVersion);
} }
@ -106,16 +171,6 @@ namespace AlicizaX.UI.Runtime
if (meta.State == UIState.Uninitialized) meta.CreateUI(); if (meta.State == UIState.Uninitialized) meta.CreateUI();
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void EnsureMetaCanOpen(UIMetadata meta)
{
if (meta.State == UIState.Closed)
{
meta.EnsureCancellationToken();
}
}
private void FinalizeShow(UIMetadata meta, object[] userDatas) private void FinalizeShow(UIMetadata meta, object[] userDatas)
{ {
if (meta.InCache) if (meta.InCache)
@ -145,11 +200,14 @@ namespace AlicizaX.UI.Runtime
private void Push(UIMetadata meta) private void Push(UIMetadata meta)
{ {
var layer = _openUI[meta.MetaInfo.UILayer]; var layer = _openUI[meta.MetaInfo.UILayer];
if (!layer.IndexMap.ContainsKey(meta.MetaInfo.RuntimeTypeHandle)) int typeId = meta.MetaInfo.TypeId;
layer.EnsureTypeCapacity(typeId);
if (layer.TypeIdToIndex[typeId] < 0)
{ {
int index = layer.OrderList.Count; layer.EnsureItemCapacity();
layer.OrderList.Add(meta); int index = layer.Count++;
layer.IndexMap[meta.MetaInfo.RuntimeTypeHandle] = index; layer.Items[index] = meta;
layer.TypeIdToIndex[typeId] = index;
if (meta.MetaInfo.FullScreen) if (meta.MetaInfo.FullScreen)
{ {
layer.LastFullscreenIndex = index; layer.LastFullscreenIndex = index;
@ -164,17 +222,27 @@ namespace AlicizaX.UI.Runtime
{ {
var layer = _openUI[meta.MetaInfo.UILayer]; var layer = _openUI[meta.MetaInfo.UILayer];
int previousFullscreenIndex = layer.LastFullscreenIndex; int previousFullscreenIndex = layer.LastFullscreenIndex;
if (layer.IndexMap.TryGetValue(meta.MetaInfo.RuntimeTypeHandle, out int index)) int typeId = meta.MetaInfo.TypeId;
if ((uint)typeId < (uint)layer.TypeIdToIndex.Length)
{ {
layer.OrderList.RemoveAt(index); int index = layer.TypeIdToIndex[typeId];
layer.IndexMap.Remove(meta.MetaInfo.RuntimeTypeHandle); if (index < 0)
for (int i = index; i < layer.OrderList.Count; i++)
{ {
var item = layer.OrderList[i]; return (-1, previousFullscreenIndex);
layer.IndexMap[item.MetaInfo.RuntimeTypeHandle] = i;
} }
int lastIndex = layer.Count - 1;
for (int i = index; i < lastIndex; i++)
{
UIMetadata item = layer.Items[i + 1];
layer.Items[i] = item;
layer.TypeIdToIndex[item.MetaInfo.TypeId] = i;
}
layer.Items[lastIndex] = null;
layer.Count = lastIndex;
layer.TypeIdToIndex[typeId] = -1;
UpdateFullscreenIndexAfterRemove(layer, meta, index); UpdateFullscreenIndexAfterRemove(layer, meta, index);
return (index, previousFullscreenIndex); return (index, previousFullscreenIndex);
} }
@ -196,23 +264,25 @@ namespace AlicizaX.UI.Runtime
private void MoveToTop(UIMetadata meta) private void MoveToTop(UIMetadata meta)
{ {
var layer = _openUI[meta.MetaInfo.UILayer]; var layer = _openUI[meta.MetaInfo.UILayer];
int lastIdx = layer.OrderList.Count - 1; int lastIdx = layer.Count - 1;
int typeId = meta.MetaInfo.TypeId;
if (!layer.IndexMap.TryGetValue(meta.MetaInfo.RuntimeTypeHandle, out int currentIdx)) if ((uint)typeId >= (uint)layer.TypeIdToIndex.Length)
return; return;
int currentIdx = layer.TypeIdToIndex[typeId];
if (currentIdx != lastIdx && currentIdx >= 0) if (currentIdx != lastIdx && currentIdx >= 0)
{ {
layer.OrderList.RemoveAt(currentIdx);
layer.OrderList.Add(meta);
for (int i = currentIdx; i < lastIdx; i++) for (int i = currentIdx; i < lastIdx; i++)
{ {
var item = layer.OrderList[i]; UIMetadata item = layer.Items[i + 1];
layer.IndexMap[item.MetaInfo.RuntimeTypeHandle] = i; layer.Items[i] = item;
layer.TypeIdToIndex[item.MetaInfo.TypeId] = i;
} }
layer.IndexMap[meta.MetaInfo.RuntimeTypeHandle] = lastIdx; layer.Items[lastIdx] = meta;
layer.TypeIdToIndex[typeId] = lastIdx;
UpdateFullscreenIndexAfterMove(layer, meta, currentIdx, lastIdx); UpdateFullscreenIndexAfterMove(layer, meta, currentIdx, lastIdx);
SortWindowDepth(meta.MetaInfo.UILayer, currentIdx); SortWindowDepth(meta.MetaInfo.UILayer, currentIdx);
@ -220,41 +290,55 @@ namespace AlicizaX.UI.Runtime
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private async UniTask UpdateVisualState(UIMetadata meta, CancellationToken cancellationToken = default) private async UniTask<bool> UpdateVisualState(UIMetadata meta, CancellationToken cancellationToken = default)
{ {
SortWindowVisible(meta.MetaInfo.UILayer); SortWindowVisible(meta.MetaInfo.UILayer);
SortWindowDepth(meta.MetaInfo.UILayer); SortWindowDepth(meta.MetaInfo.UILayer);
if (meta.State == UIState.Loaded) if (meta.State == UIState.Loaded)
{ {
await meta.View.InternalInitlized(cancellationToken); if (!await meta.View.InternalInitlized(cancellationToken))
{
return false;
}
} }
await meta.View.InternalOpen(cancellationToken); return await meta.View.InternalOpen(cancellationToken);
}
private bool UpdateVisualStateSync(UIMetadata meta)
{
SortWindowVisible(meta.MetaInfo.UILayer);
SortWindowDepth(meta.MetaInfo.UILayer);
if (meta.State == UIState.Loaded)
{
if (!meta.View.InternalInitlizedSync()) return false;
}
return meta.View.InternalOpenSync();
} }
private void SortWindowVisible(int layer, int previousFullscreenIndex = int.MinValue) private void SortWindowVisible(int layer, int previousFullscreenIndex = int.MinValue)
{ {
var layerData = _openUI[layer]; var layerData = _openUI[layer];
var list = layerData.OrderList; int count = layerData.Count;
int count = list.Count;
int fullscreenIdx = layerData.LastFullscreenIndex; int fullscreenIdx = layerData.LastFullscreenIndex;
if (fullscreenIdx >= count || (fullscreenIdx >= 0 && !IsDisplayFullscreen(list[fullscreenIdx]))) if (fullscreenIdx >= count || (fullscreenIdx >= 0 && !IsDisplayFullscreen(layerData.Items[fullscreenIdx])))
{ {
fullscreenIdx = FindLastFullscreenIndex(list, count - 1); fullscreenIdx = FindLastFullscreenIndex(layerData, count - 1);
layerData.LastFullscreenIndex = fullscreenIdx; layerData.LastFullscreenIndex = fullscreenIdx;
} }
int oldFullscreenIndex = previousFullscreenIndex == int.MinValue ? fullscreenIdx : previousFullscreenIndex; int oldFullscreenIndex = previousFullscreenIndex == int.MinValue ? fullscreenIdx : previousFullscreenIndex;
if (oldFullscreenIndex == fullscreenIdx) if (oldFullscreenIndex == fullscreenIdx)
{ {
ApplyVisibilityRange(list, fullscreenIdx >= 0 ? fullscreenIdx : 0, count, fullscreenIdx); ApplyVisibilityRange(layerData, fullscreenIdx >= 0 ? fullscreenIdx : 0, count, fullscreenIdx);
return; return;
} }
if (oldFullscreenIndex == -1 && fullscreenIdx == -1) if (oldFullscreenIndex == -1 && fullscreenIdx == -1)
{ {
ApplyVisibilityRange(list, 0, count, -1); ApplyVisibilityRange(layerData, 0, count, -1);
return; return;
} }
@ -265,21 +349,21 @@ namespace AlicizaX.UI.Runtime
? count ? count
: Math.Max(oldFullscreenIndex, fullscreenIdx) + 1; : Math.Max(oldFullscreenIndex, fullscreenIdx) + 1;
ApplyVisibilityRange(list, start, endExclusive, fullscreenIdx); ApplyVisibilityRange(layerData, start, endExclusive, fullscreenIdx);
} }
private void SortWindowDepth(int layer, int startIndex = 0) private void SortWindowDepth(int layer, int startIndex = 0)
{ {
var list = _openUI[layer].OrderList; var layerData = _openUI[layer];
int baseDepth = layer * LAYER_DEEP; int baseDepth = layer * LAYER_DEEP;
for (int i = startIndex; i < list.Count; i++) for (int i = startIndex; i < layerData.Count; i++)
{ {
int newDepth = baseDepth + i * WINDOW_DEEP; int newDepth = baseDepth + i * WINDOW_DEEP;
if (list[i].View.Depth != newDepth) if (layerData.Items[i].View.Depth != newDepth)
{ {
list[i].View.Depth = newDepth; layerData.Items[i].View.Depth = newDepth;
} }
} }
} }
@ -289,11 +373,11 @@ namespace AlicizaX.UI.Runtime
return meta.MetaInfo.FullScreen && UIStateMachine.IsDisplayActive(meta.State); return meta.MetaInfo.FullScreen && UIStateMachine.IsDisplayActive(meta.State);
} }
private static int FindLastFullscreenIndex(List<UIMetadata> list, int startIndex) private static int FindLastFullscreenIndex(LayerData layer, int startIndex)
{ {
for (int i = Math.Min(startIndex, list.Count - 1); i >= 0; i--) for (int i = Math.Min(startIndex, layer.Count - 1); i >= 0; i--)
{ {
if (IsDisplayFullscreen(list[i])) if (IsDisplayFullscreen(layer.Items[i]))
{ {
return i; return i;
} }
@ -302,28 +386,28 @@ namespace AlicizaX.UI.Runtime
return -1; return -1;
} }
private static void ApplyVisibilityRange(List<UIMetadata> list, int startInclusive, int endExclusive, int fullscreenIdx) private static void ApplyVisibilityRange(LayerData layer, int startInclusive, int endExclusive, int fullscreenIdx)
{ {
if (startInclusive < 0) if (startInclusive < 0)
{ {
startInclusive = 0; startInclusive = 0;
} }
if (endExclusive > list.Count) if (endExclusive > layer.Count)
{ {
endExclusive = list.Count; endExclusive = layer.Count;
} }
bool showAll = fullscreenIdx < 0; bool showAll = fullscreenIdx < 0;
for (int i = startInclusive; i < endExclusive; i++) for (int i = startInclusive; i < endExclusive; i++)
{ {
list[i].View.Visible = showAll || i >= fullscreenIdx; layer.Items[i].View.Visible = showAll || i >= fullscreenIdx;
} }
} }
private static void UpdateFullscreenIndexAfterRemove(LayerData layer, UIMetadata removedMeta, int removedIndex) private static void UpdateFullscreenIndexAfterRemove(LayerData layer, UIMetadata removedMeta, int removedIndex)
{ {
if (layer.OrderList.Count == 0) if (layer.Count == 0)
{ {
layer.LastFullscreenIndex = -1; layer.LastFullscreenIndex = -1;
return; return;
@ -331,7 +415,7 @@ namespace AlicizaX.UI.Runtime
if (removedMeta.MetaInfo.FullScreen && layer.LastFullscreenIndex == removedIndex) if (removedMeta.MetaInfo.FullScreen && layer.LastFullscreenIndex == removedIndex)
{ {
layer.LastFullscreenIndex = FindLastFullscreenIndex(layer.OrderList, removedIndex - 1); layer.LastFullscreenIndex = FindLastFullscreenIndex(layer, removedIndex - 1);
return; return;
} }

View File

@ -6,7 +6,7 @@ using UnityEngine;
namespace AlicizaX.UI.Runtime namespace AlicizaX.UI.Runtime
{ {
internal sealed partial class UIService : ServiceBase, IUIService, IServiceTickable internal sealed partial class UIService : ServiceBase, IUIService, IUIDebugService, IServiceTickable
{ {
private ITimerService _timerService; private ITimerService _timerService;
@ -24,16 +24,16 @@ namespace AlicizaX.UI.Runtime
for (int layerIndex = 0; layerIndex < _openUI.Length; layerIndex++) for (int layerIndex = 0; layerIndex < _openUI.Length; layerIndex++)
{ {
var layer = _openUI[layerIndex]; var layer = _openUI[layerIndex];
int count = layer.OrderList.Count; int count = layer.Count;
if (count == 0) continue; if (count == 0) continue;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
if (layer.OrderList.Count != count) if (layer.Count != count)
{ {
break; break;
} }
var window = layer.OrderList[i]; var window = layer.Items[i];
if (window.MetaInfo.NeedUpdate) if (window.MetaInfo.NeedUpdate)
window.View.InternalUpdate(); window.View.InternalUpdate();
} }
@ -110,10 +110,10 @@ namespace AlicizaX.UI.Runtime
continue; continue;
} }
int count = layer.OrderList.Count; int count = layer.Count;
for (int i = count - 1; i >= 0; i--) for (int i = count - 1; i >= 0; i--)
{ {
UIMetadata meta = layer.OrderList[i]; UIMetadata meta = layer.Items[i];
if (meta == null) if (meta == null)
{ {
continue; continue;
@ -123,27 +123,21 @@ namespace AlicizaX.UI.Runtime
meta.DisposeImmediate(); meta.DisposeImmediate();
} }
layer.OrderList.Clear(); Array.Clear(layer.Items, 0, layer.Count);
layer.IndexMap.Clear(); for (int i = 0; i < layer.TypeIdToIndex.Length; i++)
{
layer.TypeIdToIndex[i] = -1;
}
layer.Count = 0;
layer.LastFullscreenIndex = -1; layer.LastFullscreenIndex = -1;
} }
if (m_CacheWindow.Count > 0) if (m_CacheWindowCount > 0)
{ {
RuntimeTypeHandle[] handles = new RuntimeTypeHandle[m_CacheWindow.Count]; for (int i = m_CacheWindowCount - 1; i >= 0; i--)
int writeIndex = 0;
foreach (var pair in m_CacheWindow)
{ {
handles[writeIndex++] = pair.Key; CacheEntry entry = m_CacheWindow[i];
}
for (int i = 0; i < writeIndex; i++)
{
if (!m_CacheWindow.TryGetValue(handles[i], out CacheEntry entry))
{
continue;
}
if (entry.TimerHandle != 0UL && _timerService != null) if (entry.TimerHandle != 0UL && _timerService != null)
{ {
_timerService.RemoveTimer(entry.TimerHandle); _timerService.RemoveTimer(entry.TimerHandle);
@ -152,9 +146,15 @@ namespace AlicizaX.UI.Runtime
entry.Metadata.InCache = false; entry.Metadata.InCache = false;
entry.Metadata.CancelAsyncOperations(); entry.Metadata.CancelAsyncOperations();
entry.Metadata.DisposeImmediate(); entry.Metadata.DisposeImmediate();
m_CacheWindow[i] = default;
} }
m_CacheWindow.Clear(); for (int i = 0; i < m_CacheTypeIdToIndex.Length; i++)
{
m_CacheTypeIdToIndex[i] = -1;
}
m_CacheWindowCount = 0;
} }
if (m_LastCountDownHandle != 0UL && _timerService != null) if (m_LastCountDownHandle != 0UL && _timerService != null)

View File

@ -1,6 +1,4 @@
using System; using System;
using System.Buffers;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using AlicizaX; using AlicizaX;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
@ -10,12 +8,15 @@ namespace AlicizaX.UI.Runtime
{ {
public abstract partial class UIBase public abstract partial class UIBase
{ {
private readonly Dictionary<UIBase, UIMetadata> _children = new(); private UIMetadata[] _children = new UIMetadata[8];
private readonly List<UIMetadata> _updateableChildren = new(); private int[] _childTypeIdToIndex = CreateIndexArray(8);
private int _childCount;
private UIMetadata[] _updateableChildren = new UIMetadata[4];
private int _updateableChildCount;
private void UpdateChildren() private void UpdateChildren()
{ {
for (int i = 0; i < _updateableChildren.Count; i++) for (int i = 0; i < _updateableChildCount; i++)
{ {
var meta = _updateableChildren[i]; var meta = _updateableChildren[i];
if (meta.View.State == UIState.Opened) if (meta.View.State == UIState.Opened)
@ -27,72 +28,44 @@ namespace AlicizaX.UI.Runtime
private async UniTask DestroyAllChildren() private async UniTask DestroyAllChildren()
{ {
var temp = ArrayPool<UIMetadata>.Shared.Rent(_children.Count); while (_childCount > 0)
try
{ {
int i = 0; UIMetadata metadata = _children[--_childCount];
foreach (var kvp in _children) _children[_childCount] = null;
ClearChildIndex(metadata);
if (metadata.View.Visible)
{ {
temp[i++] = kvp.Value; metadata.CancelAsyncOperations();
metadata.EnsureCancellationToken();
await metadata.View.InternalClose(metadata.CancellationToken);
} }
for (int j = 0; j < i; j++) await metadata.DisposeAsync();
{ UIMetadataFactory.ReturnToPool(metadata);
UIMetadata metadata = temp[j];
if (metadata.View.Visible)
{
metadata.CancelAsyncOperations();
metadata.EnsureCancellationToken();
await metadata.View.InternalClose(metadata.CancellationToken);
}
await metadata.DisposeAsync();
UIMetadataFactory.ReturnToPool(metadata);
temp[j] = null;
}
}
finally
{
ArrayPool<UIMetadata>.Shared.Return(temp, true);
} }
_children.Clear(); _updateableChildCount = 0;
_updateableChildren.Clear();
} }
private void DestroyAllChildrenImmediate() private void DestroyAllChildrenImmediate()
{ {
var temp = ArrayPool<UIMetadata>.Shared.Rent(_children.Count); while (_childCount > 0)
try
{ {
int i = 0; UIMetadata metadata = _children[--_childCount];
foreach (var kvp in _children) _children[_childCount] = null;
{ ClearChildIndex(metadata);
temp[i++] = kvp.Value; metadata.DisposeImmediate();
} UIMetadataFactory.ReturnToPool(metadata);
for (int j = 0; j < i; j++)
{
UIMetadata metadata = temp[j];
metadata.DisposeImmediate();
UIMetadataFactory.ReturnToPool(metadata);
temp[j] = null;
}
}
finally
{
ArrayPool<UIMetadata>.Shared.Return(temp, true);
} }
_children.Clear(); _updateableChildCount = 0;
_updateableChildren.Clear();
} }
private void ChildVisible(bool value) private void ChildVisible(bool value)
{ {
foreach (KeyValuePair<UIBase, UIMetadata> kvp in _children) for (int i = 0; i < _childCount; i++)
{ {
UIBase view = kvp.Value.View; UIBase view = _children[i].View;
if (view.State == UIState.Opened) if (view.State == UIState.Opened)
{ {
view.Visible = value; view.Visible = value;
@ -112,7 +85,7 @@ namespace AlicizaX.UI.Runtime
{ {
metadata.CreateUI(); metadata.CreateUI();
UIHolderFactory.CreateUIResourceSync(metadata, parent, this); UIHolderFactory.CreateUIResourceSync(metadata, parent, this);
ProcessWidget(metadata, visible).Forget(); ProcessWidgetSync(metadata, visible);
return (UIBase)metadata.View; return (UIBase)metadata.View;
} }
@ -167,7 +140,7 @@ namespace AlicizaX.UI.Runtime
metadata.CreateUI(); metadata.CreateUI();
UIBase widget = (UIBase)metadata.View; UIBase widget = (UIBase)metadata.View;
widget.BindUIHolder(holder, this); widget.BindUIHolder(holder, this);
ProcessWidget(metadata, true).Forget(); ProcessWidgetSync(metadata, true);
return (T)widget; return (T)widget;
} }
@ -185,7 +158,11 @@ namespace AlicizaX.UI.Runtime
return; return;
} }
await meta.View.InternalInitlized(cancellationToken); if (!await meta.View.InternalInitlized(cancellationToken))
{
return;
}
meta.View.Visible = visible; meta.View.Visible = visible;
if (meta.View.Visible) if (meta.View.Visible)
{ {
@ -195,15 +172,23 @@ namespace AlicizaX.UI.Runtime
private bool AddWidget(UIMetadata meta) private bool AddWidget(UIMetadata meta)
{ {
if (!_children.TryAdd(meta.View, meta)) int typeId = meta.MetaInfo.TypeId;
EnsureChildIndexCapacity(typeId);
if (_childTypeIdToIndex[typeId] >= 0)
{ {
Log.Warning("Already has widget:{0}", meta.View); Log.Warning("Already has widget:{0}", meta.View);
return false; return false;
} }
EnsureChildCapacity();
int index = _childCount++;
_children[index] = meta;
_childTypeIdToIndex[typeId] = index;
if (meta.MetaInfo.NeedUpdate) if (meta.MetaInfo.NeedUpdate)
{ {
_updateableChildren.Add(meta); EnsureUpdateableChildCapacity();
_updateableChildren[_updateableChildCount++] = meta;
} }
return true; return true;
@ -211,7 +196,12 @@ namespace AlicizaX.UI.Runtime
public async UniTask RemoveWidget(UIBase widget) public async UniTask RemoveWidget(UIBase widget)
{ {
if (_children.Remove(widget, out var meta)) if (!TryRemoveChild(widget, out var meta))
{
return;
}
if (meta != null)
{ {
meta.CancelAsyncOperations(); meta.CancelAsyncOperations();
meta.EnsureCancellationToken(); meta.EnsureCancellationToken();
@ -229,18 +219,141 @@ namespace AlicizaX.UI.Runtime
private void RemoveUpdateableChild(UIMetadata meta) private void RemoveUpdateableChild(UIMetadata meta)
{ {
for (int i = 0; i < _updateableChildren.Count; i++) for (int i = 0; i < _updateableChildCount; i++)
{ {
if (_updateableChildren[i] != meta) if (_updateableChildren[i] != meta)
{ {
continue; continue;
} }
int lastIndex = _updateableChildren.Count - 1; int lastIndex = _updateableChildCount - 1;
_updateableChildren[i] = _updateableChildren[lastIndex]; _updateableChildren[i] = _updateableChildren[lastIndex];
_updateableChildren.RemoveAt(lastIndex); _updateableChildren[lastIndex] = null;
_updateableChildCount = lastIndex;
return; return;
} }
} }
private void ProcessWidgetSync(UIMetadata meta, bool visible)
{
if (!AddWidget(meta))
{
meta.DisposeImmediate();
UIMetadataFactory.ReturnToPool(meta);
return;
}
if (!meta.View.InternalInitlizedSync())
{
return;
}
meta.View.Visible = visible;
if (meta.View.Visible)
{
meta.View.InternalOpenSync();
}
}
private bool TryRemoveChild(UIBase widget, out UIMetadata meta)
{
meta = null;
if (widget == null)
{
return false;
}
int typeId = widget.UITypeId;
if ((uint)typeId >= (uint)_childTypeIdToIndex.Length)
{
return false;
}
int index = _childTypeIdToIndex[typeId];
if (index < 0 || index >= _childCount)
{
return false;
}
meta = _children[index];
int lastIndex = _childCount - 1;
UIMetadata last = _children[lastIndex];
_children[index] = last;
_children[lastIndex] = null;
_childCount = lastIndex;
_childTypeIdToIndex[typeId] = -1;
if (index != lastIndex && last != null)
{
_childTypeIdToIndex[last.MetaInfo.TypeId] = index;
}
return true;
}
private void ClearChildIndex(UIMetadata metadata)
{
if (metadata == null)
{
return;
}
int typeId = metadata.MetaInfo.TypeId;
if ((uint)typeId < (uint)_childTypeIdToIndex.Length)
{
_childTypeIdToIndex[typeId] = -1;
}
}
private void EnsureChildCapacity()
{
if (_childCount < _children.Length)
{
return;
}
Array.Resize(ref _children, _children.Length << 1);
}
private void EnsureChildIndexCapacity(int typeId)
{
if ((uint)typeId < (uint)_childTypeIdToIndex.Length)
{
return;
}
int oldLength = _childTypeIdToIndex.Length;
int newLength = oldLength;
while (newLength <= typeId)
{
newLength <<= 1;
}
Array.Resize(ref _childTypeIdToIndex, newLength);
for (int i = oldLength; i < newLength; i++)
{
_childTypeIdToIndex[i] = -1;
}
}
private void EnsureUpdateableChildCapacity()
{
if (_updateableChildCount < _updateableChildren.Length)
{
return;
}
Array.Resize(ref _updateableChildren, _updateableChildren.Length << 1);
}
private static int[] CreateIndexArray(int capacity)
{
int[] values = new int[capacity];
for (int i = 0; i < values.Length; i++)
{
values[i] = -1;
}
return values;
}
} }
} }

View File

@ -32,6 +32,7 @@ namespace AlicizaX.UI.Runtime
protected System.Object[] UserDatas => _userDatas; protected System.Object[] UserDatas => _userDatas;
private RuntimeTypeHandle _runtimeTypeHandle; private RuntimeTypeHandle _runtimeTypeHandle;
private int _uiTypeId = -1;
internal RuntimeTypeHandle RuntimeTypeHandler internal RuntimeTypeHandle RuntimeTypeHandler
{ {
@ -106,6 +107,19 @@ namespace AlicizaX.UI.Runtime
Dispose(true); Dispose(true);
} }
internal int UITypeId
{
get
{
if (_uiTypeId < 0 && UIMetaRegistry.TryGet(RuntimeTypeHandler, out UIMetaRegistry.UIMetaInfo metaInfo))
{
_uiTypeId = metaInfo.TypeId;
}
return _uiTypeId;
}
}
private void Dispose(bool disposing) private void Dispose(bool disposing)
{ {
if (_disposed) return; if (_disposed) return;
@ -204,81 +218,137 @@ namespace AlicizaX.UI.Runtime
internal abstract void BindUIHolder(UIHolderObjectBase holder, UIBase owner); internal abstract void BindUIHolder(UIHolderObjectBase holder, UIBase owner);
internal async UniTask InternalInitlized(CancellationToken cancellationToken = default) internal async UniTask<bool> InternalInitlized(CancellationToken cancellationToken = default)
{ {
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Initialized)) if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Initialized))
return; return false;
_state = UIState.Initialized; _state = UIState.Initialized;
Holder.OnWindowInitEvent?.Invoke(); Holder.OnWindowInitEvent?.Invoke();
await OnInitializeAsync(); await OnInitializeAsync();
if (cancellationToken.IsCancellationRequested)
return false;
OnRegisterEvent(EventListenerProxy); OnRegisterEvent(EventListenerProxy);
return true;
} }
internal async UniTask InternalOpen(CancellationToken cancellationToken = default) internal bool InternalInitlizedSync()
{
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Initialized))
return false;
_state = UIState.Initialized;
Holder.OnWindowInitEvent?.Invoke();
OnInitialize();
OnRegisterEvent(EventListenerProxy);
return true;
}
internal async UniTask<bool> InternalOpen(CancellationToken cancellationToken = default)
{ {
if (_state == UIState.Opened || _state == UIState.Opening) if (_state == UIState.Opened || _state == UIState.Opening)
return; return _state == UIState.Opened;
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Opening)) if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Opening))
return; return false;
int lifecycleVersion = BeginLifecycleTransition(); int lifecycleVersion = BeginLifecycleTransition();
_state = UIState.Opening; _state = UIState.Opening;
Visible = true; Visible = true;
Holder.OnWindowBeforeShowEvent?.Invoke(); Holder.OnWindowBeforeShowEvent?.Invoke();
try await OnOpenAsync();
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening) || cancellationToken.IsCancellationRequested)
{ {
await OnOpenAsync(); RollbackOpeningState(lifecycleVersion);
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening)) return false;
return;
cancellationToken.ThrowIfCancellationRequested();
await Holder.PlayOpenTransitionAsync(cancellationToken);
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
bool openCanceled = await Holder.PlayOpenTransitionAsync(cancellationToken).SuppressCancellationThrow();
if (openCanceled || cancellationToken.IsCancellationRequested)
{ {
return; RollbackOpeningState(lifecycleVersion);
return false;
} }
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening)) if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening))
return; return false;
_state = UIState.Opened; _state = UIState.Opened;
Holder.OnWindowAfterShowEvent?.Invoke(); Holder.OnWindowAfterShowEvent?.Invoke();
return true;
} }
internal async UniTask InternalClose(CancellationToken cancellationToken = default) internal bool InternalOpenSync()
{
if (_state == UIState.Opened || _state == UIState.Opening)
return _state == UIState.Opened;
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Opening))
return false;
int lifecycleVersion = BeginLifecycleTransition();
_state = UIState.Opening;
Visible = true;
Holder.OnWindowBeforeShowEvent?.Invoke();
OnOpen();
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening))
return false;
_state = UIState.Opened;
Holder.OnWindowAfterShowEvent?.Invoke();
return true;
}
internal async UniTask<bool> InternalClose(CancellationToken cancellationToken = default)
{ {
if (_state == UIState.Closed || _state == UIState.Closing) if (_state == UIState.Closed || _state == UIState.Closing)
return; return _state == UIState.Closed;
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Closing)) if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Closing))
return; return false;
int lifecycleVersion = BeginLifecycleTransition(); int lifecycleVersion = BeginLifecycleTransition();
_state = UIState.Closing; _state = UIState.Closing;
Holder.OnWindowBeforeClosedEvent?.Invoke(); Holder.OnWindowBeforeClosedEvent?.Invoke();
try await OnCloseAsync();
{ if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing) || cancellationToken.IsCancellationRequested)
await OnCloseAsync(); return false;
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing))
return;
cancellationToken.ThrowIfCancellationRequested(); bool closeCanceled = await Holder.PlayCloseTransitionAsync(cancellationToken).SuppressCancellationThrow();
await Holder.PlayCloseTransitionAsync(cancellationToken); if (closeCanceled || cancellationToken.IsCancellationRequested)
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{ {
return; return false;
} }
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing)) if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing))
return; return false;
Visible = false; Visible = false;
_state = UIState.Closed; _state = UIState.Closed;
Holder.OnWindowAfterClosedEvent?.Invoke(); Holder.OnWindowAfterClosedEvent?.Invoke();
return true;
}
internal bool InternalCloseSync()
{
if (_state == UIState.Closed || _state == UIState.Closing)
return _state == UIState.Closed;
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Closing))
return false;
int lifecycleVersion = BeginLifecycleTransition();
_state = UIState.Closing;
Holder.OnWindowBeforeClosedEvent?.Invoke();
OnClose();
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing))
return false;
Visible = false;
_state = UIState.Closed;
Holder.OnWindowAfterClosedEvent?.Invoke();
return true;
} }
internal void InternalUpdate() internal void InternalUpdate()
@ -342,6 +412,15 @@ namespace AlicizaX.UI.Runtime
return lifecycleVersion == _lifecycleVersion && _state == state; return lifecycleVersion == _lifecycleVersion && _state == state;
} }
private void RollbackOpeningState(int lifecycleVersion)
{
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening))
return;
Visible = false;
_state = UIState.Initialized;
}
#endregion #endregion
} }
} }

View File

@ -1,28 +1,26 @@
using System;
using System.Collections.Generic;
namespace AlicizaX.UI.Runtime namespace AlicizaX.UI.Runtime
{ {
internal static class UIStateMachine internal static class UIStateMachine
{ {
private static readonly Dictionary<UIState, HashSet<UIState>> _validTransitions = new() private static readonly ushort[] ValidTransitionMasks =
{ {
[UIState.Uninitialized] = new() { UIState.CreatedUI }, Mask(UIState.CreatedUI),
[UIState.CreatedUI] = new() { UIState.Loaded, UIState.Destroying }, Mask(UIState.Loaded, UIState.Destroying),
[UIState.Loaded] = new() { UIState.Initialized, UIState.Destroying }, Mask(UIState.Initialized, UIState.Destroying),
[UIState.Initialized] = new() { UIState.Opening, UIState.Destroying }, Mask(UIState.Opening, UIState.Destroying),
[UIState.Opening] = new() { UIState.Opened, UIState.Closing, UIState.Destroying }, Mask(UIState.Opened, UIState.Closing, UIState.Destroying),
[UIState.Opened] = new() { UIState.Closing, UIState.Destroying }, Mask(UIState.Closing, UIState.Destroying),
[UIState.Closing] = new() { UIState.Opening, UIState.Closed, UIState.Destroying }, Mask(UIState.Opening, UIState.Closed, UIState.Destroying),
[UIState.Closed] = new() { UIState.Opening, UIState.Destroying }, Mask(UIState.Opening, UIState.Destroying),
[UIState.Destroying] = new() { UIState.Destroyed }, Mask(UIState.Destroyed),
[UIState.Destroyed] = new() { } 0,
}; };
public static bool IsValidTransition(UIState from, UIState to) public static bool IsValidTransition(UIState from, UIState to)
{ {
return _validTransitions.TryGetValue(from, out var validStates) && validStates.Contains(to); int fromIndex = (int)from;
return (uint)fromIndex < (uint)ValidTransitionMasks.Length && (ValidTransitionMasks[fromIndex] & Mask(to)) != 0;
} }
public static bool ValidateTransition(string uiName, UIState from, UIState to) public static bool ValidateTransition(string uiName, UIState from, UIState to)
@ -30,16 +28,15 @@ namespace AlicizaX.UI.Runtime
if (IsValidTransition(from, to)) if (IsValidTransition(from, to))
return true; return true;
Log.Error($"[UI] Invalid state transition for {uiName}: {from} -> {to}"); Log.Error(Cysharp.Text.ZString.Format("[UI] Invalid state transition for {0}: {1} -> {2}", uiName, from, to));
return false; return false;
} }
public static HashSet<UIState> GetValidNextStates(UIState currentState) public static ushort GetValidNextStateMask(UIState currentState)
{ {
return _validTransitions.TryGetValue(currentState, out var states) int stateIndex = (int)currentState;
? states return (uint)stateIndex < (uint)ValidTransitionMasks.Length ? ValidTransitionMasks[stateIndex] : (ushort)0;
: new HashSet<UIState>();
} }
public static string GetStateDescription(UIState state) public static string GetStateDescription(UIState state)
@ -64,5 +61,20 @@ namespace AlicizaX.UI.Runtime
{ {
return state == UIState.Opening || state == UIState.Opened || state == UIState.Closing; return state == UIState.Opening || state == UIState.Opened || state == UIState.Closing;
} }
private static ushort Mask(UIState state)
{
return (ushort)(1 << (int)state);
}
private static ushort Mask(UIState stateA, UIState stateB)
{
return (ushort)(Mask(stateA) | Mask(stateB));
}
private static ushort Mask(UIState stateA, UIState stateB, UIState stateC)
{
return (ushort)(Mask(stateA) | Mask(stateB) | Mask(stateC));
}
} }
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using AlicizaX; using AlicizaX;
using Cysharp.Text;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
@ -98,25 +99,19 @@ namespace AlicizaX.UI.Runtime
private async UniTask StartAsyncLoading(RuntimeTypeHandle typeHandle, params System.Object[] userDatas) private async UniTask StartAsyncLoading(RuntimeTypeHandle typeHandle, params System.Object[] userDatas)
{ {
_loadingFlags[typeHandle] = true; _loadingFlags[typeHandle] = true;
UIMetadata metadata = UIMetadataFactory.GetWidgetMetadata(typeHandle);
Transform parent = _tabCache[typeHandle];
try UIBase widget = await CreateWidgetUIAsync(metadata, parent, false);
_loadingFlags.Remove(typeHandle);
if (widget is UIWidget tabWidget)
{ {
UIMetadata metadata = UIMetadataFactory.GetWidgetMetadata(typeHandle);
Transform parent = _tabCache[typeHandle];
UIBase widget = await CreateWidgetUIAsync(metadata, parent, false);
if (widget is not UIWidget tabWidget) return;
_loadedTabs[typeHandle] = tabWidget; _loadedTabs[typeHandle] = tabWidget;
SwitchToLoadedTab(tabWidget, userDatas); SwitchToLoadedTab(tabWidget, userDatas);
} }
catch (Exception e) else
{ {
Debug.LogError($"Tab load failed: {e}"); Debug.LogError(ZString.Format("Tab load failed: {0}", Type.GetTypeFromHandle(typeHandle)?.Name));
}
finally
{
_loadingFlags.Remove(typeHandle);
} }
} }
@ -133,7 +128,7 @@ namespace AlicizaX.UI.Runtime
{ {
if (index >= 0 && index < _typeOrder.Count) return true; if (index >= 0 && index < _typeOrder.Count) return true;
Debug.LogError($"Invalid tab index: {index}"); Debug.LogError(ZString.Format("Invalid tab index: {0}", index));
return false; return false;
} }

View File

@ -13,12 +13,12 @@ namespace AlicizaX.UI.Runtime
public void Open(params System.Object[] userDatas) public void Open(params System.Object[] userDatas)
{ {
RefreshParams(userDatas); RefreshParams(userDatas);
InternalOpen().Forget(); InternalOpenSync();
} }
public void Close() public void Close()
{ {
InternalClose().Forget(); InternalCloseSync();
} }
public void Destroy() public void Destroy()

View File

@ -15,6 +15,7 @@ namespace AlicizaX.UI.Runtime
[SerializeField] private GameObject uiRoot = null; [SerializeField] private GameObject uiRoot = null;
[SerializeField] private bool _isOrthographic = true; [SerializeField] private bool _isOrthographic = true;
private Transform _instanceRoot = null; private Transform _instanceRoot = null;
private const string CanvasScalerMissingMessage = "Not found CanvasScaler !";
private IUIService _uiService; private IUIService _uiService;
@ -24,7 +25,7 @@ namespace AlicizaX.UI.Runtime
private void Awake() private void Awake()
{ {
_uiService = AppServices.RegisterApp(new UIService()); _uiService = AppServices.RegisterApp<IUIService>(new UIService());
if (uiRoot == null) if (uiRoot == null)
{ {
throw new GameFrameworkException("UIRoot Prefab is invalid."); throw new GameFrameworkException("UIRoot Prefab is invalid.");
@ -54,7 +55,7 @@ namespace AlicizaX.UI.Runtime
CanvasScaler scaler = _uiService.UICanvasRoot.GetComponent<CanvasScaler>(); CanvasScaler scaler = _uiService.UICanvasRoot.GetComponent<CanvasScaler>();
if (scaler == null) if (scaler == null)
{ {
Log.Error($"Not found {nameof(CanvasScaler)} !"); Log.Error(CanvasScalerMissingMessage);
return; return;
} }