From de926116aa38030cf9c958fc729dc8b6cc6f5637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Tue, 28 Apr 2026 18:25:04 +0800 Subject: [PATCH] =?UTF-8?q?[Opt]=20AppService=E6=9E=B6=E6=9E=84=20=20UI?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD&=E8=B0=83=E8=AF=95=20ObjectPool=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20Timer=E6=A8=A1=E5=9D=97=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Editor/Timer/TimerComponentInspector.cs | 194 ++- Editor/UI/Inspector/UIComponentInspector.cs | 298 +++- Runtime/ABase/Service/Core/AppScope.cs | 7 + Runtime/ABase/Service/Core/AppServices.cs | 8 +- .../ABase/Service/Core/IServiceRegistry.cs | 7 +- Runtime/ABase/Service/Core/ServiceBase.cs | 4 +- .../Service/Core/ServiceContractUtility.cs | 85 +- Runtime/ABase/Service/Core/ServiceScope.cs | 455 +++-- Runtime/ABase/Service/Core/ServiceWorld.cs | 266 ++- Runtime/ABase/Service/Unity/AppServiceRoot.cs | 6 +- .../Service/Unity/MonoServiceBehaviour.cs | 44 +- Runtime/ABase/Utility/Utility.Unity.cs | 2 +- Runtime/Audio/AudioComponent.cs | 2 +- ...ebuggerComponent.TimerInformationWindow.cs | 266 +-- Runtime/Debugger/DebuggerComponent.cs | 2 +- Runtime/Localization/LocalizationComponent.cs | 2 +- .../Benchmark/MemoryPoolBenchmark.cs | 6 +- Runtime/ObjectPool/ObjectPoolComponent.cs | 2 +- .../ObjectPoolService.ObjectPool.cs | 591 ++++--- .../Resource/Resource/ResourceComponent.cs | 2 +- Runtime/Scene/SceneComponent.cs | 2 +- Runtime/Scene/SceneService.cs | 4 +- Runtime/Timer/ITimerService.cs | 16 + Runtime/Timer/TimerComponent.cs | 16 +- Runtime/Timer/TimerService.cs | 1533 +++++++++-------- Runtime/UI/Constant/Em.cs | 11 +- Runtime/UI/Constant/UIHolderFactory.cs | 48 +- Runtime/UI/Constant/UIMetaRegistry.cs | 9 +- Runtime/UI/Constant/UIMetadata.cs | 75 +- Runtime/UI/Constant/UIMetadataObject.cs | 8 - Runtime/UI/Manager/UIDebugInfo.cs | 105 ++ Runtime/UI/Manager/UIDebugInfo.cs.meta | 11 + Runtime/UI/Manager/UIService.Cache.cs | 107 +- Runtime/UI/Manager/UIService.Debug.cs | 149 ++ Runtime/UI/Manager/UIService.Debug.cs.meta | 11 + Runtime/UI/Manager/UIService.Initlize.cs | 5 +- Runtime/UI/Manager/UIService.Open.cs | 228 ++- Runtime/UI/Manager/UIService.cs | 46 +- Runtime/UI/UIBase/UIBase.Widget.cs | 237 ++- Runtime/UI/UIBase/UIBase.cs | 137 +- Runtime/UI/UIBase/UIStateMachine.cs | 52 +- Runtime/UI/UIBase/UITabWindow.cs | 23 +- Runtime/UI/UIBase/UIWidget.cs | 4 +- Runtime/UI/UIComponent.cs | 5 +- 44 files changed, 3344 insertions(+), 1747 deletions(-) create mode 100644 Runtime/UI/Manager/UIDebugInfo.cs create mode 100644 Runtime/UI/Manager/UIDebugInfo.cs.meta create mode 100644 Runtime/UI/Manager/UIService.Debug.cs create mode 100644 Runtime/UI/Manager/UIService.Debug.cs.meta diff --git a/Editor/Timer/TimerComponentInspector.cs b/Editor/Timer/TimerComponentInspector.cs index e689fa9..637d744 100644 --- a/Editor/Timer/TimerComponentInspector.cs +++ b/Editor/Timer/TimerComponentInspector.cs @@ -8,38 +8,46 @@ namespace AlicizaX.Timer.Editor [CustomEditor(typeof(TimerComponent))] internal sealed class TimerComponentInspector : GameFrameworkInspector { - private const double UPDATE_INTERVAL = 0.25d; - private const int MAX_DISPLAY_COUNT = 20; + private const double UPDATE_INTERVAL = 0.02d; + 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; -#if UNITY_EDITOR - private TimerDebugInfo[] _leakBuffer; -#endif + private readonly TimerDebugInfo[] _timerBuffer = new TimerDebugInfo[DISPLAY_COUNT]; + private readonly TimerDebugInfo[] _staleBuffer = new TimerDebugInfo[DISPLAY_COUNT]; private double _lastUpdateTime; - private int _cachedActiveCount; - private int _cachedPoolCapacity; - private int _cachedPeakActiveCount; - private int _cachedFreeCount; - private string _cachedUsageText; + private SerializedProperty _initialCapacityProperty; public override void OnInspectorGUI() { base.OnInspectorGUI(); - serializedObject.Update(); + DrawConfiguration(); serializedObject.ApplyModifiedProperties(); - 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; - if (currentTime - _lastUpdateTime >= UPDATE_INTERVAL) - { - _lastUpdateTime = currentTime; - Repaint(); - } + _initialCapacityProperty.intValue = sliderValue; } + + EditorGUILayout.HelpBox(Utility.Text.Format("Rounded by {0}. Runtime allocates timer pages during Awake/prewarm.", CAPACITY_STEP), MessageType.None); } private void DrawRuntimeDebugInfo() @@ -56,30 +64,31 @@ namespace AlicizaX.Timer.Editor 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); - _cachedUsageText = _cachedPoolCapacity > 0 - ? Utility.Text.Format("{0:F1}%", (float)_cachedActiveCount / _cachedPoolCapacity * 100f) - : "0.0%"; + timerDebugService.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount); EditorGUILayout.Space(); EditorGUILayout.LabelField("Runtime Debug", EditorStyles.boldLabel); - EditorGUILayout.LabelField("Active Timers", _cachedActiveCount.ToString()); - EditorGUILayout.LabelField("Pool Capacity", _cachedPoolCapacity.ToString()); - EditorGUILayout.LabelField("Peak Active Count", _cachedPeakActiveCount.ToString()); - EditorGUILayout.LabelField("Free Slots", _cachedFreeCount.ToString()); - EditorGUILayout.LabelField("Pool Usage", _cachedUsageText); + DrawStatistic("Active Timers", activeCount); + DrawStatistic("Pool Capacity", poolCapacity); + DrawStatistic("Peak Active Count", peakActiveCount); + DrawStatistic("Free Slots", freeCount); + DrawUsageBar("Active Usage", activeCount, poolCapacity); + DrawUsageBar("Peak Usage", peakActiveCount, poolCapacity); - DrawTimerList(timerService, _cachedActiveCount); -#if UNITY_EDITOR - DrawLeakDetection(timerService, _cachedActiveCount); -#endif + DrawTimerList(timerDebugService, activeCount); + DrawStaleTimerList(timerDebugService, activeCount); } - private void DrawTimerList(ITimerService debug, int activeCount) + private void DrawTimerList(ITimerDebugService timerDebugService, int activeCount) { EditorGUILayout.Space(); - EditorGUILayout.LabelField("Active Timers", EditorStyles.boldLabel); + EditorGUILayout.LabelField("Active Timer Sample", EditorStyles.boldLabel); if (activeCount <= 0) { @@ -87,84 +96,93 @@ namespace AlicizaX.Timer.Editor return; } - EnsureTimerBuffer(activeCount); - int timerCount = debug.GetAllTimers(_timerBuffer); - int displayCount = Mathf.Min(timerCount, MAX_DISPLAY_COUNT); - - if (displayCount < activeCount) + int timerCount = timerDebugService.GetAllTimers(_timerBuffer); + if (activeCount > DISPLAY_COUNT) { - 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]; - 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); + DrawTimerInfo(ref _timerBuffer[i]); } } -#if UNITY_EDITOR - private void DrawLeakDetection(ITimerService debug, int activeCount) + private void DrawStaleTimerList(ITimerDebugService timerDebugService, int activeCount) { if (activeCount <= 0) { return; } - EnsureLeakBuffer(activeCount); - int staleCount = debug.GetStaleOneShotTimers(_leakBuffer); + if (!(timerDebugService is ITimerEditorDebugService editorDebugService)) + { + return; + } + + int staleCount = editorDebugService.GetStaleOneShotTimers(_staleBuffer); if (staleCount <= 0) { return; } EditorGUILayout.Space(); - EditorGUILayout.LabelField(Utility.Text.Format("Stale One-Shot Timers ({0})", staleCount), EditorStyles.boldLabel); - EditorGUILayout.HelpBox("Non-loop timers older than 5 minutes. This may indicate long-delay tasks or paused timers.", MessageType.Warning); - + EditorGUILayout.HelpBox("Long-lived one-shot timers detected.", MessageType.Warning); for (int i = 0; i < staleCount; i++) { - TimerDebugInfo staleTimer = _leakBuffer[i]; - EditorGUILayout.LabelField( - 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]; + TimerDebugInfo info = _staleBuffer[i]; + EditorGUILayout.LabelField(Utility.Text.Format("ID {0}", info.TimerHandle), Utility.Text.Format("Age {0:F1}s | Left {1:F2}s", info.Age, info.LeftTime)); } } -#if UNITY_EDITOR - private void EnsureLeakBuffer(int count) + private static void DrawTimerInfo(ref TimerDebugInfo info) { - int capacity = count > 0 ? count : 1; - if (_leakBuffer == null || _leakBuffer.Length < capacity) - { - _leakBuffer = new TimerDebugInfo[capacity]; - } + byte flags = info.Flags; + string mode = (flags & TimerDebugFlags.Loop) != 0 ? "Loop" : "Once"; + string scale = (flags & TimerDebugFlags.Unscaled) != 0 ? "Unscaled" : "Scaled"; + 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 } } diff --git a/Editor/UI/Inspector/UIComponentInspector.cs b/Editor/UI/Inspector/UIComponentInspector.cs index 93ac960..6c50a6a 100644 --- a/Editor/UI/Inspector/UIComponentInspector.cs +++ b/Editor/UI/Inspector/UIComponentInspector.cs @@ -1,4 +1,4 @@ -using System; +using System; using AlicizaX.Editor; using AlicizaX.UI.Runtime; using UnityEditor; @@ -9,9 +9,26 @@ namespace AlicizaX.UI.Editor [CustomEditor(typeof(UIComponent))] 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 _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() { @@ -27,7 +44,7 @@ namespace AlicizaX.UI.Editor 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) { @@ -36,7 +53,7 @@ namespace AlicizaX.UI.Editor if (uiRoot.objectReferenceValue == null) { - if (GUILayout.Button("设置默认")) + if (GUILayout.Button("Set Default")) { GameObject defaultPrefab = AssetDatabase.LoadAssetAtPath(UIGlobalPath.UIPrefabPath); uiRoot.objectReferenceValue = defaultPrefab; @@ -49,14 +66,283 @@ namespace AlicizaX.UI.Editor } EditorGUI.EndDisabledGroup(); serializedObject.ApplyModifiedProperties(); - Repaint(); - } + DrawRuntimeDebugInfo(); + if (EditorApplication.isPlaying) + { + Repaint(); + } + } private void OnEnable() { uiRoot = serializedObject.FindProperty("uiRoot"); _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(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; } } + } diff --git a/Runtime/ABase/Service/Core/AppScope.cs b/Runtime/ABase/Service/Core/AppScope.cs index 06ec262..92b20eb 100644 --- a/Runtime/ABase/Service/Core/AppScope.cs +++ b/Runtime/ABase/Service/Core/AppScope.cs @@ -26,4 +26,11 @@ namespace AlicizaX { public int Order { get; } } + + internal enum ServiceScopeKind : byte + { + App = 0, + Scene = 1, + Gameplay = 2, + } } diff --git a/Runtime/ABase/Service/Core/AppServices.cs b/Runtime/ABase/Service/Core/AppServices.cs index 1fa9873..fe19da6 100644 --- a/Runtime/ABase/Service/Core/AppServices.cs +++ b/Runtime/ABase/Service/Core/AppServices.cs @@ -21,8 +21,12 @@ namespace AlicizaX return _world; } - public static T RegisterApp(T service, params Type[] extraContracts) where T : class, IService - => RequireWorld().App.Register(service, extraContracts); + public static T RegisterAppSelf(T service) where T : class, IService + => RequireWorld().App.RegisterSelf(service); + + public static TContract RegisterApp(IService service) + where TContract : class, IService + => RequireWorld().App.Register(service); public static bool TryGetApp(out T service) where T : class, IService { diff --git a/Runtime/ABase/Service/Core/IServiceRegistry.cs b/Runtime/ABase/Service/Core/IServiceRegistry.cs index 651ec14..4574fb3 100644 --- a/Runtime/ABase/Service/Core/IServiceRegistry.cs +++ b/Runtime/ABase/Service/Core/IServiceRegistry.cs @@ -1,10 +1,9 @@ -using System; - -namespace AlicizaX +namespace AlicizaX { public interface IServiceRegistry { - T Register(T service, params Type[] extraContracts) where T : class, IService; + T RegisterSelf(T service) where T : class, IService; + TContract Register(IService service) where TContract : class, IService; bool TryGet(out T service) where T : class, IService; T Require() where T : class, IService; } diff --git a/Runtime/ABase/Service/Core/ServiceBase.cs b/Runtime/ABase/Service/Core/ServiceBase.cs index 53fce49..429ff91 100644 --- a/Runtime/ABase/Service/Core/ServiceBase.cs +++ b/Runtime/ABase/Service/Core/ServiceBase.cs @@ -1,3 +1,5 @@ +using Cysharp.Text; + namespace AlicizaX { internal interface IServiceLifecycle @@ -15,7 +17,7 @@ namespace AlicizaX void IServiceLifecycle.Initialize(ServiceContext context) { 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; IsInitialized = true; diff --git a/Runtime/ABase/Service/Core/ServiceContractUtility.cs b/Runtime/ABase/Service/Core/ServiceContractUtility.cs index dbce33b..d7dcaee 100644 --- a/Runtime/ABase/Service/Core/ServiceContractUtility.cs +++ b/Runtime/ABase/Service/Core/ServiceContractUtility.cs @@ -1,78 +1,37 @@ using System; -using System.Collections.Generic; namespace AlicizaX { internal static class ServiceContractUtility { - private static readonly HashSet ExcludedContracts = new HashSet + 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), - typeof(IMonoService), - 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> _contractCache = new Dictionary>(); - - public static List Collect(Type serviceType, IReadOnlyList 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(contracts); // don't mutate the cached list - var unique = new HashSet(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; + _serviceType = serviceType; + _contractType = contractType; } - private static List BuildContracts(Type serviceType) - { - var contracts = new List { serviceType }; - var unique = new HashSet { serviceType }; + public int Count => _contractType == null || _contractType == _serviceType ? 1 : 2; - var interfaces = serviceType.GetInterfaces(); - for (var i = 0; i < interfaces.Length; i++) + public Type this[int index] + { + get { - var contract = interfaces[i]; - if (!typeof(IService).IsAssignableFrom(contract)) continue; - if (ExcludedContracts.Contains(contract)) continue; - if (unique.Add(contract)) contracts.Add(contract); + if (index == 0) return _serviceType; + if (index == 1 && Count == 2) return _contractType; + throw new IndexOutOfRangeException(); } - 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."); } } } diff --git a/Runtime/ABase/Service/Core/ServiceScope.cs b/Runtime/ABase/Service/Core/ServiceScope.cs index 9283c53..8f2267a 100644 --- a/Runtime/ABase/Service/Core/ServiceScope.cs +++ b/Runtime/ABase/Service/Core/ServiceScope.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using Cysharp.Text; namespace AlicizaX { internal sealed class ServiceScope : IDisposable, IServiceRegistry { - private readonly Dictionary _servicesByContract = new Dictionary(); - private readonly Dictionary> _contractsByService = new Dictionary>(ReferenceComparer.Instance); + private const int MissingIndex = -1; + + private readonly Dictionary _servicesByContract = new Dictionary(); + private readonly Dictionary _entriesByService = new Dictionary(ReferenceComparer.Instance); private readonly List _registrationOrder = new List(); private readonly List _tickables = new List(); @@ -14,101 +17,74 @@ namespace AlicizaX private readonly List _fixedTickables = new List(); private readonly List _gizmoDrawables = new List(); - private IServiceTickable[] _tickableSnapshot = Array.Empty(); - private IServiceLateTickable[] _lateTickableSnapshot = Array.Empty(); - private IServiceFixedTickable[] _fixedTickableSnapshot = Array.Empty(); - private IServiceGizmoDrawable[] _gizmoSnapshot = Array.Empty(); + private readonly List _pendingChanges = new List(); private bool _tickablesDirty; private bool _lateTickablesDirty; private bool _fixedTickablesDirty; 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)); + Kind = kind; Name = string.IsNullOrWhiteSpace(name) ? throw new ArgumentException("Scope name cannot be empty.", nameof(name)) : name; Order = order; + CreationIndex = creationIndex; } internal ServiceWorld World { get; } + internal ServiceScopeKind Kind { get; } + public string Name { get; } internal int Order { get; } + internal int CreationIndex { get; } + internal bool IsDisposed { get; private set; } - public T Register(T service, params Type[] extraContracts) - where T : class, IService - { - EnsureNotDisposed(); + public T RegisterSelf(T service) where T : class, IService + => RegisterInternal(service, ServiceContractUtility.Create(service != null ? service.GetType() : typeof(T))); - if (service == null) - throw new ArgumentNullException(nameof(service)); - - 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 TContract Register(IService service) + where TContract : class, IService + => RegisterInternal(service, ServiceContractUtility.Create(service != null ? service.GetType() : typeof(TContract), typeof(TContract))) as TContract; public bool Unregister() where T : class, IService { - if (!_servicesByContract.TryGetValue(typeof(T), out var service)) + if (!_servicesByContract.TryGetValue(typeof(T).TypeHandle, out var service)) return false; return Unregister(service); } public bool Unregister(IService service) { - if (service == null || !_contractsByService.ContainsKey(service)) + if (service == null || !_entriesByService.TryGetValue(service, out var entry)) return false; 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); - RemoveBindings(service); + if (_isIterating) + { + if (entry.PendingRemove) return true; + entry.PendingRemove = true; + _entriesByService[service] = entry; + _pendingChanges.Add(PendingChange.Remove(service)); + return true; + } + + RemoveEntry(service, entry, true); lifecycle.Destroy(); return true; } public bool TryGet(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; return service != null; @@ -118,60 +94,180 @@ namespace AlicizaX return false; } + internal bool TryGet(Type contract, out IService service) + => _servicesByContract.TryGetValue(contract.TypeHandle, out service); + public T Require() where T : class, IService { if (TryGet(out T service)) return service; - Log.Error($"Scope {Name} does not contain service {typeof(T).FullName}."); - return default; + throw new InvalidOperationException(ZString.Format("Scope {0} does not contain service {1}.", Name, typeof(T).FullName)); } public bool HasContract(Type contractType) - => _servicesByContract.ContainsKey(contractType); + => _servicesByContract.ContainsKey(contractType.TypeHandle); internal void Tick(float deltaTime) { - var snapshot = GetTickSnapshot(); - for (var i = 0; i < snapshot.Length; i++) snapshot[i].Tick(deltaTime); + SortTickablesIfDirty(); + _isIterating = true; + for (var i = 0; i < _tickables.Count; i++) _tickables[i].Tick(deltaTime); + _isIterating = false; + FlushPendingChanges(); } internal void LateTick(float deltaTime) { - var snapshot = GetLateTickSnapshot(); - for (var i = 0; i < snapshot.Length; i++) snapshot[i].LateTick(deltaTime); + SortLateTickablesIfDirty(); + _isIterating = true; + for (var i = 0; i < _lateTickables.Count; i++) _lateTickables[i].LateTick(deltaTime); + _isIterating = false; + FlushPendingChanges(); } internal void FixedTick(float fixedDeltaTime) { - var snapshot = GetFixedTickSnapshot(); - for (var i = 0; i < snapshot.Length; i++) snapshot[i].FixedTick(fixedDeltaTime); + SortFixedTickablesIfDirty(); + _isIterating = true; + for (var i = 0; i < _fixedTickables.Count; i++) _fixedTickables[i].FixedTick(fixedDeltaTime); + _isIterating = false; + FlushPendingChanges(); } internal void DrawGizmos() { - var snapshot = GetGizmoSnapshot(); - for (var i = 0; i < snapshot.Length; i++) snapshot[i].DrawGizmos(); + SortGizmoDrawablesIfDirty(); + _isIterating = true; + for (var i = 0; i < _gizmoDrawables.Count; i++) _gizmoDrawables[i].DrawGizmos(); + _isIterating = false; + FlushPendingChanges(); } public void Dispose() { if (IsDisposed) return; - var snapshot = _registrationOrder.ToArray(); - for (var i = snapshot.Length - 1; i >= 0; i--) + _isIterating = false; + _pendingChanges.Clear(); + + for (var i = _registrationOrder.Count - 1; i >= 0; i--) { - var service = snapshot[i]; - if (!_contractsByService.ContainsKey(service)) continue; + var service = _registrationOrder[i]; + if (service == null || !_entriesByService.TryGetValue(service, out var entry)) continue; if (service is not IServiceLifecycle lifecycle) - throw new InvalidOperationException($"Service {service.GetType().FullName} must implement {nameof(IServiceLifecycle)}."); - RemoveFromLifecycleLists(service); - RemoveContractBindings(service); // skip _registrationOrder.Remove — we clear below + throw new InvalidOperationException(ZString.Format("Service {0} must implement {1}.", service.GetType().FullName, nameof(IServiceLifecycle))); + + RemoveEntry(service, entry, false); lifecycle.Destroy(); } _registrationOrder.Clear(); + _tickables.Clear(); + _lateTickables.Clear(); + _fixedTickables.Clear(); + _gizmoDrawables.Clear(); IsDisposed = true; } + private T RegisterInternal(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() { if (IsDisposed) throw new ObjectDisposedException(Name); @@ -186,115 +282,202 @@ namespace AlicizaX service is IServiceGizmoDrawable)) { 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) { + entry.TickIndex = _tickables.Count; _tickables.Add(tickable); _tickablesDirty = true; } if (service is IServiceLateTickable late) { + entry.LateTickIndex = _lateTickables.Count; _lateTickables.Add(late); _lateTickablesDirty = true; } if (service is IServiceFixedTickable fixed_) { + entry.FixedTickIndex = _fixedTickables.Count; _fixedTickables.Add(fixed_); _fixedTickablesDirty = true; } if (service is IServiceGizmoDrawable gizmo) { + entry.GizmoIndex = _gizmoDrawables.Count; _gizmoDrawables.Add(gizmo); _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 (service is IServiceLateTickable late && _lateTickables.Remove(late)) _lateTickablesDirty = true; - if (service is IServiceFixedTickable fixed_ && _fixedTickables.Remove(fixed_)) _fixedTickablesDirty = true; - if (service is IServiceGizmoDrawable gizmo && _gizmoDrawables.Remove(gizmo)) _gizmoDrawablesDirty = true; + if (entry.TickIndex != MissingIndex) RemoveTickableAt(entry.TickIndex); + if (entry.LateTickIndex != MissingIndex) RemoveLateTickableAt(entry.LateTickIndex); + if (entry.FixedTickIndex != MissingIndex) RemoveFixedTickableAt(entry.FixedTickIndex); + 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++) - _servicesByContract.Remove(contracts[i]); + var change = _pendingChanges[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); - _registrationOrder.Remove(service); + _pendingChanges.Clear(); } - // Used during full Dispose — skips the O(n) _registrationOrder.Remove since we clear the list afterwards. - 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() + private void SortTickablesIfDirty() { if (_tickablesDirty) { _tickables.Sort(CompareByOrder); - _tickableSnapshot = _tickables.Count > 0 ? _tickables.ToArray() : Array.Empty(); + RebuildTickIndices(); _tickablesDirty = false; } - - return _tickableSnapshot; } - private IServiceLateTickable[] GetLateTickSnapshot() + private void SortLateTickablesIfDirty() { if (_lateTickablesDirty) { _lateTickables.Sort(CompareByOrder); - _lateTickableSnapshot = _lateTickables.Count > 0 ? _lateTickables.ToArray() : Array.Empty(); + RebuildLateTickIndices(); _lateTickablesDirty = false; } - - return _lateTickableSnapshot; } - private IServiceFixedTickable[] GetFixedTickSnapshot() + private void SortFixedTickablesIfDirty() { if (_fixedTickablesDirty) { _fixedTickables.Sort(CompareByOrder); - _fixedTickableSnapshot = _fixedTickables.Count > 0 ? _fixedTickables.ToArray() : Array.Empty(); + RebuildFixedTickIndices(); _fixedTickablesDirty = false; } - - return _fixedTickableSnapshot; } - private IServiceGizmoDrawable[] GetGizmoSnapshot() + private void SortGizmoDrawablesIfDirty() { if (_gizmoDrawablesDirty) { _gizmoDrawables.Sort(CompareByOrder); - _gizmoSnapshot = _gizmoDrawables.Count > 0 ? _gizmoDrawables.ToArray() : Array.Empty(); + RebuildGizmoIndices(); _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 a, T b) @@ -303,5 +486,47 @@ namespace AlicizaX var right = b is IServiceOrder ob ? ob.Order : 0; 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); + } } } diff --git a/Runtime/ABase/Service/Core/ServiceWorld.cs b/Runtime/ABase/Service/Core/ServiceWorld.cs index 68c017f..a816ca0 100644 --- a/Runtime/ABase/Service/Core/ServiceWorld.cs +++ b/Runtime/ABase/Service/Core/ServiceWorld.cs @@ -1,28 +1,31 @@ using System; using System.Collections.Generic; +using Cysharp.Text; namespace AlicizaX { internal sealed class ServiceWorld : IDisposable { - private readonly List _scopes = new List(); - private readonly Dictionary _scopesByType = new Dictionary(); + private const int ScopeSlotCount = 3; - private ServiceScope[] _scopeSnapshot = Array.Empty(); + private readonly ServiceScope[] _scopesByKind = new ServiceScope[ScopeSlotCount]; + private readonly ServiceScope[] _activeScopes = new ServiceScope[ScopeSlotCount]; + private readonly Dictionary _servicesByContract = new Dictionary(); + + private int _activeScopeCount; + private int _nextScopeCreationIndex; private bool _scopesDirty; - private ServiceScope _sceneScope; - private ServiceScope _gameplayScope; 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 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 { @@ -30,7 +33,7 @@ namespace AlicizaX { if (!HasScene) 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) throw new InvalidOperationException("Gameplay scope has not been created yet."); - return _gameplayScope; + return _scopesByKind[(int)ServiceScopeKind.Gameplay]; } } internal ServiceScope EnsureScene(int order = ServiceDomainOrder.Scene) - => _sceneScope is { IsDisposed: false } - ? _sceneScope - : _sceneScope = CreateScopeInternal(typeof(SceneScope), nameof(SceneScope), order); + => IsAlive(ServiceScopeKind.Scene) + ? _scopesByKind[(int)ServiceScopeKind.Scene] + : CreateScopeInternal(ServiceScopeKind.Scene, nameof(SceneScope), order); internal bool TryGetScene(out ServiceScope scope) { - scope = HasScene ? _sceneScope : null; + scope = IsAlive(ServiceScopeKind.Scene) ? _scopesByKind[(int)ServiceScopeKind.Scene] : null; return scope != null; } @@ -62,35 +65,21 @@ namespace AlicizaX } internal bool DestroyScene() - { - if (!HasScene) - return false; - - var scope = _sceneScope; - _sceneScope = null; - return DestroyScope(scope, typeof(SceneScope)); - } + => DestroyScope(ServiceScopeKind.Scene); internal ServiceScope EnsureGameplay(int order = ServiceDomainOrder.Gameplay) - => _gameplayScope is { IsDisposed: false } - ? _gameplayScope - : _gameplayScope = CreateScopeInternal(typeof(GameplayScope), nameof(GameplayScope), order); + => IsAlive(ServiceScopeKind.Gameplay) + ? _scopesByKind[(int)ServiceScopeKind.Gameplay] + : CreateScopeInternal(ServiceScopeKind.Gameplay, nameof(GameplayScope), order); internal bool TryGetGameplay(out ServiceScope scope) { - scope = HasGameplay ? _gameplayScope : null; + scope = IsAlive(ServiceScopeKind.Gameplay) ? _scopesByKind[(int)ServiceScopeKind.Gameplay] : null; return scope != null; } internal bool DestroyGameplay() - { - if (!HasGameplay) - return false; - - var scope = _gameplayScope; - _gameplayScope = null; - return DestroyScope(scope, typeof(GameplayScope)); - } + => DestroyScope(ServiceScopeKind.Gameplay); internal bool TryGet(out T service) where T : class, IService => TryGet(null, out service); @@ -100,12 +89,10 @@ namespace AlicizaX if (preferredScope != null && !preferredScope.IsDisposed && preferredScope.TryGet(out service)) return true; - var snapshot = GetScopeSnapshot(); - for (var i = snapshot.Length - 1; i >= 0; i--) + if (_servicesByContract.TryGetValue(typeof(T).TypeHandle, out var bindings) && bindings.TryGetBest(out var raw)) { - var scope = snapshot[i]; - if (ReferenceEquals(scope, preferredScope)) continue; - if (scope.TryGet(out service)) return true; + service = raw as T; + return service != null; } service = null; @@ -117,82 +104,209 @@ namespace AlicizaX internal T Require(ServiceScope preferredScope) where T : class, IService { 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) { - var snapshot = GetScopeSnapshot(); - for (var i = 0; i < snapshot.Length; i++) snapshot[i].Tick(deltaTime); + SortScopesIfDirty(); + for (var i = 0; i < _activeScopeCount; i++) _activeScopes[i].Tick(deltaTime); } internal void LateTick(float deltaTime) { - var snapshot = GetScopeSnapshot(); - for (var i = 0; i < snapshot.Length; i++) snapshot[i].LateTick(deltaTime); + SortScopesIfDirty(); + for (var i = 0; i < _activeScopeCount; i++) _activeScopes[i].LateTick(deltaTime); } internal void FixedTick(float fixedDeltaTime) { - var snapshot = GetScopeSnapshot(); - for (var i = 0; i < snapshot.Length; i++) snapshot[i].FixedTick(fixedDeltaTime); + SortScopesIfDirty(); + for (var i = 0; i < _activeScopeCount; i++) _activeScopes[i].FixedTick(fixedDeltaTime); } internal void DrawGizmos() { - var snapshot = GetScopeSnapshot(); - for (var i = 0; i < snapshot.Length; i++) snapshot[i].DrawGizmos(); + SortScopesIfDirty(); + for (var i = 0; i < _activeScopeCount; i++) _activeScopes[i].DrawGizmos(); } public void Dispose() { - var snapshot = _scopes.ToArray(); - for (var i = snapshot.Length - 1; i >= 0; i--) - snapshot[i].Dispose(); + for (var i = _activeScopeCount - 1; i >= 0; i--) + _activeScopes[i].Dispose(); - _sceneScope = null; - _gameplayScope = null; - _scopes.Clear(); - _scopesByType.Clear(); - _scopeSnapshot = Array.Empty(); + for (var i = 0; i < ScopeSlotCount; i++) + _scopesByKind[i] = null; + + _activeScopeCount = 0; + _servicesByContract.Clear(); } - private bool DestroyScope(ServiceScope scope, Type scopeType) + internal void AddContract(ServiceScope scope, Type contract, IService service) { - if (scope == null) - return false; + var contractHandle = contract.TypeHandle; + if (!_servicesByContract.TryGetValue(contractHandle, out var bindings)) + { + bindings = default; + _servicesByContract.Add(contractHandle, bindings); + } - _scopesByType.Remove(scopeType); - _scopes.Remove(scope); - _scopesDirty = true; + bindings.Set(scope.Kind, scope, service); + _servicesByContract[contractHandle] = bindings; + } + + 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(); return true; } - private ServiceScope CreateScopeInternal(Type scopeType, string scopeName, int order) + private ServiceScope CreateScopeInternal(ServiceScopeKind kind, string scopeName, int order) { - if (_scopesByType.ContainsKey(scopeType)) - throw new InvalidOperationException($"Scope {scopeType.Name} already exists."); + if (IsAlive(kind)) + throw new InvalidOperationException(ZString.Format("Scope {0} already exists.", scopeName)); - var scope = new ServiceScope(this, scopeName, order); - _scopes.Add(scope); - _scopesByType.Add(scopeType, scope); - _scopes.Sort(CompareScopeOrder); + var scope = new ServiceScope(this, kind, scopeName, order, _nextScopeCreationIndex++); + _scopesByKind[(int)kind] = scope; + _activeScopes[_activeScopeCount++] = scope; _scopesDirty = true; 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(); - _scopesDirty = false; + if (!ReferenceEquals(_activeScopes[i], scope)) continue; + + _activeScopeCount--; + _activeScopes[i] = _activeScopes[_activeScopeCount]; + _activeScopes[_activeScopeCount] = null; + _scopesDirty = true; + return; } - return _scopeSnapshot; } - private static int CompareScopeOrder(ServiceScope left, ServiceScope right) - => left.Order.CompareTo(right.Order); + private void SortScopesIfDirty() + { + 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; + } + } } } diff --git a/Runtime/ABase/Service/Unity/AppServiceRoot.cs b/Runtime/ABase/Service/Unity/AppServiceRoot.cs index 53fec15..fd3d16a 100644 --- a/Runtime/ABase/Service/Unity/AppServiceRoot.cs +++ b/Runtime/ABase/Service/Unity/AppServiceRoot.cs @@ -1,11 +1,10 @@ -using Cysharp.Threading.Tasks; using UnityEngine; namespace AlicizaX { [DefaultExecutionOrder(-32000)] [DisallowMultipleComponent] - public class AppServiceRoot : MonoBehaviour + public abstract class AppServiceRoot : MonoBehaviour { private static AppServiceRoot s_activeRoot; @@ -55,14 +54,13 @@ namespace AlicizaX if (AppServices.HasWorld) AppServices.RequireWorld().DrawGizmos(); } - protected virtual async void OnDestroy() + protected virtual void OnDestroy() { if (s_activeRoot == this) s_activeRoot = null; if (_ownsWorld && AppServices.HasWorld) { - await UniTask.Yield(); AppServices.Shutdown(); } diff --git a/Runtime/ABase/Service/Unity/MonoServiceBehaviour.cs b/Runtime/ABase/Service/Unity/MonoServiceBehaviour.cs index 79c09f6..b44ab02 100644 --- a/Runtime/ABase/Service/Unity/MonoServiceBehaviour.cs +++ b/Runtime/ABase/Service/Unity/MonoServiceBehaviour.cs @@ -1,3 +1,4 @@ +using Cysharp.Text; using UnityEngine; namespace AlicizaX @@ -11,7 +12,7 @@ namespace AlicizaX void IServiceLifecycle.Initialize(ServiceContext context) { 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; IsInitialized = true; @@ -55,7 +56,7 @@ namespace AlicizaX if (_dontDestroyOnLoad) DontDestroyOnLoad(gameObject); - scope.Register(this); + scope.RegisterSelf(this); } private void OnDestroy() @@ -69,36 +70,41 @@ namespace AlicizaX private static ServiceScope ResolveOrCreateScope() { - if (typeof(TScope) == typeof(AppScope)) - return AppServices.RequireWorld().App; - - if (typeof(TScope) == typeof(SceneScope)) - return AppServices.EnsureScene(); - - if (typeof(TScope) == typeof(GameplayScope)) - return AppServices.EnsureGameplay(); - - throw new System.InvalidOperationException($"Unsupported service scope: {typeof(TScope).FullName}."); + var kind = ScopeKindCache.Kind; + if (kind == ServiceScopeKind.App) return AppServices.RequireWorld().App; + if (kind == ServiceScopeKind.Scene) return AppServices.EnsureScene(); + return AppServices.EnsureGameplay(); } private static bool TryResolveScope(out ServiceScope scope) { - if (typeof(TScope) == typeof(AppScope)) + var kind = ScopeKindCache.Kind; + if (kind == ServiceScopeKind.App) { scope = AppServices.RequireWorld().App; return true; } - if (typeof(TScope) == typeof(SceneScope)) + if (kind == ServiceScopeKind.Scene) return AppServices.TryGetScene(out scope); - if (typeof(TScope) == typeof(GameplayScope)) - return AppServices.TryGetGameplay(out scope); - - scope = null; - return false; + return AppServices.TryGetGameplay(out scope); } protected virtual void OnAwake() { } } + + internal static class ScopeKindCache + 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)); + } + } } diff --git a/Runtime/ABase/Utility/Utility.Unity.cs b/Runtime/ABase/Utility/Utility.Unity.cs index c1fcc27..aa823b9 100644 --- a/Runtime/ABase/Utility/Utility.Unity.cs +++ b/Runtime/ABase/Utility/Utility.Unity.cs @@ -286,7 +286,7 @@ namespace AlicizaX return; } - _loopService = AppServices.RegisterApp(new UtilityLoopService()); + _loopService = AppServices.RegisterAppSelf(new UtilityLoopService()); } private static UtilityLoopService EnsureLoopService() diff --git a/Runtime/Audio/AudioComponent.cs b/Runtime/Audio/AudioComponent.cs index 2930b95..74fdb9a 100644 --- a/Runtime/Audio/AudioComponent.cs +++ b/Runtime/Audio/AudioComponent.cs @@ -16,7 +16,7 @@ namespace AlicizaX.Audio.Runtime private void Awake() { - _audioService = AppServices.RegisterApp(new AudioService()); + _audioService = AppServices.RegisterApp(new AudioService()); } private void Start() diff --git a/Runtime/Debugger/DebuggerComponent.TimerInformationWindow.cs b/Runtime/Debugger/DebuggerComponent.TimerInformationWindow.cs index 5fdd115..e12645a 100644 --- a/Runtime/Debugger/DebuggerComponent.TimerInformationWindow.cs +++ b/Runtime/Debugger/DebuggerComponent.TimerInformationWindow.cs @@ -8,83 +8,89 @@ namespace AlicizaX.Debugger.Runtime { 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 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 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 { public VisualElement Root; + public VisualElement Fill; public VisualElement LoopIndicator; public VisualElement ScaleIndicator; public VisualElement StateIndicator; - public VisualElement Fill; } - private ITimerService _mTimerDebug; - private readonly TimerDebugInfo[] m_TimerInfos = new TimerDebugInfo[MAX_DISPLAY_COUNT]; - private readonly RowView[] m_TimerRows = new RowView[MAX_DISPLAY_COUNT]; - private VisualElement m_ActiveUsageFill; - private VisualElement m_PeakUsageFill; - private VisualElement m_FreeUsageFill; - private VisualElement m_OverflowNote; - private VisualElement m_EmptyNote; - private float m_RefreshCountdown; + private ITimerService _timerService; + private ITimerDebugService _timerDebugService; + private readonly TimerDebugInfo[] _timerInfos = new TimerDebugInfo[MAX_DISPLAY_COUNT]; + private readonly RowView[] _timerRows = new RowView[MAX_DISPLAY_COUNT]; + private UsageView _activeUsage; + private UsageView _peakUsage; + private UsageView _freeUsage; + private VisualElement _overflowNote; + private VisualElement _emptyNote; + private float _refreshCountdown; + private int _visibleRowCount; public override void Initialize(params object[] args) { - _mTimerDebug = AppServices.Require(); + if (AppServices.TryGet(out _timerService)) + { + _timerDebugService = _timerService as ITimerDebugService; + } } public override void OnEnter() { - m_RefreshCountdown = 0f; + _refreshCountdown = 0f; RefreshContent(); } public override void OnUpdate(float elapseSeconds, float realElapseSeconds) { - m_RefreshCountdown -= realElapseSeconds; - if (m_RefreshCountdown > 0f) + _refreshCountdown -= realElapseSeconds; + if (_refreshCountdown > 0f) { return; } - m_RefreshCountdown = REFRESH_INTERVAL; + _refreshCountdown = REFRESH_INTERVAL; RefreshContent(); } protected override void BuildWindow(VisualElement root) { - if (_mTimerDebug == null) - { - return; - } - root.Add(CreateActionButton("Refresh", RefreshContent, DebuggerTheme.ButtonSurfaceActive, DebuggerTheme.PrimaryText)); - VisualElement overview = CreateSection("Timer Pool Overview", out VisualElement overviewCard); - overviewCard.Add(CreateUsageRow("Active Usage", DebuggerTheme.Accent, out m_ActiveUsageFill)); - overviewCard.Add(CreateUsageRow("Peak Usage", DebuggerTheme.Warning, out m_PeakUsageFill)); - overviewCard.Add(CreateUsageRow("Free Capacity", DebuggerTheme.Positive, out m_FreeUsageFill)); + VisualElement overview = CreateSection("Timer Pool", out VisualElement overviewCard); + overviewCard.Add(CreateUsageRow("Active", DebuggerTheme.Accent, out _activeUsage)); + overviewCard.Add(CreateUsageRow("Peak", DebuggerTheme.Warning, out _peakUsage)); + overviewCard.Add(CreateUsageRow("Free", DebuggerTheme.Positive, out _freeUsage)); root.Add(overview); VisualElement sample = CreateSection("Timer Sample", out VisualElement sampleCard); sampleCard.Add(CreateNoteLabel(SAMPLE_NOTE, DebuggerTheme.SecondaryText)); - m_OverflowNote = CreateNoteLabel(OVERFLOW_NOTE, new Color(1f, 0.5f, 0f)); - m_OverflowNote.style.display = DisplayStyle.None; - sampleCard.Add(m_OverflowNote); + _overflowNote = CreateNoteLabel(OVERFLOW_NOTE, DebuggerTheme.Warning); + _overflowNote.style.display = DisplayStyle.None; + sampleCard.Add(_overflowNote); - m_EmptyNote = CreateNoteLabel(EMPTY_NOTE, DebuggerTheme.SecondaryText); - m_EmptyNote.style.display = DisplayStyle.None; - sampleCard.Add(m_EmptyNote); + _emptyNote = CreateNoteLabel(EMPTY_NOTE, DebuggerTheme.SecondaryText); + _emptyNote.style.display = DisplayStyle.None; + sampleCard.Add(_emptyNote); + _visibleRowCount = 0; for (int i = 0; i < MAX_DISPLAY_COUNT; i++) { - m_TimerRows[i] = CreateTimerRow(sampleCard); - m_TimerRows[i].Root.style.display = DisplayStyle.None; + _timerRows[i] = CreateTimerRow(sampleCard); + _timerRows[i].Root.style.display = DisplayStyle.None; } root.Add(sample); @@ -93,50 +99,76 @@ namespace AlicizaX.Debugger.Runtime private void RefreshContent() { - if (_mTimerDebug == null || m_ActiveUsageFill == null) + if (_emptyNote == null || _overflowNote == null) { 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; - SetFillRatio(m_ActiveUsageFill, activeCount / capacity); - SetFillRatio(m_PeakUsageFill, peakActiveCount / capacity); - SetFillRatio(m_FreeUsageFill, freeCount / capacity); + SetFillRatio(_activeUsage.Fill, activeCount / capacity); + SetFillRatio(_peakUsage.Fill, peakActiveCount / capacity); + SetFillRatio(_freeUsage.Fill, freeCount / capacity); if (activeCount <= 0) { - m_EmptyNote.style.display = DisplayStyle.Flex; - m_OverflowNote.style.display = DisplayStyle.None; - SetTimerRowsVisible(0); + _emptyNote.style.display = DisplayStyle.Flex; + _overflowNote.style.display = DisplayStyle.None; + SetVisibleRows(0); return; } - m_EmptyNote.style.display = 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; + _emptyNote.style.display = DisplayStyle.None; + _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++) { - 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(); row.style.flexDirection = FlexDirection.Column; row.style.marginBottom = 8f * scale; @@ -148,80 +180,72 @@ namespace AlicizaX.Debugger.Runtime titleLabel.style.marginBottom = 4f * scale; row.Add(titleLabel); - VisualElement track = new VisualElement(); - 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; - - fill = new VisualElement(); + VisualElement track = CreateTrack(14f * scale); + VisualElement fill = new VisualElement(); fill.style.height = Length.Percent(100f); fill.style.width = Length.Percent(0f); fill.style.backgroundColor = fillColor; track.Add(fill); row.Add(track); + usageView.Fill = fill; 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) { - float scale = DebuggerComponent.Instance != null ? DebuggerComponent.Instance.GetUiScale() : 1f; + float scale = GetScale(); VisualElement row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; row.style.alignItems = Align.Center; row.style.height = 20f * 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; view.Root = row; - view.LoopIndicator = loopIndicator; - view.ScaleIndicator = scaleIndicator; - view.StateIndicator = stateIndicator; - view.Fill = fill; + view.LoopIndicator = CreateIndicator(6f * scale, DebuggerTheme.Accent); + view.ScaleIndicator = CreateIndicator(6f * scale, DebuggerTheme.Warning); + view.StateIndicator = CreateIndicator(6f * scale, DebuggerTheme.Positive); + 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; } + 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) { VisualElement indicator = new VisualElement(); @@ -239,31 +263,24 @@ namespace AlicizaX.Debugger.Runtime bool isRunning = (flags & TimerDebugFlags.Running) != 0; bool isLoop = (flags & TimerDebugFlags.Loop) != 0; bool isUnscaled = (flags & TimerDebugFlags.Unscaled) != 0; - float duration = info.Duration; - float ratio = duration > 0f ? info.LeftTime / duration : 0f; - if (ratio < 0f) - { - ratio = 0f; - } - else if (ratio > 1f) - { - ratio = 1f; - } + float ratio = info.Duration > 0f ? info.LeftTime / info.Duration : 0f; - row.Root.style.display = DisplayStyle.Flex; row.LoopIndicator.style.opacity = isLoop ? 1f : 0.2f; row.ScaleIndicator.style.opacity = isUnscaled ? 1f : 0.35f; row.ScaleIndicator.style.backgroundColor = isUnscaled ? DebuggerTheme.Warning : DebuggerTheme.Accent; row.StateIndicator.style.opacity = isRunning ? 1f : 0.45f; 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 ? ratio <= 0.2f ? DebuggerTheme.Warning : DebuggerTheme.Positive : DebuggerTheme.SecondaryText; + SetFillRatio(row.Fill, ratio); } private static void SetFillRatio(VisualElement fill, float ratio) { + if (fill == null) + { + return; + } + if (ratio < 0f) { ratio = 0f; @@ -275,6 +292,11 @@ namespace AlicizaX.Debugger.Runtime fill.style.width = Length.Percent(ratio * 100f); } + + private static float GetScale() + { + return Instance != null ? Instance.GetUiScale() : 1f; + } } } } diff --git a/Runtime/Debugger/DebuggerComponent.cs b/Runtime/Debugger/DebuggerComponent.cs index f05f460..771bcd1 100644 --- a/Runtime/Debugger/DebuggerComponent.cs +++ b/Runtime/Debugger/DebuggerComponent.cs @@ -263,7 +263,7 @@ namespace AlicizaX.Debugger.Runtime if (!AppServices.TryGetApp(out _mDebuggerService)) { - _mDebuggerService = AppServices.RegisterApp(new DebuggerService()); + _mDebuggerService = AppServices.RegisterApp(new DebuggerService()); } if (_mDebuggerService == null) diff --git a/Runtime/Localization/LocalizationComponent.cs b/Runtime/Localization/LocalizationComponent.cs index b225cec..46a2ca8 100644 --- a/Runtime/Localization/LocalizationComponent.cs +++ b/Runtime/Localization/LocalizationComponent.cs @@ -55,7 +55,7 @@ namespace AlicizaX.Localization.Runtime { if (!AppServices.TryGetApp(out _mLocalizationService)) { - _mLocalizationService = AppServices.RegisterApp(new LocalizationService()); + _mLocalizationService = AppServices.RegisterApp(new LocalizationService()); } if (_mLocalizationService == null) diff --git a/Runtime/MemoryPool/Benchmark/MemoryPoolBenchmark.cs b/Runtime/MemoryPool/Benchmark/MemoryPoolBenchmark.cs index 86ab6f9..af930ae 100644 --- a/Runtime/MemoryPool/Benchmark/MemoryPoolBenchmark.cs +++ b/Runtime/MemoryPool/Benchmark/MemoryPoolBenchmark.cs @@ -615,13 +615,11 @@ namespace AlicizaX { MemoryPool.ClearAll(); MemoryPool.Prewarm(objectCount); - Type type = typeof(BenchmarkMemory); - RestartCaseMeasure(); for (int i = 0; i < loopCount; i++) { - IMemory item = MemoryPool.Acquire(type); - MemoryPool.Release(item); + BenchmarkMemory item = MemoryPool.Acquire(); + MemoryPool.Release(item); } StopCaseMeasure(); diff --git a/Runtime/ObjectPool/ObjectPoolComponent.cs b/Runtime/ObjectPool/ObjectPoolComponent.cs index cb91493..1689733 100644 --- a/Runtime/ObjectPool/ObjectPoolComponent.cs +++ b/Runtime/ObjectPool/ObjectPoolComponent.cs @@ -14,7 +14,7 @@ namespace AlicizaX private void Awake() { - _mObjectPoolService = AppServices.RegisterApp(new ObjectPoolService()); + _mObjectPoolService = AppServices.RegisterApp(new ObjectPoolService()); Application.lowMemory += OnLowMemory; } diff --git a/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs b/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs index fcfb6c6..5ac8c15 100644 --- a/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs +++ b/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs @@ -32,16 +32,25 @@ namespace AlicizaX.ObjectPool } } - private ObjectSlot[] m_Slots; - private int m_SlotCount; - private int[] m_FreeStack; - private int m_FreeTop; + private ObjectSlot[][] m_Pages; + private int[][] m_PageFreeStacks; + private int[] m_PageAliveCounts; + 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 StringOpenHashMap m_AllNameHeadMap; private StringOpenHashMap m_NameCursorMap; private readonly bool m_AllowMultiSpawn; + private readonly MemoryPoolHandle m_ObjectMemoryPoolHandle; private float m_AutoReleaseInterval; private int m_Capacity; private float m_ExpireTime; @@ -59,18 +68,37 @@ namespace AlicizaX.ObjectPool private const int DefaultReleasePerFrame = 8; 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, float autoReleaseInterval, int capacity, float expireTime, int priority) : base(name) { int initCap = Math.Min(Math.Max(capacity, 1), InitSlotCapacity); - m_Slots = SlotArrayPool.Rent(initCap); - m_FreeStack = SlotArrayPool.Rent(initCap); + m_Pages = SlotArrayPool.Rent(InitPageCapacity); + m_PageFreeStacks = SlotArrayPool.Rent(InitPageCapacity); + m_PageAliveCounts = SlotArrayPool.Rent(InitPageCapacity); + m_PageFreeTops = SlotArrayPool.Rent(InitPageCapacity); + m_PageFlags = SlotArrayPool.Rent(InitPageCapacity); + m_FreePageStack = SlotArrayPool.Rent(InitPageCapacity); + m_EmptyPageStack = SlotArrayPool.Rent(InitPageCapacity); + m_ReleasedPageStack = SlotArrayPool.Rent(InitPageCapacity); m_TargetMap = new ReferenceOpenHashMap(initCap); m_AllNameHeadMap = new StringOpenHashMap(initCap); m_NameCursorMap = new StringOpenHashMap(initCap); + m_PageCount = 0; + m_FreePageTop = 0; + m_EmptyPageTop = 0; + m_ReleasedPageTop = 0; m_AllowMultiSpawn = allowMultiSpawn; + m_ObjectMemoryPoolHandle = MemoryPool.GetHandle(typeof(T)); m_AutoReleaseInterval = autoReleaseInterval; m_Capacity = capacity; m_ExpireTime = expireTime; @@ -155,7 +183,7 @@ namespace AlicizaX.ObjectPool if (obj == 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 UnityEngine.Debug.LogError($"Target '{obj.Target.GetType().FullName}' is already registered in pool '{FullName}'."); @@ -175,7 +203,7 @@ namespace AlicizaX.ObjectPool if (idx < 0) return; - ref var slot = ref m_Slots[idx]; + ref var slot = ref GetSlotRef(idx); slot.Obj = obj; slot.SpawnCount = spawned ? 1 : 0; slot.LastUseTime = Time.realtimeSinceStartup; @@ -190,7 +218,7 @@ namespace AlicizaX.ObjectPool string objectName = obj.Name ?? string.Empty; if (m_AllNameHeadMap.TryGetValue(objectName, out int existingHead)) { - m_Slots[existingHead].PrevByName = idx; + GetSlotRef(existingHead).PrevByName = idx; slot.NextByName = existingHead; } else @@ -221,7 +249,7 @@ namespace AlicizaX.ObjectPool int head = FindAvailableByName(name); 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 UNITY_EDITOR @@ -253,7 +281,7 @@ namespace AlicizaX.ObjectPool if (!m_AllNameHeadMap.TryGetValue(name, out int head)) return -1; - ref var headSlot = ref m_Slots[head]; + ref var headSlot = ref GetSlotRef(head); if (headSlot.IsAlive() && headSlot.SpawnCount == 0 && string.Equals(headSlot.Obj.Name, name, StringComparison.Ordinal)) @@ -269,7 +297,7 @@ namespace AlicizaX.ObjectPool do { - ref var slot = ref m_Slots[current]; + ref var slot = ref GetSlotRef(current); if (slot.IsAlive() && slot.SpawnCount == 0 && string.Equals(slot.Obj.Name, name, StringComparison.Ordinal)) @@ -288,11 +316,9 @@ namespace AlicizaX.ObjectPool private int GetValidNameCursor(string name, int head) { - if (m_NameCursorMap.TryGetValue(name, out int cursor) - && cursor >= 0 - && cursor < m_SlotCount) + if (m_NameCursorMap.TryGetValue(name, out int cursor) && IsValidIndex(cursor)) { - ref var slot = ref m_Slots[cursor]; + ref var slot = ref GetSlotRef(cursor); if (slot.IsAlive() && string.Equals(slot.Obj.Name, name, StringComparison.Ordinal)) return cursor; } @@ -339,8 +365,8 @@ namespace AlicizaX.ObjectPool int current = m_UnusedHead; while (current >= 0) { - int next = m_Slots[current].NextUnused; - ref var slot = ref m_Slots[current]; + int next = GetSlotRef(current).NextUnused; + ref var slot = ref GetSlotRef(current); if (CanReleaseSlot(ref slot)) { ReleaseSlot(current, false); @@ -352,8 +378,7 @@ namespace AlicizaX.ObjectPool if (released > 0) { m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released); - TrimSlotCountTail(); - ShrinkStorageIfEmpty(); + ReleaseEmptyPages(EmptyPageReleaseBudget); UpdateActiveState(); ValidateState(); } @@ -400,66 +425,39 @@ namespace AlicizaX.ObjectPool return; m_ShrinkCounter = 0; - - 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.Rent(targetCapacity); - var newFreeStack = SlotArrayPool.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.Return(m_Slots, true); - SlotArrayPool.Return(m_FreeStack, true); - - m_Slots = newSlots; - m_FreeStack = newFreeStack; - m_FreeTop = newFreeTop; - } - } + ReleaseEmptyPages(EmptyPageReleaseBudget); } internal override void Shutdown() { 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]; - if (!slot.IsAlive()) continue; - slot.Obj.Release(true); - MemoryPool.Release(slot.Obj); - slot.Obj = null; - slot.SetAlive(false); + ObjectSlot[] slots = m_Pages[page]; + if (slots == null) continue; + + for (int offset = 0; offset < PageSize; offset++) + { + 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_AllNameHeadMap.Clear(); m_NameCursorMap.Clear(); - SlotArrayPool.Return(m_Slots, true); - SlotArrayPool.Return(m_FreeStack, true); - m_Slots = null; - m_FreeStack = null; + ReleaseAllPages(); + ReturnPageStorage(); - m_SlotCount = 0; - m_FreeTop = 0; + m_PageCount = 0; + m_FreePageTop = 0; + m_EmptyPageTop = 0; + m_ReleasedPageTop = 0; m_PendingReleaseCount = 0; m_UnusedHead = -1; m_UnusedTail = -1; @@ -480,19 +478,25 @@ namespace AlicizaX.ObjectPool int write = 0; 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]; - if (!slot.IsAlive()) continue; + ObjectSlot[] slots = m_Pages[page]; + if (slots == null) continue; - if (write < capacity) + for (int offset = 0; offset < PageSize; offset++) { - results[write] = new ObjectInfo(slot.Obj.Name, slot.Obj.Locked, - slot.Obj.CustomCanReleaseFlag, - slot.Obj.LastUseTime, slot.SpawnCount); - } + ref var slot = ref slots[offset]; + if (!slot.IsAlive()) continue; - 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; @@ -506,7 +510,7 @@ namespace AlicizaX.ObjectPool [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SpawnSlot(int idx, float now) { - ref var slot = ref m_Slots[idx]; + ref var slot = ref GetSlotRef(idx); if (slot.SpawnCount == 0) MarkSlotUnavailable(idx); @@ -519,7 +523,7 @@ namespace AlicizaX.ObjectPool [MethodImpl(MethodImplOptions.AggressiveInlining)] private void UnspawnSlot(int idx) { - ref var slot = ref m_Slots[idx]; + ref var slot = ref GetSlotRef(idx); float now = Time.realtimeSinceStartup; slot.LastUseTime = now; slot.Obj.LastUseTime = now; @@ -548,43 +552,65 @@ namespace AlicizaX.ObjectPool [MethodImpl(MethodImplOptions.AggressiveInlining)] private int AllocSlot() { - if (m_FreeTop > 0) - return m_FreeStack[--m_FreeTop]; + int page = GetWritablePage(); + if (page < 0) + return -1; - if (m_SlotCount >= m_Slots.Length) - { - GrowSlots(); - if (m_SlotCount >= m_Slots.Length) - return -1; - } + int freeTop = m_PageFreeTops[page] - 1; + int offset = m_PageFreeStacks[page][freeTop]; + m_PageFreeTops[page] = freeTop; + m_PageAliveCounts[page]++; - return m_SlotCount++; + if (freeTop == 0) + RemoveFreePage(page); + + return MakeIndex(page, offset); } - private void GrowSlots() + private int GetWritablePage() { - int currentCap = m_Slots.Length; - int maxCap = m_Capacity == int.MaxValue ? int.MaxValue : Math.Max(m_Capacity, InitSlotCapacity); - int newCap = Math.Min(Math.Max(currentCap * 2, InitSlotCapacity), maxCap); - if (newCap <= currentCap) - return; + while (m_FreePageTop > 0) + { + int page = m_FreePageStack[m_FreePageTop - 1]; + if (IsAllocatedPage(page) && m_PageFreeTops[page] > 0) + return page; - var newSlots = SlotArrayPool.Rent(newCap); - var newFreeStack = SlotArrayPool.Rent(newCap); + m_FreePageTop--; + if (page >= 0 && page < m_PageFlags.Length) + m_PageFlags[page] = (byte)(m_PageFlags[page] & ~PageInFreeStack); + } - Array.Copy(m_Slots, 0, newSlots, 0, m_SlotCount); - Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop); + return AllocatePage(); + } - SlotArrayPool.Return(m_Slots, true); - SlotArrayPool.Return(m_FreeStack, true); + private int AllocatePage() + { + int page; + if (m_ReleasedPageTop > 0) + { + page = m_ReleasedPageStack[--m_ReleasedPageTop]; + } + else + { + page = m_PageCount++; + EnsurePageStorageCapacity(m_PageCount); + } - m_Slots = newSlots; - m_FreeStack = newFreeStack; + m_Pages[page] = SlotArrayPool.Rent(PageSize); + m_PageFreeStacks[page] = SlotArrayPool.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) { - ref var slot = ref m_Slots[idx]; + ref var slot = ref GetSlotRef(idx); if (!slot.IsAlive()) return; if (slot.SpawnCount > 0) return; @@ -595,7 +621,7 @@ namespace AlicizaX.ObjectPool m_TargetMap.Remove(obj.Target); obj.Release(false); - MemoryPool.Release(obj); + m_ObjectMemoryPoolHandle.Release(obj); slot.Obj = null; slot.SetAlive(false); @@ -605,21 +631,19 @@ namespace AlicizaX.ObjectPool slot.PrevUnused = -1; slot.NextUnused = -1; - if (m_FreeTop >= m_FreeStack.Length) - { - int newCap = m_FreeStack.Length * 2; - var newFreeStack = SlotArrayPool.Rent(newCap); - Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop); - SlotArrayPool.Return(m_FreeStack, true); - m_FreeStack = newFreeStack; - } - m_FreeStack[m_FreeTop++] = idx; + int page = PageOf(idx); + int offset = OffsetOf(idx); + m_PageFreeStacks[page][m_PageFreeTops[page]++] = offset; + m_PageAliveCounts[page]--; + + if (m_PageFreeTops[page] == 1) + AddFreePage(page); + + if (m_PageAliveCounts[page] == 0) + AddEmptyPageCandidate(page); if (compactStorage) - { - TrimSlotCountTail(); - ShrinkStorageIfEmpty(); - } + ReleaseEmptyPages(EmptyPageReleaseBudget); } private bool EnsureRegisterCapacity() @@ -637,36 +661,194 @@ namespace AlicizaX.ObjectPool 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()) - 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; + return (page << PageBits) | offset; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int NextPowerOf2(int value) + private static int PageOf(int index) { - value--; - value |= value >> 1; - value |= value >> 2; - value |= value >> 4; - value |= value >> 8; - value |= value >> 16; - return value + 1; + return index >> PageBits; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int OffsetOf(int index) + { + 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.Rent(newCap); + var newPageFreeStacks = SlotArrayPool.Rent(newCap); + var newPageAliveCounts = SlotArrayPool.Rent(newCap); + var newPageFreeTops = SlotArrayPool.Rent(newCap); + var newPageFlags = SlotArrayPool.Rent(newCap); + var newFreePageStack = SlotArrayPool.Rent(newCap); + var newEmptyPageStack = SlotArrayPool.Rent(newCap); + var newReleasedPageStack = SlotArrayPool.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.Return(m_Pages, true); + SlotArrayPool.Return(m_PageFreeStacks, true); + SlotArrayPool.Return(m_PageAliveCounts, true); + SlotArrayPool.Return(m_PageFreeTops, true); + SlotArrayPool.Return(m_PageFlags, true); + SlotArrayPool.Return(m_FreePageStack, true); + SlotArrayPool.Return(m_EmptyPageStack, true); + SlotArrayPool.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.Rent(newCap); + Array.Copy(stack, 0, newStack, 0, stack.Length); + SlotArrayPool.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.Return(m_Pages[page], true); + SlotArrayPool.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.Return(m_Pages[page], true); + if (m_PageFreeStacks[page] != null) + SlotArrayPool.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.Return(m_Pages, true); + SlotArrayPool.Return(m_PageFreeStacks, true); + SlotArrayPool.Return(m_PageAliveCounts, true); + SlotArrayPool.Return(m_PageFreeTops, true); + SlotArrayPool.Return(m_PageFlags, true); + SlotArrayPool.Return(m_FreePageStack, true); + SlotArrayPool.Return(m_EmptyPageStack, true); + SlotArrayPool.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) { - ref var slot = ref m_Slots[idx]; + ref var slot = ref GetSlotRef(idx); string objectName = slot.Obj.Name ?? string.Empty; if (!m_AllNameHeadMap.TryGetValue(objectName, out int head)) return; @@ -675,7 +857,7 @@ namespace AlicizaX.ObjectPool int next = slot.NextByName; if (prev >= 0) { - m_Slots[prev].NextByName = next; + GetSlotRef(prev).NextByName = next; } else { @@ -701,7 +883,7 @@ namespace AlicizaX.ObjectPool } if (next >= 0) - m_Slots[next].PrevByName = prev; + GetSlotRef(next).PrevByName = prev; slot.PrevByName = -1; slot.NextByName = -1; @@ -714,7 +896,7 @@ namespace AlicizaX.ObjectPool while (current >= 0 && released < maxReleaseCount) { - ref var slot = ref m_Slots[current]; + ref var slot = ref GetSlotRef(current); int next = slot.NextUnused; if (requireExpired && slot.LastUseTime > expireThreshold) @@ -738,10 +920,7 @@ namespace AlicizaX.ObjectPool } if (released > 0) - { - TrimSlotCountTail(); - ShrinkStorageIfEmpty(); - } + ReleaseEmptyPages(EmptyPageReleaseBudget); return released; } @@ -756,7 +935,7 @@ namespace AlicizaX.ObjectPool { 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) { return m_LastBudgetScanStart; @@ -775,23 +954,12 @@ namespace AlicizaX.ObjectPool && slot.Obj.CustomCanReleaseFlag; } - private void ShrinkStorageIfEmpty() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsValidIndex(int index) { - if (m_TargetMap.Count > 0 || m_Slots.Length <= InitSlotCapacity) - return; - - SlotArrayPool.Return(m_Slots, true); - SlotArrayPool.Return(m_FreeStack, true); - - m_Slots = SlotArrayPool.Rent(InitSlotCapacity); - m_FreeStack = SlotArrayPool.Rent(InitSlotCapacity); - m_AllNameHeadMap.Clear(); - m_NameCursorMap.Clear(); - m_SlotCount = 0; - m_FreeTop = 0; - m_UnusedHead = -1; - m_UnusedTail = -1; - m_LastBudgetScanStart = -1; + int page = index >> PageBits; + int offset = index & PageMask; + return offset < PageSize && IsAllocatedPage(page); } [Conditional("UNITY_EDITOR")] @@ -800,53 +968,60 @@ namespace AlicizaX.ObjectPool #if UNITY_EDITOR && ENABLE_OBJECTPOOL_VALIDATION int aliveCount = 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]; - if (!slot.IsAlive()) - continue; + ObjectSlot[] slots = m_Pages[page]; + if (slots == null) continue; - aliveCount++; - - object target = slot.Obj.Target; - if (!m_TargetMap.TryGetValue(target, out int mappedIdx) || mappedIdx != i) + for (int offset = 0; offset < PageSize; offset++) { - UnityEngine.Debug.LogError($"Object pool '{FullName}' target index map is inconsistent."); - continue; - } + int idx = MakeIndex(page, offset); + ref var slot = ref slots[offset]; + if (!slot.IsAlive()) + continue; - string objectName = slot.Obj.Name ?? string.Empty; - if (!m_AllNameHeadMap.TryGetValue(objectName, out int head)) - { - UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name head is missing."); - continue; - } + aliveCount++; - if (slot.PrevByName < 0 && head != i) - { - 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) + object target = slot.Obj.Target; + if (!m_TargetMap.TryGetValue(target, out int mappedIdx) || mappedIdx != idx) { - UnityEngine.Debug.LogError($"Object pool '{FullName}' unused list is inconsistent."); + UnityEngine.Debug.LogError($"Object pool '{FullName}' target index map is inconsistent."); + continue; } - } - else - { - if (inUnusedList) + + string objectName = slot.Obj.Name ?? string.Empty; + if (!m_AllNameHeadMap.TryGetValue(objectName, out int head)) { - 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; while (current >= 0) { - ref var slot = ref m_Slots[current]; + ref var slot = ref GetSlotRef(current); if (!slot.IsAlive() || slot.SpawnCount != 0) { UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain contains invalid slot."); @@ -886,7 +1061,7 @@ namespace AlicizaX.ObjectPool private void MarkSlotAvailable(int idx) { AddToUnusedListTail(idx); - ref var slot = ref m_Slots[idx]; + ref var slot = ref GetSlotRef(idx); if (slot.IsAlive()) m_NameCursorMap.AddOrUpdate(slot.Obj.Name ?? string.Empty, idx); } @@ -898,7 +1073,7 @@ namespace AlicizaX.ObjectPool 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) return; @@ -906,7 +1081,7 @@ namespace AlicizaX.ObjectPool slot.NextUnused = -1; if (m_UnusedTail >= 0) - m_Slots[m_UnusedTail].NextUnused = idx; + GetSlotRef(m_UnusedTail).NextUnused = idx; else m_UnusedHead = idx; @@ -915,7 +1090,7 @@ namespace AlicizaX.ObjectPool 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) return; @@ -923,12 +1098,12 @@ namespace AlicizaX.ObjectPool int next = slot.NextUnused; if (prev >= 0) - m_Slots[prev].NextUnused = next; + GetSlotRef(prev).NextUnused = next; else m_UnusedHead = next; if (next >= 0) - m_Slots[next].PrevUnused = prev; + GetSlotRef(next).PrevUnused = prev; else m_UnusedTail = prev; @@ -945,7 +1120,7 @@ namespace AlicizaX.ObjectPool return null; 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)) return null; diff --git a/Runtime/Resource/Resource/ResourceComponent.cs b/Runtime/Resource/Resource/ResourceComponent.cs index 2755711..74b1353 100644 --- a/Runtime/Resource/Resource/ResourceComponent.cs +++ b/Runtime/Resource/Resource/ResourceComponent.cs @@ -151,7 +151,7 @@ namespace AlicizaX.Resource.Runtime private void Awake() { - _resourceService = AppServices.RegisterApp(new ResourceService()); + _resourceService = AppServices.RegisterApp(new ResourceService()); Application.lowMemory += OnLowMemory; } diff --git a/Runtime/Scene/SceneComponent.cs b/Runtime/Scene/SceneComponent.cs index fa9ba90..88fdc04 100644 --- a/Runtime/Scene/SceneComponent.cs +++ b/Runtime/Scene/SceneComponent.cs @@ -11,7 +11,7 @@ namespace AlicizaX.Scene.Runtime { if (!AppServices.TryGetApp(out _)) { - AppServices.RegisterApp(new SceneService()); + AppServices.RegisterApp(new SceneService()); } AppServices.EnsureScene(); diff --git a/Runtime/Scene/SceneService.cs b/Runtime/Scene/SceneService.cs index 1b6a4bc..3c3d12e 100644 --- a/Runtime/Scene/SceneService.cs +++ b/Runtime/Scene/SceneService.cs @@ -292,7 +292,7 @@ namespace AlicizaX.Scene.Runtime var sceneScope = Context.EnsureScene(); if (!sceneScope.TryGet(out var sceneState)) { - sceneState = sceneScope.Register(new SceneDomainStateService()); + sceneState = (SceneDomainStateService)sceneScope.Register(new SceneDomainStateService()); } return sceneState; @@ -301,7 +301,7 @@ namespace AlicizaX.Scene.Runtime private SceneDomainStateService PrepareSceneStateForMainSceneLoad(string location) { var sceneScope = Context.ResetScene(); - var sceneState = sceneScope.Register(new SceneDomainStateService()); + var sceneState = (SceneDomainStateService)sceneScope.Register(new SceneDomainStateService()); sceneState.MarkMainSceneLoading(location); return sceneState; } diff --git a/Runtime/Timer/ITimerService.cs b/Runtime/Timer/ITimerService.cs index a5b75df..26dd95c 100644 --- a/Runtime/Timer/ITimerService.cs +++ b/Runtime/Timer/ITimerService.cs @@ -29,11 +29,27 @@ namespace AlicizaX.Timer.Runtime float GetLeftTime(ulong timerHandle); void Restart(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); 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); } +#endif } diff --git a/Runtime/Timer/TimerComponent.cs b/Runtime/Timer/TimerComponent.cs index d1ebf46..c4a44b6 100644 --- a/Runtime/Timer/TimerComponent.cs +++ b/Runtime/Timer/TimerComponent.cs @@ -8,6 +8,12 @@ namespace AlicizaX.Timer.Runtime [UnityEngine.Scripting.Preserve] public sealed class TimerComponent : MonoBehaviour { + private const int MIN_INITIAL_CAPACITY = 256; + + [SerializeField] + [Min(MIN_INITIAL_CAPACITY)] + private int _initialCapacity = 1024; + private void Awake() { if (AppServices.TryGet(out _)) @@ -15,7 +21,15 @@ namespace AlicizaX.Timer.Runtime return; } - AppServices.RegisterApp(new TimerService()); + AppServices.RegisterApp(new TimerService(_initialCapacity)); + } + + private void OnValidate() + { + if (_initialCapacity < MIN_INITIAL_CAPACITY) + { + _initialCapacity = MIN_INITIAL_CAPACITY; + } } } } diff --git a/Runtime/Timer/TimerService.cs b/Runtime/Timer/TimerService.cs index 66ab4c1..ea58ab8 100644 --- a/Runtime/Timer/TimerService.cs +++ b/Runtime/Timer/TimerService.cs @@ -6,6 +6,7 @@ using UnityEngine; namespace AlicizaX.Timer.Runtime { public delegate void TimerHandlerNoArgs(); + internal delegate void TimerGenericInvoker(object handler, object arg); internal static class TimerGenericInvokerCache where T : class @@ -22,15 +23,19 @@ namespace AlicizaX.Timer.Runtime [UnityEngine.Scripting.Preserve] [Il2CppSetOption(Option.NullChecks, false)] [Il2CppSetOption(Option.ArrayBoundsChecks, false)] - internal sealed class TimerService : ServiceBase, ITimerService, IServiceTickable + internal sealed class TimerService : ServiceBase, ITimerService, ITimerCapacityService, ITimerDebugService, IServiceTickable +#if UNITY_EDITOR + , ITimerEditorDebugService +#endif { private const int PAGE_SHIFT = 8; private const int PAGE_SIZE = 1 << PAGE_SHIFT; private const int PAGE_MASK = PAGE_SIZE - 1; - private const int INITIAL_PAGE_TABLE_CAPACITY = 4; - private const int INITIAL_INDEX_CAPACITY = PAGE_SIZE; - private const float MINIMUM_DELAY = 0.0001f; - private const float LEAK_DETECTION_THRESHOLD = 300f; + private const int DEFAULT_INITIAL_CAPACITY = 1024; + private const int MAX_PAGE_COUNT = 4096; + private const int INVALID_INDEX = -1; + private const double MINIMUM_DELAY_SECONDS = 0.000001d; + private const double STALE_ONE_SHOT_SECONDS = 300d; private const byte HANDLER_NONE = 0; private const byte HANDLER_NO_ARGS = 1; @@ -40,18 +45,20 @@ namespace AlicizaX.Timer.Runtime private const byte STATE_RUNNING = 1 << 1; private const byte STATE_LOOP = 1 << 2; private const byte STATE_UNSCALED = 1 << 3; + private const byte STATE_RELEASE_PENDING = 1 << 4; private sealed class TimerPage { public readonly ulong[] Handles = new ulong[PAGE_SIZE]; - public readonly int[] QueueIndices = new int[PAGE_SIZE]; - public readonly int[] ActiveIndices = new int[PAGE_SIZE]; - public readonly float[] TriggerTimes = new float[PAGE_SIZE]; - public readonly float[] Durations = new float[PAGE_SIZE]; - public readonly float[] RemainingTimes = new float[PAGE_SIZE]; - public readonly float[] CreationTimes = new float[PAGE_SIZE]; + public readonly uint[] Versions = new uint[PAGE_SIZE]; public readonly byte[] States = new byte[PAGE_SIZE]; public readonly byte[] HandlerTypes = new byte[PAGE_SIZE]; + public readonly double[] TriggerTimes = new double[PAGE_SIZE]; + public readonly double[] Durations = new double[PAGE_SIZE]; + public readonly double[] RemainingTimes = new double[PAGE_SIZE]; + public readonly double[] CreationTimes = new double[PAGE_SIZE]; + public readonly int[] QueueIndices = new int[PAGE_SIZE]; + public readonly int[] ActiveIndices = new int[PAGE_SIZE]; public readonly TimerHandlerNoArgs[] NoArgsHandlers = new TimerHandlerNoArgs[PAGE_SIZE]; public readonly TimerGenericInvoker[] GenericInvokers = new TimerGenericInvoker[PAGE_SIZE]; public readonly object[] GenericHandlers = new object[PAGE_SIZE]; @@ -61,225 +68,67 @@ namespace AlicizaX.Timer.Runtime { for (int i = 0; i < PAGE_SIZE; i++) { - QueueIndices[i] = -1; - ActiveIndices[i] = -1; + QueueIndices[i] = INVALID_INDEX; + ActiveIndices[i] = INVALID_INDEX; } } } - private sealed class TimerQueue + private sealed class IntPage { - private readonly TimerService _owner; - private int[] _heap; - private int _count; - - public TimerQueue(TimerService owner) - { - _owner = owner; - _heap = new int[INITIAL_INDEX_CAPACITY]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(int slotIndex) - { - EnsureCapacity(_count + 1); - - int heapIndex = _count++; - _heap[heapIndex] = slotIndex; - _owner.SetQueueIndex(slotIndex, heapIndex); - BubbleUp(heapIndex); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Remove(int slotIndex) - { - int heapIndex = _owner.GetQueueIndex(slotIndex); - if ((uint)heapIndex >= (uint)_count) - { - _owner.SetQueueIndex(slotIndex, -1); - return; - } - - int lastIndex = --_count; - int lastSlotIndex = _heap[lastIndex]; - _owner.SetQueueIndex(slotIndex, -1); - - if (heapIndex == lastIndex) - { - return; - } - - _heap[heapIndex] = lastSlotIndex; - _owner.SetQueueIndex(lastSlotIndex, heapIndex); - if (!BubbleUp(heapIndex)) - { - BubbleDown(heapIndex); - } - } - - public void Advance(float currentTime) - { - while (_count > 0) - { - int slotIndex = _heap[0]; - if (!_owner.IsSlotActive(slotIndex) || !_owner.IsSlotRunning(slotIndex)) - { - RemoveRoot(); - continue; - } - - if (_owner.GetTriggerTime(slotIndex) > currentTime) - { - return; - } - - RemoveRoot(); - _owner.ProcessDueTimer(slotIndex, currentTime); - } - } - - public void Clear() - { - while (_count > 0) - { - int slotIndex = _heap[--_count]; - _owner.SetQueueIndex(slotIndex, -1); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void RemoveRoot() - { - int rootSlotIndex = _heap[0]; - int lastIndex = --_count; - _owner.SetQueueIndex(rootSlotIndex, -1); - - if (lastIndex <= 0) - { - return; - } - - int lastSlotIndex = _heap[lastIndex]; - _heap[0] = lastSlotIndex; - _owner.SetQueueIndex(lastSlotIndex, 0); - BubbleDown(0); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool BubbleUp(int index) - { - bool moved = false; - while (index > 0) - { - int parentIndex = (index - 1) >> 1; - if (!Less(_heap[index], _heap[parentIndex])) - { - break; - } - - Swap(index, parentIndex); - index = parentIndex; - moved = true; - } - - return moved; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void BubbleDown(int index) - { - while (true) - { - int leftIndex = (index << 1) + 1; - if (leftIndex >= _count) - { - return; - } - - int rightIndex = leftIndex + 1; - int smallestIndex = leftIndex; - if (rightIndex < _count && Less(_heap[rightIndex], _heap[leftIndex])) - { - smallestIndex = rightIndex; - } - - if (!Less(_heap[smallestIndex], _heap[index])) - { - return; - } - - Swap(index, smallestIndex); - index = smallestIndex; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool Less(int leftSlotIndex, int rightSlotIndex) - { - float leftTrigger = _owner.GetTriggerTime(leftSlotIndex); - float rightTrigger = _owner.GetTriggerTime(rightSlotIndex); - if (leftTrigger < rightTrigger) - { - return true; - } - - if (leftTrigger > rightTrigger) - { - return false; - } - - return _owner.GetHandle(leftSlotIndex) < _owner.GetHandle(rightSlotIndex); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Swap(int leftIndex, int rightIndex) - { - int leftSlotIndex = _heap[leftIndex]; - int rightSlotIndex = _heap[rightIndex]; - _heap[leftIndex] = rightSlotIndex; - _heap[rightIndex] = leftSlotIndex; - _owner.SetQueueIndex(leftSlotIndex, rightIndex); - _owner.SetQueueIndex(rightSlotIndex, leftIndex); - } - - private void EnsureCapacity(int required) - { - if (required <= _heap.Length) - { - return; - } - - int newCapacity = TimerService.GetExpandedCapacity(_heap.Length, required); - int[] newHeap = new int[newCapacity]; - Array.Copy(_heap, 0, newHeap, 0, _count); - _heap = newHeap; - } + public readonly int[] Values = new int[PAGE_SIZE]; } private TimerPage[] _pages; - private int[] _freeSlots; - private int[] _activeSlots; - private readonly TimerQueue _scaledQueue; - private readonly TimerQueue _unscaledQueue; - + private IntPage[] _freeSlotPages; + private IntPage[] _activeSlotPages; + private IntPage[] _scaledHeapPages; + private IntPage[] _unscaledHeapPages; private int _pageCount; private int _slotCapacity; private int _freeCount; private int _activeCount; private int _peakActiveCount; + private int _scaledHeapCount; + private int _unscaledHeapCount; private int _executingSlotIndex; - private float _executingCurrentTime; + private double _executingCurrentTime; - public TimerService() + public TimerService() : this(DEFAULT_INITIAL_CAPACITY) { - _pages = new TimerPage[INITIAL_PAGE_TABLE_CAPACITY]; - _freeSlots = new int[INITIAL_INDEX_CAPACITY]; - _activeSlots = new int[INITIAL_INDEX_CAPACITY]; - _scaledQueue = new TimerQueue(this); - _unscaledQueue = new TimerQueue(this); - _executingSlotIndex = -1; + } + + public TimerService(int initialCapacity) + { + int normalizedCapacity = NormalizeCapacity(initialCapacity); + _pages = new TimerPage[MAX_PAGE_COUNT]; + _freeSlotPages = new IntPage[MAX_PAGE_COUNT]; + _activeSlotPages = new IntPage[MAX_PAGE_COUNT]; + _scaledHeapPages = new IntPage[MAX_PAGE_COUNT]; + _unscaledHeapPages = new IntPage[MAX_PAGE_COUNT]; + _executingSlotIndex = INVALID_INDEX; + Prewarm(normalizedCapacity); + } + + public int Order + { + get { return 0; } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Prewarm(int capacity) + { + int targetCapacity = NormalizeCapacity(capacity); + if (targetCapacity > MAX_PAGE_COUNT * PAGE_SIZE) + { + targetCapacity = MAX_PAGE_COUNT * PAGE_SIZE; + } + + while (_slotCapacity < targetCapacity) + { + AddPage(); + } - AddPage(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -290,17 +139,18 @@ namespace AlicizaX.Timer.Runtime return 0UL; } - int slotIndex = AcquireSlotIndex(); - InitializeTimer(slotIndex, time, isLoop, isUnscaled); + int slotIndex = AcquireSlot(); + if (slotIndex < 0) + { + return 0UL; + } - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - int slotOffset = slotIndex & PAGE_MASK; - page.HandlerTypes[slotOffset] = HANDLER_NO_ARGS; - page.NoArgsHandlers[slotOffset] = callback; - - AddToActiveSet(slotIndex); - ScheduleTimer(slotIndex); - return page.Handles[slotOffset]; + InitializeSlot(slotIndex, NormalizeDelay(time), isLoop, isUnscaled); + SetHandlerType(slotIndex, HANDLER_NO_ARGS); + SetNoArgsHandler(slotIndex, callback); + AddActive(slotIndex); + AddToQueue(slotIndex, isUnscaled); + return GetHandle(slotIndex); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -311,39 +161,41 @@ namespace AlicizaX.Timer.Runtime return 0UL; } - int slotIndex = AcquireSlotIndex(); - InitializeTimer(slotIndex, time, isLoop, isUnscaled); + int slotIndex = AcquireSlot(); + if (slotIndex < 0) + { + return 0UL; + } - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - int slotOffset = slotIndex & PAGE_MASK; - page.HandlerTypes[slotOffset] = HANDLER_GENERIC; - page.GenericInvokers[slotOffset] = TimerGenericInvokerCache.Invoke; - page.GenericHandlers[slotOffset] = callback; - page.GenericArgs[slotOffset] = arg; - - AddToActiveSet(slotIndex); - ScheduleTimer(slotIndex); - return page.Handles[slotOffset]; + InitializeSlot(slotIndex, NormalizeDelay(time), isLoop, isUnscaled); + SetHandlerType(slotIndex, HANDLER_GENERIC); + SetGenericInvoker(slotIndex, TimerGenericInvokerCache.Invoke); + SetGenericHandler(slotIndex, callback); + SetGenericArg(slotIndex, arg); + AddActive(slotIndex); + AddToQueue(slotIndex, isUnscaled); + return GetHandle(slotIndex); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Stop(ulong timerHandle) { int slotIndex = GetSlotIndex(timerHandle); - if (slotIndex < 0 || !IsSlotRunning(slotIndex)) + if (slotIndex < 0 || (GetState(slotIndex) & STATE_RUNNING) == 0) { return; } + bool isUnscaled = IsUnscaled(slotIndex); if (GetQueueIndex(slotIndex) >= 0) { - GetQueue(IsSlotUnscaled(slotIndex)).Remove(slotIndex); - float remainingTime = GetTriggerTime(slotIndex) - GetCurrentTime(IsSlotUnscaled(slotIndex)); - SetRemainingTime(slotIndex, remainingTime > MINIMUM_DELAY ? remainingTime : MINIMUM_DELAY); + RemoveFromQueue(slotIndex, isUnscaled); + double leftTime = GetTriggerTime(slotIndex) - GetCurrentTime(isUnscaled); + SetRemainingTime(slotIndex, leftTime > MINIMUM_DELAY_SECONDS ? leftTime : MINIMUM_DELAY_SECONDS); } else { - SetRemainingTime(slotIndex, IsSlotLoop(slotIndex) ? GetDuration(slotIndex) : MINIMUM_DELAY); + SetRemainingTime(slotIndex, IsLoop(slotIndex) ? GetDuration(slotIndex) : MINIMUM_DELAY_SECONDS); } ClearState(slotIndex, STATE_RUNNING); @@ -353,28 +205,29 @@ namespace AlicizaX.Timer.Runtime public void Resume(ulong timerHandle) { int slotIndex = GetSlotIndex(timerHandle); - if (slotIndex < 0 || IsSlotRunning(slotIndex)) + if (slotIndex < 0 || (GetState(slotIndex) & STATE_RUNNING) != 0) { return; } - float delay = GetRemainingTime(slotIndex); - if (delay <= MINIMUM_DELAY) + bool isUnscaled = IsUnscaled(slotIndex); + double delay = GetRemainingTime(slotIndex); + if (delay <= MINIMUM_DELAY_SECONDS) { - delay = MINIMUM_DELAY; + delay = MINIMUM_DELAY_SECONDS; } - SetTriggerTime(slotIndex, GetCurrentTime(IsSlotUnscaled(slotIndex)) + delay); - SetRemainingTime(slotIndex, 0f); + SetTriggerTime(slotIndex, GetCurrentTime(isUnscaled) + delay); + SetRemainingTime(slotIndex, 0d); SetState(slotIndex, STATE_RUNNING); - ScheduleTimer(slotIndex); + AddToQueue(slotIndex, isUnscaled); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsRunning(ulong timerHandle) { int slotIndex = GetSlotIndex(timerHandle); - return slotIndex >= 0 && IsSlotRunning(slotIndex); + return slotIndex >= 0 && (GetState(slotIndex) & STATE_RUNNING) != 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -386,13 +239,10 @@ namespace AlicizaX.Timer.Runtime return 0f; } - if (!IsSlotRunning(slotIndex)) - { - return GetRemainingTime(slotIndex); - } - - float leftTime = GetTriggerTime(slotIndex) - GetCurrentTime(IsSlotUnscaled(slotIndex)); - return leftTime > 0f ? leftTime : 0f; + double leftTime = (GetState(slotIndex) & STATE_RUNNING) == 0 + ? GetRemainingTime(slotIndex) + : GetTriggerTime(slotIndex) - GetCurrentTime(IsUnscaled(slotIndex)); + return leftTime > 0d ? (float)leftTime : 0f; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -404,15 +254,16 @@ namespace AlicizaX.Timer.Runtime return; } + bool isUnscaled = IsUnscaled(slotIndex); if (GetQueueIndex(slotIndex) >= 0) { - GetQueue(IsSlotUnscaled(slotIndex)).Remove(slotIndex); + RemoveFromQueue(slotIndex, isUnscaled); } - SetTriggerTime(slotIndex, GetCurrentTime(IsSlotUnscaled(slotIndex)) + GetDuration(slotIndex)); - SetRemainingTime(slotIndex, 0f); + SetTriggerTime(slotIndex, GetCurrentTime(isUnscaled) + GetDuration(slotIndex)); + SetRemainingTime(slotIndex, 0d); SetState(slotIndex, STATE_RUNNING); - ScheduleTimer(slotIndex); + AddToQueue(slotIndex, isUnscaled); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -421,15 +272,15 @@ namespace AlicizaX.Timer.Runtime int slotIndex = GetSlotIndex(timerHandle); if (slotIndex >= 0) { - ReleaseTimer(slotIndex); + ReleaseSlot(slotIndex); } } void IServiceTickable.Tick(float deltaTime) { RecoverInterruptedExecution(); - _scaledQueue.Advance(Time.time); - _unscaledQueue.Advance(Time.unscaledTime); + AdvanceQueue(false, Time.timeAsDouble); + AdvanceQueue(true, Time.unscaledTimeAsDouble); } protected override void OnInitialize() @@ -438,12 +289,10 @@ namespace AlicizaX.Timer.Runtime protected override void OnDestroyService() { - RemoveAllTimers(); + ClearAll(); } - public int Order => 0; - - void ITimerService.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount) + void ITimerDebugService.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount) { activeCount = _activeCount; poolCapacity = _slotCapacity; @@ -451,26 +300,7 @@ namespace AlicizaX.Timer.Runtime freeCount = _freeCount; } - int ITimerService.GetAllTimers(TimerDebugInfo[] results) - { - if (results == null || results.Length == 0) - { - return 0; - } - - int count = _activeCount < results.Length ? _activeCount : results.Length; - float currentTime = Time.time; - float currentUnscaledTime = Time.unscaledTime; - float realtimeSinceStartup = Time.realtimeSinceStartup; - for (int i = 0; i < count; i++) - { - FillDebugInfo(_activeSlots[i], ref results[i], currentTime, currentUnscaledTime, realtimeSinceStartup); - } - - return count; - } - - int ITimerService.GetStaleOneShotTimers(TimerDebugInfo[] results) + int ITimerDebugService.GetAllTimers(TimerDebugInfo[] results) { if (results == null || results.Length == 0) { @@ -478,48 +308,159 @@ namespace AlicizaX.Timer.Runtime } int count = 0; - float currentTime = Time.time; - float currentUnscaledTime = Time.unscaledTime; - float realtimeSinceStartup = Time.realtimeSinceStartup; - - for (int i = 0; i < _activeCount && count < results.Length; i++) + double scaledTime = Time.timeAsDouble; + double unscaledTime = Time.unscaledTimeAsDouble; + double realtime = Time.realtimeSinceStartupAsDouble; + int limit = results.Length; + for (int i = 0; i < _activeCount && count < limit; i++) { - int slotIndex = _activeSlots[i]; - if (IsSlotLoop(slotIndex)) - { - continue; - } - - float age = realtimeSinceStartup - GetCreationTime(slotIndex); - if (age <= LEAK_DETECTION_THRESHOLD) - { - continue; - } - - FillDebugInfo(slotIndex, ref results[count], currentTime, currentUnscaledTime, realtimeSinceStartup); + FillDebugInfo(GetActiveSlot(i), ref results[count], scaledTime, unscaledTime, realtime); count++; } return count; } +#if UNITY_EDITOR + int ITimerEditorDebugService.GetStaleOneShotTimers(TimerDebugInfo[] results) + { + if (results == null || results.Length == 0) + { + return 0; + } + + int count = 0; + double scaledTime = Time.timeAsDouble; + double unscaledTime = Time.unscaledTimeAsDouble; + double realtime = Time.realtimeSinceStartupAsDouble; + int limit = results.Length; + for (int i = 0; i < _activeCount && count < limit; i++) + { + int slotIndex = GetActiveSlot(i); + if (IsLoop(slotIndex) || realtime - GetCreationTime(slotIndex) <= STALE_ONE_SHOT_SECONDS) + { + continue; + } + + FillDebugInfo(slotIndex, ref results[count], scaledTime, unscaledTime, realtime); + count++; + } + + return count; + } +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int AcquireSlotIndex() + private int AcquireSlot() { if (_freeCount <= 0) { AddPage(); + if (_freeCount <= 0) + { + return INVALID_INDEX; + } } - return _freeSlots[--_freeCount]; + return GetFreeSlot(--_freeCount); + } + + private void AddPage() + { + if (_pageCount >= MAX_PAGE_COUNT) + { + return; + } + + EnsureIndexPage(_freeSlotPages, _pageCount); + EnsureIndexPage(_activeSlotPages, _pageCount); + EnsureIndexPage(_scaledHeapPages, _pageCount); + EnsureIndexPage(_unscaledHeapPages, _pageCount); + + TimerPage page = new TimerPage(); + _pages[_pageCount] = page; + + int baseSlotIndex = _pageCount << PAGE_SHIFT; + for (int i = 0; i < PAGE_SIZE; i++) + { + SetFreeSlot(_freeCount++, baseSlotIndex + i); + } + + _pageCount++; + _slotCapacity += PAGE_SIZE; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InitializeTimer(int slotIndex, float time, bool isLoop, bool isUnscaled) + private static void EnsureIndexPage(IntPage[] pages, int pageIndex) + { + if (pages[pageIndex] == null) + { + pages[pageIndex] = new IntPage(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetFreeSlot(int index) + { + return GetPagedInt(_freeSlotPages, index); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetFreeSlot(int index, int value) + { + SetPagedInt(_freeSlotPages, index, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetActiveSlot(int index) + { + return GetPagedInt(_activeSlotPages, index); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetActiveSlot(int index, int value) + { + SetPagedInt(_activeSlotPages, index, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetPagedInt(IntPage[] pages, int index) + { + return pages[index >> PAGE_SHIFT].Values[index & PAGE_MASK]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetPagedInt(IntPage[] pages, int index, int value) + { + pages[index >> PAGE_SHIFT].Values[index & PAGE_MASK] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InitializeSlot(int slotIndex, double duration, bool isLoop, bool isUnscaled) + { + int pageIndex = slotIndex >> PAGE_SHIFT; + int offset = slotIndex & PAGE_MASK; + TimerPage page = _pages[pageIndex]; + uint version = page.Versions[offset] + 1U; + page.Versions[offset] = version == 0U ? 1U : version; + page.Handles[offset] = ComposeHandle(slotIndex, page.Versions[offset]); + page.States[offset] = ComposeState(isLoop, isUnscaled); + page.TriggerTimes[offset] = GetCurrentTime(isUnscaled) + duration; + page.Durations[offset] = duration; + page.RemainingTimes[offset] = 0d; + page.CreationTimes[offset] = Time.realtimeSinceStartupAsDouble; + page.QueueIndices[offset] = INVALID_INDEX; + page.ActiveIndices[offset] = INVALID_INDEX; + page.HandlerTypes[offset] = HANDLER_NONE; + page.NoArgsHandlers[offset] = null; + page.GenericInvokers[offset] = null; + page.GenericHandlers[offset] = null; + page.GenericArgs[offset] = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte ComposeState(bool isLoop, bool isUnscaled) { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - int slotOffset = slotIndex & PAGE_MASK; - float duration = NormalizeDelay(time); byte state = (byte)(STATE_ACTIVE | STATE_RUNNING); if (isLoop) { @@ -531,33 +472,117 @@ namespace AlicizaX.Timer.Runtime state |= STATE_UNSCALED; } - page.Handles[slotOffset] = ComposeHandle(slotIndex, NextVersion(page.Handles[slotOffset])); - page.TriggerTimes[slotOffset] = GetCurrentTime(isUnscaled) + duration; - page.Durations[slotOffset] = duration; - page.RemainingTimes[slotOffset] = 0f; - page.CreationTimes[slotOffset] = Time.realtimeSinceStartup; - page.States[slotOffset] = state; - page.HandlerTypes[slotOffset] = HANDLER_NONE; - page.NoArgsHandlers[slotOffset] = null; - page.GenericInvokers[slotOffset] = null; - page.GenericHandlers[slotOffset] = null; - page.GenericArgs[slotOffset] = null; - page.QueueIndices[slotOffset] = -1; - page.ActiveIndices[slotOffset] = -1; + return state; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void RemoveAllTimers() + private static ulong ComposeHandle(int slotIndex, uint version) { - RecoverInterruptedExecution(); + return ((ulong)version << 32) | (uint)(slotIndex + 1); + } - while (_activeCount > 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetSlotIndex(ulong timerHandle) + { + int slotIndex = (int)((timerHandle & 0xffffffffUL) - 1UL); + if ((uint)slotIndex >= (uint)_slotCapacity || GetHandle(slotIndex) != timerHandle || (GetState(slotIndex) & STATE_ACTIVE) == 0) { - ReleaseTimer(_activeSlots[_activeCount - 1]); + return INVALID_INDEX; } - _scaledQueue.Clear(); - _unscaledQueue.Clear(); + return slotIndex; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddActive(int slotIndex) + { + int activeIndex = _activeCount++; + SetActiveSlot(activeIndex, slotIndex); + SetActiveIndex(slotIndex, activeIndex); + if (_activeCount > _peakActiveCount) + { + _peakActiveCount = _activeCount; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RemoveActive(int slotIndex) + { + int activeIndex = GetActiveIndex(slotIndex); + if ((uint)activeIndex >= (uint)_activeCount) + { + SetActiveIndex(slotIndex, INVALID_INDEX); + return; + } + + int lastIndex = --_activeCount; + int lastSlotIndex = GetActiveSlot(lastIndex); + SetActiveSlot(activeIndex, lastSlotIndex); + SetActiveIndex(lastSlotIndex, activeIndex); + SetActiveSlot(lastIndex, 0); + SetActiveIndex(slotIndex, INVALID_INDEX); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ReleaseSlot(int slotIndex) + { + byte state = GetState(slotIndex); + if ((state & STATE_ACTIVE) == 0) + { + return; + } + + if (GetQueueIndex(slotIndex) >= 0) + { + RemoveFromQueue(slotIndex, (state & STATE_UNSCALED) != 0); + } + + RemoveActive(slotIndex); + ClearHandler(slotIndex); + SetRemainingTime(slotIndex, 0d); + SetTriggerTime(slotIndex, 0d); + SetDuration(slotIndex, 0d); + SetCreationTime(slotIndex, 0d); + SetHandle(slotIndex, 0UL); + + if (slotIndex == _executingSlotIndex) + { + SetStateRaw(slotIndex, STATE_RELEASE_PENDING); + return; + } + + SetStateRaw(slotIndex, 0); + SetFreeSlot(_freeCount++, slotIndex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ClearHandler(int slotIndex) + { + SetHandlerType(slotIndex, HANDLER_NONE); + SetNoArgsHandler(slotIndex, null); + SetGenericInvoker(slotIndex, null); + SetGenericHandler(slotIndex, null); + SetGenericArg(slotIndex, null); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FreeReleasedExecutingSlot(int slotIndex) + { + SetStateRaw(slotIndex, 0); + SetFreeSlot(_freeCount++, slotIndex); + } + + private void ClearAll() + { + _scaledHeapCount = 0; + _unscaledHeapCount = 0; + while (_activeCount > 0) + { + _executingSlotIndex = INVALID_INDEX; + ReleaseSlot(GetActiveSlot(_activeCount - 1)); + } + + _executingSlotIndex = INVALID_INDEX; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -569,28 +594,41 @@ namespace AlicizaX.Timer.Runtime return; } - _executingSlotIndex = -1; - if (!IsSlotActive(slotIndex) || GetQueueIndex(slotIndex) >= 0) + _executingSlotIndex = INVALID_INDEX; + if ((uint)slotIndex >= (uint)_slotCapacity) { return; } - if (!IsSlotLoop(slotIndex)) + byte state = GetState(slotIndex); + if ((state & STATE_RELEASE_PENDING) != 0) { - ReleaseTimer(slotIndex); + FreeReleasedExecutingSlot(slotIndex); return; } - if (IsSlotRunning(slotIndex)) + if ((state & STATE_ACTIVE) == 0 || GetQueueIndex(slotIndex) >= 0) { - RescheduleLoopTimer(slotIndex, _executingCurrentTime); + return; + } + + if ((state & STATE_LOOP) == 0) + { + ReleaseSlot(slotIndex); + return; + } + + if ((state & STATE_RUNNING) != 0) + { + RescheduleLoop(slotIndex, _executingCurrentTime); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessDueTimer(int slotIndex, float currentTime) + private void ProcessDueTimer(int slotIndex, double currentTime) { - if (!IsSlotActive(slotIndex) || !IsSlotRunning(slotIndex)) + byte state = GetState(slotIndex); + if ((state & (STATE_ACTIVE | STATE_RUNNING)) != (STATE_ACTIVE | STATE_RUNNING)) { return; } @@ -598,464 +636,511 @@ namespace AlicizaX.Timer.Runtime _executingSlotIndex = slotIndex; _executingCurrentTime = currentTime; - switch (GetHandlerType(slotIndex)) + byte handlerType = GetHandlerType(slotIndex); + if (handlerType == HANDLER_NO_ARGS) { - case HANDLER_NO_ARGS: - InvokeNoArgs(slotIndex); - break; - - case HANDLER_GENERIC: - InvokeGeneric(slotIndex); - break; + GetNoArgsHandler(slotIndex).Invoke(); + } + else if (handlerType == HANDLER_GENERIC) + { + GetGenericInvoker(slotIndex).Invoke(GetGenericHandler(slotIndex), GetGenericArg(slotIndex)); } - _executingSlotIndex = -1; + _executingSlotIndex = INVALID_INDEX; - if (!IsSlotActive(slotIndex)) + state = GetState(slotIndex); + if ((state & STATE_RELEASE_PENDING) != 0) { + FreeReleasedExecutingSlot(slotIndex); return; } - if (GetQueueIndex(slotIndex) >= 0) + if ((state & STATE_ACTIVE) == 0 || GetQueueIndex(slotIndex) >= 0) { return; } - if (!IsSlotLoop(slotIndex)) - { - ReleaseTimer(slotIndex); - return; - } - - if (IsSlotRunning(slotIndex)) - { - RescheduleLoopTimer(slotIndex, currentTime); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ScheduleTimer(int slotIndex) - { - if (!IsSlotActive(slotIndex) || !IsSlotRunning(slotIndex) || GetQueueIndex(slotIndex) >= 0) - { - return; - } - - bool isUnscaled = IsSlotUnscaled(slotIndex); - float currentTime = GetCurrentTime(isUnscaled); - if (GetTriggerTime(slotIndex) <= currentTime) - { - SetTriggerTime(slotIndex, currentTime + MINIMUM_DELAY); - } - - GetQueue(isUnscaled).Add(slotIndex); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void RescheduleLoopTimer(int slotIndex, float currentTime) - { - float duration = GetDuration(slotIndex); - float nextTriggerTime = GetTriggerTime(slotIndex) + duration; - if (nextTriggerTime <= currentTime) - { - float overrun = currentTime - nextTriggerTime; - int skipCount = (int)(overrun / duration) + 1; - nextTriggerTime += skipCount * duration; - } - - SetTriggerTime(slotIndex, nextTriggerTime); - ScheduleTimer(slotIndex); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ReleaseTimer(int slotIndex) - { - if (!IsSlotActive(slotIndex)) - { - return; - } - - if (GetQueueIndex(slotIndex) >= 0) - { - GetQueue(IsSlotUnscaled(slotIndex)).Remove(slotIndex); - } - - if (_executingSlotIndex == slotIndex) - { - _executingSlotIndex = -1; - } - - RemoveFromActiveSet(slotIndex); - - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - int slotOffset = slotIndex & PAGE_MASK; - page.QueueIndices[slotOffset] = -1; - page.ActiveIndices[slotOffset] = -1; - page.TriggerTimes[slotOffset] = 0f; - page.Durations[slotOffset] = 0f; - page.RemainingTimes[slotOffset] = 0f; - page.CreationTimes[slotOffset] = 0f; - page.States[slotOffset] = 0; - page.HandlerTypes[slotOffset] = HANDLER_NONE; - page.NoArgsHandlers[slotOffset] = null; - page.GenericInvokers[slotOffset] = null; - page.GenericHandlers[slotOffset] = null; - page.GenericArgs[slotOffset] = null; - - _freeSlots[_freeCount++] = slotIndex; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddToActiveSet(int slotIndex) - { - EnsureIndexBufferCapacity(ref _activeSlots, _activeCount + 1); - - int activeIndex = _activeCount; - _activeSlots[activeIndex] = slotIndex; - SetActiveIndex(slotIndex, activeIndex); - _activeCount = activeIndex + 1; - - if (_activeCount > _peakActiveCount) - { - _peakActiveCount = _activeCount; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void RemoveFromActiveSet(int slotIndex) - { - int activeIndex = GetActiveIndex(slotIndex); - if ((uint)activeIndex >= (uint)_activeCount) - { - return; - } - - int lastIndex = --_activeCount; - int lastSlotIndex = _activeSlots[lastIndex]; - if (activeIndex != lastIndex) - { - _activeSlots[activeIndex] = lastSlotIndex; - SetActiveIndex(lastSlotIndex, activeIndex); - } - - SetActiveIndex(slotIndex, -1); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetSlotIndex(ulong timerHandle) - { - uint slotValue = (uint)timerHandle; - if (slotValue == 0U) - { - return -1; - } - - int slotIndex = (int)(slotValue - 1U); - if ((uint)slotIndex >= (uint)_slotCapacity) - { - return -1; - } - - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - int slotOffset = slotIndex & PAGE_MASK; - return page.States[slotOffset] != 0 && page.Handles[slotOffset] == timerHandle ? slotIndex : -1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void FillDebugInfo(int slotIndex, ref TimerDebugInfo info, float currentTime, float currentUnscaledTime, float realtimeSinceStartup) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - int slotOffset = slotIndex & PAGE_MASK; - byte state = page.States[slotOffset]; - float leftTime; - if ((state & STATE_RUNNING) != 0) - { - leftTime = page.TriggerTimes[slotOffset] - ((state & STATE_UNSCALED) != 0 ? currentUnscaledTime : currentTime); - if (leftTime < 0f) - { - leftTime = 0f; - } - } - else - { - leftTime = page.RemainingTimes[slotOffset]; - } - - byte debugFlags = 0; - if ((state & STATE_RUNNING) != 0) - { - debugFlags |= TimerDebugFlags.Running; - } - if ((state & STATE_LOOP) != 0) { - debugFlags |= TimerDebugFlags.Loop; - } - - if ((state & STATE_UNSCALED) != 0) - { - debugFlags |= TimerDebugFlags.Unscaled; - } - - info.TimerHandle = page.Handles[slotOffset]; - info.LeftTime = leftTime; - info.Duration = page.Durations[slotOffset]; - info.Age = realtimeSinceStartup - page.CreationTimes[slotOffset]; - info.Flags = debugFlags; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddPage() - { - EnsurePageTableCapacity(_pageCount + 1); - - TimerPage page = new TimerPage(); - _pages[_pageCount] = page; - - int baseSlotIndex = _pageCount << PAGE_SHIFT; - EnsureIndexBufferCapacity(ref _freeSlots, _freeCount + PAGE_SIZE); - for (int i = 0; i < PAGE_SIZE; i++) - { - _freeSlots[_freeCount + i] = baseSlotIndex + PAGE_SIZE - 1 - i; - } - - _freeCount += PAGE_SIZE; - _pageCount++; - _slotCapacity = _pageCount << PAGE_SHIFT; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong ComposeHandle(int slotIndex, uint version) - { - return ((ulong)version << 32) | ((uint)slotIndex + 1U); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint NextVersion(ulong previousHandle) - { - uint version = (uint)(previousHandle >> 32); - version++; - if (version == 0U) - { - version = 1U; - } - - return version; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float NormalizeDelay(float time) - { - return time > MINIMUM_DELAY ? time : MINIMUM_DELAY; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float GetCurrentTime(bool isUnscaled) - { - return isUnscaled ? Time.unscaledTime : Time.time; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetExpandedCapacity(int currentCapacity, int requiredCapacity) - { - int newCapacity = currentCapacity; - if (newCapacity < PAGE_SIZE) - { - newCapacity = PAGE_SIZE; - } - - while (newCapacity < requiredCapacity) - { - int growth = newCapacity >> 1; - if (growth < PAGE_SIZE) + if ((state & STATE_RUNNING) != 0) { - growth = PAGE_SIZE; + RescheduleLoop(slotIndex, currentTime); } - newCapacity += growth; + return; } - return newCapacity; + ReleaseSlot(slotIndex); } - private static void EnsureIndexBufferCapacity(ref int[] buffer, int requiredCapacity) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RescheduleLoop(int slotIndex, double currentTime) { - if (requiredCapacity <= buffer.Length) + double duration = GetDuration(slotIndex); + double triggerTime = GetTriggerTime(slotIndex) + duration; + if (triggerTime <= currentTime) + { + triggerTime = currentTime + duration; + } + + SetTriggerTime(slotIndex, triggerTime); + AddToQueue(slotIndex, IsUnscaled(slotIndex)); + } + + private void AdvanceQueue(bool isUnscaled, double currentTime) + { + while (GetHeapCount(isUnscaled) > 0) + { + int slotIndex = GetHeapRoot(isUnscaled); + byte state = GetState(slotIndex); + if ((state & (STATE_ACTIVE | STATE_RUNNING)) != (STATE_ACTIVE | STATE_RUNNING)) + { + RemoveRoot(isUnscaled); + continue; + } + + if (GetTriggerTime(slotIndex) > currentTime) + { + return; + } + + RemoveRoot(isUnscaled); + ProcessDueTimer(slotIndex, currentTime); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddToQueue(int slotIndex, bool isUnscaled) + { + int heapIndex = GetHeapCount(isUnscaled); + SetHeapCount(isUnscaled, heapIndex + 1); + SetHeapValue(isUnscaled, heapIndex, slotIndex); + SetQueueIndex(slotIndex, heapIndex); + BubbleUp(isUnscaled, heapIndex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RemoveFromQueue(int slotIndex, bool isUnscaled) + { + int heapIndex = GetQueueIndex(slotIndex); + int count = GetHeapCount(isUnscaled); + if ((uint)heapIndex >= (uint)count) + { + SetQueueIndex(slotIndex, INVALID_INDEX); + return; + } + + int lastIndex = count - 1; + int lastSlotIndex = GetHeapValue(isUnscaled, lastIndex); + SetHeapCount(isUnscaled, lastIndex); + SetQueueIndex(slotIndex, INVALID_INDEX); + if (heapIndex == lastIndex) { return; } - int newCapacity = GetExpandedCapacity(buffer.Length, requiredCapacity); - int[] newBuffer = new int[newCapacity]; - Array.Copy(buffer, 0, newBuffer, 0, buffer.Length); - buffer = newBuffer; + SetHeapValue(isUnscaled, heapIndex, lastSlotIndex); + SetQueueIndex(lastSlotIndex, heapIndex); + if (!BubbleUp(isUnscaled, heapIndex)) + { + BubbleDown(isUnscaled, heapIndex); + } } - private void EnsurePageTableCapacity(int requiredPageCount) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RemoveRoot(bool isUnscaled) { - if (requiredPageCount <= _pages.Length) + int rootSlotIndex = GetHeapRoot(isUnscaled); + int lastIndex = GetHeapCount(isUnscaled) - 1; + int lastSlotIndex = GetHeapValue(isUnscaled, lastIndex); + SetHeapCount(isUnscaled, lastIndex); + SetQueueIndex(rootSlotIndex, INVALID_INDEX); + if (lastIndex <= 0) { return; } - int newCapacity = _pages.Length; - while (newCapacity < requiredPageCount) + SetHeapValue(isUnscaled, 0, lastSlotIndex); + SetQueueIndex(lastSlotIndex, 0); + BubbleDown(isUnscaled, 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool BubbleUp(bool isUnscaled, int index) + { + bool moved = false; + while (index > 0) { - newCapacity <<= 1; + int parentIndex = (index - 1) >> 1; + if (!Less(GetHeapValue(isUnscaled, index), GetHeapValue(isUnscaled, parentIndex))) + { + break; + } + + SwapHeap(isUnscaled, index, parentIndex); + index = parentIndex; + moved = true; } - TimerPage[] newPages = new TimerPage[newCapacity]; - Array.Copy(_pages, 0, newPages, 0, _pageCount); - _pages = newPages; + return moved; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private TimerQueue GetQueue(bool isUnscaled) + private void BubbleDown(bool isUnscaled, int index) { - return isUnscaled ? _unscaledQueue : _scaledQueue; + int count = GetHeapCount(isUnscaled); + while (true) + { + int leftIndex = (index << 1) + 1; + if (leftIndex >= count) + { + return; + } + + int rightIndex = leftIndex + 1; + int smallestIndex = leftIndex; + if (rightIndex < count && Less(GetHeapValue(isUnscaled, rightIndex), GetHeapValue(isUnscaled, leftIndex))) + { + smallestIndex = rightIndex; + } + + if (!Less(GetHeapValue(isUnscaled, smallestIndex), GetHeapValue(isUnscaled, index))) + { + return; + } + + SwapHeap(isUnscaled, index, smallestIndex); + index = smallestIndex; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InvokeNoArgs(int slotIndex) + private bool Less(int leftSlotIndex, int rightSlotIndex) { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - page.NoArgsHandlers[slotIndex & PAGE_MASK](); + double leftTime = GetTriggerTime(leftSlotIndex); + double rightTime = GetTriggerTime(rightSlotIndex); + if (leftTime < rightTime) + { + return true; + } + + if (leftTime > rightTime) + { + return false; + } + + return GetHandle(leftSlotIndex) < GetHandle(rightSlotIndex); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InvokeGeneric(int slotIndex) + private void SwapHeap(bool isUnscaled, int leftIndex, int rightIndex) { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - int slotOffset = slotIndex & PAGE_MASK; - page.GenericInvokers[slotOffset](page.GenericHandlers[slotOffset], page.GenericArgs[slotOffset]); + int leftSlotIndex = GetHeapValue(isUnscaled, leftIndex); + int rightSlotIndex = GetHeapValue(isUnscaled, rightIndex); + SetHeapValue(isUnscaled, leftIndex, rightSlotIndex); + SetHeapValue(isUnscaled, rightIndex, leftSlotIndex); + SetQueueIndex(leftSlotIndex, rightIndex); + SetQueueIndex(rightSlotIndex, leftIndex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetHeapCount(bool isUnscaled) + { + return isUnscaled ? _unscaledHeapCount : _scaledHeapCount; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetHeapCount(bool isUnscaled, int value) + { + if (isUnscaled) + { + _unscaledHeapCount = value; + } + else + { + _scaledHeapCount = value; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetHeapRoot(bool isUnscaled) + { + return GetHeapValue(isUnscaled, 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetHeapValue(bool isUnscaled, int index) + { + return isUnscaled ? GetPagedInt(_unscaledHeapPages, index) : GetPagedInt(_scaledHeapPages, index); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetHeapValue(bool isUnscaled, int index, int value) + { + if (isUnscaled) + { + SetPagedInt(_unscaledHeapPages, index, value); + } + else + { + SetPagedInt(_scaledHeapPages, index, value); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsUnscaled(int slotIndex) + { + return (GetState(slotIndex) & STATE_UNSCALED) != 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsLoop(int slotIndex) + { + return (GetState(slotIndex) & STATE_LOOP) != 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double GetCurrentTime(bool isUnscaled) + { + return isUnscaled ? Time.unscaledTimeAsDouble : Time.timeAsDouble; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double NormalizeDelay(float delay) + { + return delay > MINIMUM_DELAY_SECONDS ? delay : MINIMUM_DELAY_SECONDS; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int NormalizeCapacity(int capacity) + { + int normalizedCapacity = capacity > PAGE_SIZE ? capacity : PAGE_SIZE; + int remainder = normalizedCapacity & PAGE_MASK; + if (remainder != 0) + { + normalizedCapacity += PAGE_SIZE - remainder; + } + + return normalizedCapacity; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private TimerPage GetPage(int slotIndex) + { + return _pages[slotIndex >> PAGE_SHIFT]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetOffset(int slotIndex) + { + return slotIndex & PAGE_MASK; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private ulong GetHandle(int slotIndex) { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - return page.Handles[slotIndex & PAGE_MASK]; + return GetPage(slotIndex).Handles[GetOffset(slotIndex)]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private float GetTriggerTime(int slotIndex) + private void SetHandle(int slotIndex, ulong value) { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - return page.TriggerTimes[slotIndex & PAGE_MASK]; + GetPage(slotIndex).Handles[GetOffset(slotIndex)] = value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetTriggerTime(int slotIndex, float value) + private byte GetState(int slotIndex) { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - page.TriggerTimes[slotIndex & PAGE_MASK] = value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private float GetDuration(int slotIndex) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - return page.Durations[slotIndex & PAGE_MASK]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private float GetRemainingTime(int slotIndex) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - return page.RemainingTimes[slotIndex & PAGE_MASK]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetRemainingTime(int slotIndex, float value) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - page.RemainingTimes[slotIndex & PAGE_MASK] = value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private float GetCreationTime(int slotIndex) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - return page.CreationTimes[slotIndex & PAGE_MASK]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetQueueIndex(int slotIndex) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - return page.QueueIndices[slotIndex & PAGE_MASK]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetQueueIndex(int slotIndex, int queueIndex) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - page.QueueIndices[slotIndex & PAGE_MASK] = queueIndex; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetActiveIndex(int slotIndex) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - return page.ActiveIndices[slotIndex & PAGE_MASK]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetActiveIndex(int slotIndex, int activeIndex) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - page.ActiveIndices[slotIndex & PAGE_MASK] = activeIndex; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private byte GetHandlerType(int slotIndex) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - return page.HandlerTypes[slotIndex & PAGE_MASK]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsSlotActive(int slotIndex) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - return (page.States[slotIndex & PAGE_MASK] & STATE_ACTIVE) != 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsSlotRunning(int slotIndex) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - return (page.States[slotIndex & PAGE_MASK] & STATE_RUNNING) != 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsSlotLoop(int slotIndex) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - return (page.States[slotIndex & PAGE_MASK] & STATE_LOOP) != 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsSlotUnscaled(int slotIndex) - { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - return (page.States[slotIndex & PAGE_MASK] & STATE_UNSCALED) != 0; + return GetPage(slotIndex).States[GetOffset(slotIndex)]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetState(int slotIndex, byte mask) { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - int slotOffset = slotIndex & PAGE_MASK; - page.States[slotOffset] = (byte)(page.States[slotOffset] | mask); + TimerPage page = GetPage(slotIndex); + int offset = GetOffset(slotIndex); + page.States[offset] = (byte)(page.States[offset] | mask); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ClearState(int slotIndex, byte mask) { - TimerPage page = _pages[slotIndex >> PAGE_SHIFT]; - int slotOffset = slotIndex & PAGE_MASK; - page.States[slotOffset] = (byte)(page.States[slotOffset] & ~mask); + TimerPage page = GetPage(slotIndex); + int offset = GetOffset(slotIndex); + page.States[offset] = (byte)(page.States[offset] & ~mask); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetStateRaw(int slotIndex, byte value) + { + GetPage(slotIndex).States[GetOffset(slotIndex)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private byte GetHandlerType(int slotIndex) + { + return GetPage(slotIndex).HandlerTypes[GetOffset(slotIndex)]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetHandlerType(int slotIndex, byte value) + { + GetPage(slotIndex).HandlerTypes[GetOffset(slotIndex)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double GetTriggerTime(int slotIndex) + { + return GetPage(slotIndex).TriggerTimes[GetOffset(slotIndex)]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetTriggerTime(int slotIndex, double value) + { + GetPage(slotIndex).TriggerTimes[GetOffset(slotIndex)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double GetDuration(int slotIndex) + { + return GetPage(slotIndex).Durations[GetOffset(slotIndex)]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetDuration(int slotIndex, double value) + { + GetPage(slotIndex).Durations[GetOffset(slotIndex)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double GetRemainingTime(int slotIndex) + { + return GetPage(slotIndex).RemainingTimes[GetOffset(slotIndex)]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetRemainingTime(int slotIndex, double value) + { + GetPage(slotIndex).RemainingTimes[GetOffset(slotIndex)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double GetCreationTime(int slotIndex) + { + return GetPage(slotIndex).CreationTimes[GetOffset(slotIndex)]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetCreationTime(int slotIndex, double value) + { + GetPage(slotIndex).CreationTimes[GetOffset(slotIndex)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetQueueIndex(int slotIndex) + { + return GetPage(slotIndex).QueueIndices[GetOffset(slotIndex)]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetQueueIndex(int slotIndex, int value) + { + GetPage(slotIndex).QueueIndices[GetOffset(slotIndex)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetActiveIndex(int slotIndex) + { + return GetPage(slotIndex).ActiveIndices[GetOffset(slotIndex)]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetActiveIndex(int slotIndex, int value) + { + GetPage(slotIndex).ActiveIndices[GetOffset(slotIndex)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private TimerHandlerNoArgs GetNoArgsHandler(int slotIndex) + { + return GetPage(slotIndex).NoArgsHandlers[GetOffset(slotIndex)]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetNoArgsHandler(int slotIndex, TimerHandlerNoArgs value) + { + GetPage(slotIndex).NoArgsHandlers[GetOffset(slotIndex)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private TimerGenericInvoker GetGenericInvoker(int slotIndex) + { + return GetPage(slotIndex).GenericInvokers[GetOffset(slotIndex)]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetGenericInvoker(int slotIndex, TimerGenericInvoker value) + { + GetPage(slotIndex).GenericInvokers[GetOffset(slotIndex)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private object GetGenericHandler(int slotIndex) + { + return GetPage(slotIndex).GenericHandlers[GetOffset(slotIndex)]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetGenericHandler(int slotIndex, object value) + { + GetPage(slotIndex).GenericHandlers[GetOffset(slotIndex)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private object GetGenericArg(int slotIndex) + { + return GetPage(slotIndex).GenericArgs[GetOffset(slotIndex)]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetGenericArg(int slotIndex, object value) + { + GetPage(slotIndex).GenericArgs[GetOffset(slotIndex)] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FillDebugInfo(int slotIndex, ref TimerDebugInfo info, double scaledTime, double unscaledTime, double realtime) + { + byte state = GetState(slotIndex); + bool running = (state & STATE_RUNNING) != 0; + bool unscaled = (state & STATE_UNSCALED) != 0; + double leftTime = running ? GetTriggerTime(slotIndex) - (unscaled ? unscaledTime : scaledTime) : GetRemainingTime(slotIndex); + if (leftTime < 0d) + { + leftTime = 0d; + } + + byte flags = 0; + if (running) + { + flags |= TimerDebugFlags.Running; + } + + if ((state & STATE_LOOP) != 0) + { + flags |= TimerDebugFlags.Loop; + } + + if (unscaled) + { + flags |= TimerDebugFlags.Unscaled; + } + + info.TimerHandle = GetHandle(slotIndex); + info.LeftTime = (float)leftTime; + info.Duration = (float)GetDuration(slotIndex); + info.Age = (float)(realtime - GetCreationTime(slotIndex)); + info.Flags = flags; } } } diff --git a/Runtime/UI/Constant/Em.cs b/Runtime/UI/Constant/Em.cs index b5a1d14..086ff4d 100644 --- a/Runtime/UI/Constant/Em.cs +++ b/Runtime/UI/Constant/Em.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq.Expressions; +using Cysharp.Text; public static class InstanceFactory { @@ -10,19 +11,19 @@ public static class InstanceFactory { if (!_constructorCache.TryGetValue(type, out var constructor)) { - // 验证是否存在公共无参构造函数 + // 楠岃瘉鏄惁瀛樺湪鍏叡鏃犲弬鏋勯€犲嚱鏁? var ctor = type.GetConstructor(Type.EmptyTypes); 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 lambda = Expression.Lambda>(newExpr); constructor = lambda.Compile(); - // 缓存委托 + // 缂撳瓨濮旀墭 _constructorCache[type] = constructor; } diff --git a/Runtime/UI/Constant/UIHolderFactory.cs b/Runtime/UI/Constant/UIHolderFactory.cs index a42ea20..42f166d 100644 --- a/Runtime/UI/Constant/UIHolderFactory.cs +++ b/Runtime/UI/Constant/UIHolderFactory.cs @@ -1,6 +1,7 @@ using System; using AlicizaX.Resource.Runtime; using AlicizaX; +using Cysharp.Text; using Cysharp.Threading.Tasks; using UnityEngine; using Object = UnityEngine.Object; @@ -10,7 +11,6 @@ namespace AlicizaX.UI.Runtime public static class UIHolderFactory { private static IResourceService ResourceService => AppServices.Require(); - private static bool AllowLegacyResourcesFallback => UnityEngine.Application.isEditor || UnityEngine.Debug.isDebugBuild; public static async UniTask CreateUIHolderAsync(Transform parent) where T : UIHolderObjectBase { @@ -90,65 +90,23 @@ namespace AlicizaX.UI.Runtime private static async UniTask 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); } 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); } 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); 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); diff --git a/Runtime/UI/Constant/UIMetaRegistry.cs b/Runtime/UI/Constant/UIMetaRegistry.cs index cd359a5..bb60d9d 100644 --- a/Runtime/UI/Constant/UIMetaRegistry.cs +++ b/Runtime/UI/Constant/UIMetaRegistry.cs @@ -15,8 +15,9 @@ namespace AlicizaX.UI.Runtime public readonly bool FullScreen; public readonly int CacheTime; 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; HolderRuntimeTypeHandle = holderRuntimeTypeHandle; @@ -24,18 +25,22 @@ namespace AlicizaX.UI.Runtime FullScreen = fullScreen; CacheTime = cacheTime; NeedUpdate = needUpdate; + TypeId = typeId; } } private static readonly Dictionary _typeHandleMap = new(); private static readonly Dictionary _stringHandleMap = new(); + private static int _nextTypeId; + public static int TypeCount => _nextTypeId; [MethodImpl(MethodImplOptions.AggressiveInlining)] 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 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; } diff --git a/Runtime/UI/Constant/UIMetadata.cs b/Runtime/UI/Constant/UIMetadata.cs index 63a447b..631ba8a 100644 --- a/Runtime/UI/Constant/UIMetadata.cs +++ b/Runtime/UI/Constant/UIMetadata.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using System.Threading; using AlicizaX; +using Cysharp.Text; using Cysharp.Threading.Tasks; namespace AlicizaX.UI.Runtime @@ -12,10 +13,21 @@ namespace AlicizaX.UI.Runtime public readonly UIMetaRegistry.UIMetaInfo MetaInfo; public readonly UIResRegistry.UIResInfo ResInfo; public readonly Type UILogicType; + public readonly string UILogicTypeName; + public readonly string UIHolderTypeName; public bool InCache = false; private CancellationTokenSource _cancellationTokenSource; 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 { @@ -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() { + RequestCancelCurrentOperation(); + _showInProgress = false; + _closeInProgress = false; + } + + public void RequestCancelCurrentOperation() + { + _cancelRequested = true; _cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; @@ -90,16 +158,19 @@ namespace AlicizaX.UI.Runtime } UILogicType = uiType; + UILogicTypeName = uiType.Name; 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)) { - 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; } } } diff --git a/Runtime/UI/Constant/UIMetadataObject.cs b/Runtime/UI/Constant/UIMetadataObject.cs index 3446922..99b65ff 100644 --- a/Runtime/UI/Constant/UIMetadataObject.cs +++ b/Runtime/UI/Constant/UIMetadataObject.cs @@ -12,14 +12,6 @@ namespace AlicizaX.UI.Runtime return obj; } - public static UIMetadataObject Create(UIMetadata target, RuntimeTypeHandle handle) - { - UIMetadataObject obj = MemoryPool.Acquire(); - obj.Initialize(handle.GetHashCode().ToString(), target); - return obj; - } - - protected internal override void Release(bool isShutdown) { UIMetadata metadata = Target; diff --git a/Runtime/UI/Manager/UIDebugInfo.cs b/Runtime/UI/Manager/UIDebugInfo.cs new file mode 100644 index 0000000..dc33ae6 --- /dev/null +++ b/Runtime/UI/Manager/UIDebugInfo.cs @@ -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; + } + } +} diff --git a/Runtime/UI/Manager/UIDebugInfo.cs.meta b/Runtime/UI/Manager/UIDebugInfo.cs.meta new file mode 100644 index 0000000..a08f784 --- /dev/null +++ b/Runtime/UI/Manager/UIDebugInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bdaac10881036e041859876d3852c452 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UI/Manager/UIService.Cache.cs b/Runtime/UI/Manager/UIService.Cache.cs index 1a35714..bb52eea 100644 --- a/Runtime/UI/Manager/UIService.Cache.cs +++ b/Runtime/UI/Manager/UIService.Cache.cs @@ -1,7 +1,7 @@ using System; -using System.Collections.Generic; using AlicizaX; using AlicizaX.Timer.Runtime; +using Cysharp.Text; namespace AlicizaX.UI.Runtime { @@ -19,7 +19,9 @@ namespace AlicizaX.UI.Runtime } } - private readonly Dictionary 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) { @@ -35,7 +37,7 @@ namespace AlicizaX.UI.Runtime return; } - RemoveFromCache(uiMetadata.MetaInfo.RuntimeTypeHandle); + RemoveFromCache(uiMetadata.MetaInfo.TypeId); ulong timerHandle = 0UL; uiMetadata.View.Holder.transform.SetParent(UICacheLayer); @@ -51,36 +53,115 @@ namespace AlicizaX.UI.Runtime 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; - m_CacheWindow.Add(uiMetadata.MetaInfo.RuntimeTypeHandle, new CacheEntry(uiMetadata, timerHandle)); + AddToCache(uiMetadata, timerHandle); } private void OnTimerDisposeWindow(UIMetadata meta) { if (meta != null) { - RemoveFromCache(meta.MetaInfo.RuntimeTypeHandle); + RemoveFromCache(meta.MetaInfo.TypeId); meta.DisposeImmediate(); } } 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); - entry.Metadata.InCache = false; - if (entry.TimerHandle != 0UL && _timerService != null) - { - _timerService.RemoveTimer(entry.TimerHandle); - } + RemoveFromCache(metaInfo.TypeId); } } + 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() { if (_timerService != null) diff --git a/Runtime/UI/Manager/UIService.Debug.cs b/Runtime/UI/Manager/UIService.Debug.cs new file mode 100644 index 0000000..dbddd33 --- /dev/null +++ b/Runtime/UI/Manager/UIService.Debug.cs @@ -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; + } + } +} diff --git a/Runtime/UI/Manager/UIService.Debug.cs.meta b/Runtime/UI/Manager/UIService.Debug.cs.meta new file mode 100644 index 0000000..78b38f0 --- /dev/null +++ b/Runtime/UI/Manager/UIService.Debug.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7e3f116e63c64ff4d82fa01efb769894 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UI/Manager/UIService.Initlize.cs b/Runtime/UI/Manager/UIService.Initlize.cs index 9e0e24f..59ddbea 100644 --- a/Runtime/UI/Manager/UIService.Initlize.cs +++ b/Runtime/UI/Manager/UIService.Initlize.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using Cysharp.Text; +using System.Collections.Generic; using UnityEngine; namespace AlicizaX.UI.Runtime @@ -57,7 +58,7 @@ namespace AlicizaX.UI.Runtime 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(); rect.SetParent(UICanvasRoot); rect.localScale = Vector3.one; diff --git a/Runtime/UI/Manager/UIService.Open.cs b/Runtime/UI/Manager/UIService.Open.cs index e874997..e230968 100644 --- a/Runtime/UI/Manager/UIService.Open.cs +++ b/Runtime/UI/Manager/UIService.Open.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using AlicizaX; @@ -9,16 +8,54 @@ namespace AlicizaX.UI.Runtime { sealed class LayerData { - public readonly List OrderList; - public readonly Dictionary IndexMap; + public UIMetadata[] Items; + public int[] TypeIdToIndex; + public int Count; public int LastFullscreenIndex; public LayerData(int initialCapacity) { - OrderList = new List(initialCapacity); - IndexMap = new Dictionary(initialCapacity); + Items = new UIMetadata[initialCapacity]; + TypeIdToIndex = new int[initialCapacity]; + for (int i = 0; i < TypeIdToIndex.Length; i++) + { + TypeIdToIndex[i] = -1; + } + + Count = 0; 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 @@ -28,30 +65,51 @@ namespace AlicizaX.UI.Runtime private async UniTask ShowUIImplAsync(UIMetadata metaInfo, params object[] userDatas) { CreateMetaUI(metaInfo); - EnsureMetaCanOpen(metaInfo); - await UIHolderFactory.CreateUIResourceAsync(metaInfo, UICacheLayer); - if (metaInfo.View == null || metaInfo.State == UIState.Uninitialized || metaInfo.State == UIState.Destroyed) + if (!metaInfo.BeginShowOperation()) { + 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; } FinalizeShow(metaInfo, userDatas); - await UpdateVisualState(metaInfo, metaInfo.CancellationToken); - return metaInfo.View; + bool showResult = await UpdateVisualState(metaInfo, metaInfo.CancellationToken); + metaInfo.EndShowOperation(operationVersion); + return showResult ? metaInfo.View : null; } private UIBase ShowUIImplSync(UIMetadata metaInfo, params object[] userDatas) { CreateMetaUI(metaInfo); - EnsureMetaCanOpen(metaInfo); - UIHolderFactory.CreateUIResourceSync(metaInfo, UICacheLayer); - if (metaInfo.View == null || metaInfo.State == UIState.Uninitialized || metaInfo.State == UIState.Destroyed) + if (!metaInfo.BeginShowOperation()) { + 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; } FinalizeShow(metaInfo, userDatas); - UpdateVisualState(metaInfo).Forget(); + bool showResult = UpdateVisualStateSync(metaInfo); + metaInfo.EndShowOperation(operationVersion); + if (!showResult) + { + return null; + } + return metaInfo.View; } @@ -62,29 +120,35 @@ namespace AlicizaX.UI.Runtime return; } + if (!meta.BeginCloseOperation()) + { + return; + } + + int operationVersion = meta.OperationVersion; + if (meta.State == UIState.CreatedUI) { - meta.CancelAsyncOperations(); await meta.DisposeAsync(); + meta.EndCloseOperation(operationVersion); return; } if (meta.State == UIState.Loaded || meta.State == UIState.Initialized) { - meta.CancelAsyncOperations(); var popResult = Pop(meta); SortWindowVisible(meta.MetaInfo.UILayer, popResult.previousFullscreenIndex); SortWindowDepth(meta.MetaInfo.UILayer, popResult.removedIndex >= 0 ? popResult.removedIndex : 0); meta.View.Visible = false; CacheWindow(meta, force); + meta.EndCloseOperation(operationVersion); return; } - meta.CancelAsyncOperations(); - meta.EnsureCancellationToken(); - await meta.View.InternalClose(); - if (meta.State != UIState.Closed) + bool closeResult = await meta.View.InternalClose(meta.CancellationToken); + if (!closeResult || meta.State != UIState.Closed) { + meta.EndCloseOperation(operationVersion); return; } @@ -92,6 +156,7 @@ namespace AlicizaX.UI.Runtime SortWindowVisible(meta.MetaInfo.UILayer, closedPopResult.previousFullscreenIndex); SortWindowDepth(meta.MetaInfo.UILayer, closedPopResult.removedIndex >= 0 ? closedPopResult.removedIndex : 0); CacheWindow(meta, force); + meta.EndCloseOperation(operationVersion); } @@ -106,16 +171,6 @@ namespace AlicizaX.UI.Runtime 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) { if (meta.InCache) @@ -145,11 +200,14 @@ namespace AlicizaX.UI.Runtime private void Push(UIMetadata meta) { 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.OrderList.Add(meta); - layer.IndexMap[meta.MetaInfo.RuntimeTypeHandle] = index; + layer.EnsureItemCapacity(); + int index = layer.Count++; + layer.Items[index] = meta; + layer.TypeIdToIndex[typeId] = index; if (meta.MetaInfo.FullScreen) { layer.LastFullscreenIndex = index; @@ -164,17 +222,27 @@ namespace AlicizaX.UI.Runtime { var layer = _openUI[meta.MetaInfo.UILayer]; 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); - layer.IndexMap.Remove(meta.MetaInfo.RuntimeTypeHandle); - - for (int i = index; i < layer.OrderList.Count; i++) + int index = layer.TypeIdToIndex[typeId]; + if (index < 0) { - var item = layer.OrderList[i]; - layer.IndexMap[item.MetaInfo.RuntimeTypeHandle] = i; + return (-1, previousFullscreenIndex); } + 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); return (index, previousFullscreenIndex); } @@ -196,23 +264,25 @@ namespace AlicizaX.UI.Runtime private void MoveToTop(UIMetadata meta) { 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; + int currentIdx = layer.TypeIdToIndex[typeId]; + if (currentIdx != lastIdx && currentIdx >= 0) { - layer.OrderList.RemoveAt(currentIdx); - layer.OrderList.Add(meta); - for (int i = currentIdx; i < lastIdx; i++) { - var item = layer.OrderList[i]; - layer.IndexMap[item.MetaInfo.RuntimeTypeHandle] = i; + UIMetadata item = layer.Items[i + 1]; + 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); SortWindowDepth(meta.MetaInfo.UILayer, currentIdx); @@ -220,41 +290,55 @@ namespace AlicizaX.UI.Runtime } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private async UniTask UpdateVisualState(UIMetadata meta, CancellationToken cancellationToken = default) + private async UniTask UpdateVisualState(UIMetadata meta, CancellationToken cancellationToken = default) { SortWindowVisible(meta.MetaInfo.UILayer); SortWindowDepth(meta.MetaInfo.UILayer); 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) { var layerData = _openUI[layer]; - var list = layerData.OrderList; - int count = list.Count; + int count = layerData.Count; 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; } int oldFullscreenIndex = previousFullscreenIndex == int.MinValue ? fullscreenIdx : previousFullscreenIndex; if (oldFullscreenIndex == fullscreenIdx) { - ApplyVisibilityRange(list, fullscreenIdx >= 0 ? fullscreenIdx : 0, count, fullscreenIdx); + ApplyVisibilityRange(layerData, fullscreenIdx >= 0 ? fullscreenIdx : 0, count, fullscreenIdx); return; } if (oldFullscreenIndex == -1 && fullscreenIdx == -1) { - ApplyVisibilityRange(list, 0, count, -1); + ApplyVisibilityRange(layerData, 0, count, -1); return; } @@ -265,21 +349,21 @@ namespace AlicizaX.UI.Runtime ? count : Math.Max(oldFullscreenIndex, fullscreenIdx) + 1; - ApplyVisibilityRange(list, start, endExclusive, fullscreenIdx); + ApplyVisibilityRange(layerData, start, endExclusive, fullscreenIdx); } private void SortWindowDepth(int layer, int startIndex = 0) { - var list = _openUI[layer].OrderList; + var layerData = _openUI[layer]; 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; - 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); } - private static int FindLastFullscreenIndex(List 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; } @@ -302,28 +386,28 @@ namespace AlicizaX.UI.Runtime return -1; } - private static void ApplyVisibilityRange(List list, int startInclusive, int endExclusive, int fullscreenIdx) + private static void ApplyVisibilityRange(LayerData layer, int startInclusive, int endExclusive, int fullscreenIdx) { if (startInclusive < 0) { startInclusive = 0; } - if (endExclusive > list.Count) + if (endExclusive > layer.Count) { - endExclusive = list.Count; + endExclusive = layer.Count; } bool showAll = fullscreenIdx < 0; 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) { - if (layer.OrderList.Count == 0) + if (layer.Count == 0) { layer.LastFullscreenIndex = -1; return; @@ -331,7 +415,7 @@ namespace AlicizaX.UI.Runtime if (removedMeta.MetaInfo.FullScreen && layer.LastFullscreenIndex == removedIndex) { - layer.LastFullscreenIndex = FindLastFullscreenIndex(layer.OrderList, removedIndex - 1); + layer.LastFullscreenIndex = FindLastFullscreenIndex(layer, removedIndex - 1); return; } diff --git a/Runtime/UI/Manager/UIService.cs b/Runtime/UI/Manager/UIService.cs index ac4ce67..94139d2 100644 --- a/Runtime/UI/Manager/UIService.cs +++ b/Runtime/UI/Manager/UIService.cs @@ -6,7 +6,7 @@ using UnityEngine; namespace AlicizaX.UI.Runtime { - internal sealed partial class UIService : ServiceBase, IUIService, IServiceTickable + internal sealed partial class UIService : ServiceBase, IUIService, IUIDebugService, IServiceTickable { private ITimerService _timerService; @@ -24,16 +24,16 @@ namespace AlicizaX.UI.Runtime for (int layerIndex = 0; layerIndex < _openUI.Length; layerIndex++) { var layer = _openUI[layerIndex]; - int count = layer.OrderList.Count; + int count = layer.Count; if (count == 0) continue; for (int i = 0; i < count; i++) { - if (layer.OrderList.Count != count) + if (layer.Count != count) { break; } - var window = layer.OrderList[i]; + var window = layer.Items[i]; if (window.MetaInfo.NeedUpdate) window.View.InternalUpdate(); } @@ -110,10 +110,10 @@ namespace AlicizaX.UI.Runtime continue; } - int count = layer.OrderList.Count; + int count = layer.Count; for (int i = count - 1; i >= 0; i--) { - UIMetadata meta = layer.OrderList[i]; + UIMetadata meta = layer.Items[i]; if (meta == null) { continue; @@ -123,27 +123,21 @@ namespace AlicizaX.UI.Runtime meta.DisposeImmediate(); } - layer.OrderList.Clear(); - layer.IndexMap.Clear(); + Array.Clear(layer.Items, 0, layer.Count); + for (int i = 0; i < layer.TypeIdToIndex.Length; i++) + { + layer.TypeIdToIndex[i] = -1; + } + + layer.Count = 0; layer.LastFullscreenIndex = -1; } - if (m_CacheWindow.Count > 0) + if (m_CacheWindowCount > 0) { - RuntimeTypeHandle[] handles = new RuntimeTypeHandle[m_CacheWindow.Count]; - int writeIndex = 0; - foreach (var pair in m_CacheWindow) + for (int i = m_CacheWindowCount - 1; i >= 0; i--) { - handles[writeIndex++] = pair.Key; - } - - for (int i = 0; i < writeIndex; i++) - { - if (!m_CacheWindow.TryGetValue(handles[i], out CacheEntry entry)) - { - continue; - } - + CacheEntry entry = m_CacheWindow[i]; if (entry.TimerHandle != 0UL && _timerService != null) { _timerService.RemoveTimer(entry.TimerHandle); @@ -152,9 +146,15 @@ namespace AlicizaX.UI.Runtime entry.Metadata.InCache = false; entry.Metadata.CancelAsyncOperations(); 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) diff --git a/Runtime/UI/UIBase/UIBase.Widget.cs b/Runtime/UI/UIBase/UIBase.Widget.cs index 0c3d6d6..e38f88d 100644 --- a/Runtime/UI/UIBase/UIBase.Widget.cs +++ b/Runtime/UI/UIBase/UIBase.Widget.cs @@ -1,6 +1,4 @@ using System; -using System.Buffers; -using System.Collections.Generic; using System.Threading; using AlicizaX; using Cysharp.Threading.Tasks; @@ -10,12 +8,15 @@ namespace AlicizaX.UI.Runtime { public abstract partial class UIBase { - private readonly Dictionary _children = new(); - private readonly List _updateableChildren = new(); + private UIMetadata[] _children = new UIMetadata[8]; + private int[] _childTypeIdToIndex = CreateIndexArray(8); + private int _childCount; + private UIMetadata[] _updateableChildren = new UIMetadata[4]; + private int _updateableChildCount; private void UpdateChildren() { - for (int i = 0; i < _updateableChildren.Count; i++) + for (int i = 0; i < _updateableChildCount; i++) { var meta = _updateableChildren[i]; if (meta.View.State == UIState.Opened) @@ -27,72 +28,44 @@ namespace AlicizaX.UI.Runtime private async UniTask DestroyAllChildren() { - var temp = ArrayPool.Shared.Rent(_children.Count); - try + while (_childCount > 0) { - int i = 0; - foreach (var kvp in _children) + UIMetadata metadata = _children[--_childCount]; + _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++) - { - 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.Shared.Return(temp, true); + await metadata.DisposeAsync(); + UIMetadataFactory.ReturnToPool(metadata); } - _children.Clear(); - _updateableChildren.Clear(); + _updateableChildCount = 0; } private void DestroyAllChildrenImmediate() { - var temp = ArrayPool.Shared.Rent(_children.Count); - try + while (_childCount > 0) { - int i = 0; - foreach (var kvp in _children) - { - temp[i++] = kvp.Value; - } - - for (int j = 0; j < i; j++) - { - UIMetadata metadata = temp[j]; - metadata.DisposeImmediate(); - UIMetadataFactory.ReturnToPool(metadata); - temp[j] = null; - } - } - finally - { - ArrayPool.Shared.Return(temp, true); + UIMetadata metadata = _children[--_childCount]; + _children[_childCount] = null; + ClearChildIndex(metadata); + metadata.DisposeImmediate(); + UIMetadataFactory.ReturnToPool(metadata); } - _children.Clear(); - _updateableChildren.Clear(); + _updateableChildCount = 0; } private void ChildVisible(bool value) { - foreach (KeyValuePair 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) { view.Visible = value; @@ -112,7 +85,7 @@ namespace AlicizaX.UI.Runtime { metadata.CreateUI(); UIHolderFactory.CreateUIResourceSync(metadata, parent, this); - ProcessWidget(metadata, visible).Forget(); + ProcessWidgetSync(metadata, visible); return (UIBase)metadata.View; } @@ -167,7 +140,7 @@ namespace AlicizaX.UI.Runtime metadata.CreateUI(); UIBase widget = (UIBase)metadata.View; widget.BindUIHolder(holder, this); - ProcessWidget(metadata, true).Forget(); + ProcessWidgetSync(metadata, true); return (T)widget; } @@ -185,7 +158,11 @@ namespace AlicizaX.UI.Runtime return; } - await meta.View.InternalInitlized(cancellationToken); + if (!await meta.View.InternalInitlized(cancellationToken)) + { + return; + } + meta.View.Visible = visible; if (meta.View.Visible) { @@ -195,15 +172,23 @@ namespace AlicizaX.UI.Runtime 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); return false; } + EnsureChildCapacity(); + int index = _childCount++; + _children[index] = meta; + _childTypeIdToIndex[typeId] = index; + if (meta.MetaInfo.NeedUpdate) { - _updateableChildren.Add(meta); + EnsureUpdateableChildCapacity(); + _updateableChildren[_updateableChildCount++] = meta; } return true; @@ -211,7 +196,12 @@ namespace AlicizaX.UI.Runtime 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.EnsureCancellationToken(); @@ -229,18 +219,141 @@ namespace AlicizaX.UI.Runtime private void RemoveUpdateableChild(UIMetadata meta) { - for (int i = 0; i < _updateableChildren.Count; i++) + for (int i = 0; i < _updateableChildCount; i++) { if (_updateableChildren[i] != meta) { continue; } - int lastIndex = _updateableChildren.Count - 1; + int lastIndex = _updateableChildCount - 1; _updateableChildren[i] = _updateableChildren[lastIndex]; - _updateableChildren.RemoveAt(lastIndex); + _updateableChildren[lastIndex] = null; + _updateableChildCount = lastIndex; 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; + } } } diff --git a/Runtime/UI/UIBase/UIBase.cs b/Runtime/UI/UIBase/UIBase.cs index 29d382e..bf1d414 100644 --- a/Runtime/UI/UIBase/UIBase.cs +++ b/Runtime/UI/UIBase/UIBase.cs @@ -32,6 +32,7 @@ namespace AlicizaX.UI.Runtime protected System.Object[] UserDatas => _userDatas; private RuntimeTypeHandle _runtimeTypeHandle; + private int _uiTypeId = -1; internal RuntimeTypeHandle RuntimeTypeHandler { @@ -106,6 +107,19 @@ namespace AlicizaX.UI.Runtime 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) { if (_disposed) return; @@ -204,81 +218,137 @@ namespace AlicizaX.UI.Runtime internal abstract void BindUIHolder(UIHolderObjectBase holder, UIBase owner); - internal async UniTask InternalInitlized(CancellationToken cancellationToken = default) + internal async UniTask InternalInitlized(CancellationToken cancellationToken = default) { if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Initialized)) - return; + return false; _state = UIState.Initialized; Holder.OnWindowInitEvent?.Invoke(); await OnInitializeAsync(); + if (cancellationToken.IsCancellationRequested) + return false; + 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 InternalOpen(CancellationToken cancellationToken = default) { if (_state == UIState.Opened || _state == UIState.Opening) - return; + return _state == UIState.Opened; if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Opening)) - return; + return false; int lifecycleVersion = BeginLifecycleTransition(); _state = UIState.Opening; Visible = true; Holder.OnWindowBeforeShowEvent?.Invoke(); - try + await OnOpenAsync(); + if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening) || cancellationToken.IsCancellationRequested) { - await OnOpenAsync(); - if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening)) - return; - - cancellationToken.ThrowIfCancellationRequested(); - await Holder.PlayOpenTransitionAsync(cancellationToken); + RollbackOpeningState(lifecycleVersion); + return false; } - 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)) - return; + return false; _state = UIState.Opened; 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 InternalClose(CancellationToken cancellationToken = default) { if (_state == UIState.Closed || _state == UIState.Closing) - return; + return _state == UIState.Closed; if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Closing)) - return; + return false; int lifecycleVersion = BeginLifecycleTransition(); _state = UIState.Closing; Holder.OnWindowBeforeClosedEvent?.Invoke(); - try - { - await OnCloseAsync(); - if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing)) - return; + await OnCloseAsync(); + if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing) || cancellationToken.IsCancellationRequested) + return false; - cancellationToken.ThrowIfCancellationRequested(); - await Holder.PlayCloseTransitionAsync(cancellationToken); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + bool closeCanceled = await Holder.PlayCloseTransitionAsync(cancellationToken).SuppressCancellationThrow(); + if (closeCanceled || cancellationToken.IsCancellationRequested) { - return; + return false; } if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing)) - return; + return false; Visible = false; _state = UIState.Closed; 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() @@ -342,6 +412,15 @@ namespace AlicizaX.UI.Runtime return lifecycleVersion == _lifecycleVersion && _state == state; } + private void RollbackOpeningState(int lifecycleVersion) + { + if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening)) + return; + + Visible = false; + _state = UIState.Initialized; + } + #endregion } } diff --git a/Runtime/UI/UIBase/UIStateMachine.cs b/Runtime/UI/UIBase/UIStateMachine.cs index 4fa6679..97a0a96 100644 --- a/Runtime/UI/UIBase/UIStateMachine.cs +++ b/Runtime/UI/UIBase/UIStateMachine.cs @@ -1,28 +1,26 @@ -using System; -using System.Collections.Generic; - namespace AlicizaX.UI.Runtime { internal static class UIStateMachine { - private static readonly Dictionary> _validTransitions = new() + private static readonly ushort[] ValidTransitionMasks = { - [UIState.Uninitialized] = new() { UIState.CreatedUI }, - [UIState.CreatedUI] = new() { UIState.Loaded, UIState.Destroying }, - [UIState.Loaded] = new() { UIState.Initialized, UIState.Destroying }, - [UIState.Initialized] = new() { UIState.Opening, UIState.Destroying }, - [UIState.Opening] = new() { UIState.Opened, UIState.Closing, UIState.Destroying }, - [UIState.Opened] = new() { UIState.Closing, UIState.Destroying }, - [UIState.Closing] = new() { UIState.Opening, UIState.Closed, UIState.Destroying }, - [UIState.Closed] = new() { UIState.Opening, UIState.Destroying }, - [UIState.Destroying] = new() { UIState.Destroyed }, - [UIState.Destroyed] = new() { } + Mask(UIState.CreatedUI), + Mask(UIState.Loaded, UIState.Destroying), + Mask(UIState.Initialized, UIState.Destroying), + Mask(UIState.Opening, UIState.Destroying), + Mask(UIState.Opened, UIState.Closing, UIState.Destroying), + Mask(UIState.Closing, UIState.Destroying), + Mask(UIState.Opening, UIState.Closed, UIState.Destroying), + Mask(UIState.Opening, UIState.Destroying), + Mask(UIState.Destroyed), + 0, }; 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) @@ -30,16 +28,15 @@ namespace AlicizaX.UI.Runtime if (IsValidTransition(from, to)) 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; } - public static HashSet GetValidNextStates(UIState currentState) + public static ushort GetValidNextStateMask(UIState currentState) { - return _validTransitions.TryGetValue(currentState, out var states) - ? states - : new HashSet(); + int stateIndex = (int)currentState; + return (uint)stateIndex < (uint)ValidTransitionMasks.Length ? ValidTransitionMasks[stateIndex] : (ushort)0; } public static string GetStateDescription(UIState state) @@ -64,5 +61,20 @@ namespace AlicizaX.UI.Runtime { 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)); + } } } diff --git a/Runtime/UI/UIBase/UITabWindow.cs b/Runtime/UI/UIBase/UITabWindow.cs index 14a8e5f..5d80105 100644 --- a/Runtime/UI/UIBase/UITabWindow.cs +++ b/Runtime/UI/UIBase/UITabWindow.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using AlicizaX; +using Cysharp.Text; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UI; @@ -98,25 +99,19 @@ namespace AlicizaX.UI.Runtime private async UniTask StartAsyncLoading(RuntimeTypeHandle typeHandle, params System.Object[] userDatas) { _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; SwitchToLoadedTab(tabWidget, userDatas); } - catch (Exception e) + else { - Debug.LogError($"Tab load failed: {e}"); - } - finally - { - _loadingFlags.Remove(typeHandle); + Debug.LogError(ZString.Format("Tab load failed: {0}", Type.GetTypeFromHandle(typeHandle)?.Name)); } } @@ -133,7 +128,7 @@ namespace AlicizaX.UI.Runtime { 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; } diff --git a/Runtime/UI/UIBase/UIWidget.cs b/Runtime/UI/UIBase/UIWidget.cs index 7ea97cd..c30a570 100644 --- a/Runtime/UI/UIBase/UIWidget.cs +++ b/Runtime/UI/UIBase/UIWidget.cs @@ -13,12 +13,12 @@ namespace AlicizaX.UI.Runtime public void Open(params System.Object[] userDatas) { RefreshParams(userDatas); - InternalOpen().Forget(); + InternalOpenSync(); } public void Close() { - InternalClose().Forget(); + InternalCloseSync(); } public void Destroy() diff --git a/Runtime/UI/UIComponent.cs b/Runtime/UI/UIComponent.cs index 0bf300f..0a74bd8 100644 --- a/Runtime/UI/UIComponent.cs +++ b/Runtime/UI/UIComponent.cs @@ -15,6 +15,7 @@ namespace AlicizaX.UI.Runtime [SerializeField] private GameObject uiRoot = null; [SerializeField] private bool _isOrthographic = true; private Transform _instanceRoot = null; + private const string CanvasScalerMissingMessage = "Not found CanvasScaler !"; private IUIService _uiService; @@ -24,7 +25,7 @@ namespace AlicizaX.UI.Runtime private void Awake() { - _uiService = AppServices.RegisterApp(new UIService()); + _uiService = AppServices.RegisterApp(new UIService()); if (uiRoot == null) { throw new GameFrameworkException("UIRoot Prefab is invalid."); @@ -54,7 +55,7 @@ namespace AlicizaX.UI.Runtime CanvasScaler scaler = _uiService.UICanvasRoot.GetComponent(); if (scaler == null) { - Log.Error($"Not found {nameof(CanvasScaler)} !"); + Log.Error(CanvasScalerMissingMessage); return; }