Skip to content

Commit 3caf48d

Browse files
committed
refactor(ModSettings): enhance prewarming and entry management for mod settings UI
- Introduced a new ModSettingsReusableEntryNodePool to manage reusable UI components, improving memory efficiency and performance. - Implemented background prewarming for mod settings to reduce initial load times and enhance user experience. - Updated various controls to support dynamic binding and clear action handling, ensuring better responsiveness in the UI. - Refactored existing methods to streamline the creation and management of mod settings entries, enhancing maintainability and clarity.
1 parent 61ba411 commit 3caf48d

9 files changed

Lines changed: 1416 additions & 302 deletions

File tree

Settings/Integration/Patches/ModSettingsUiPatches.cs

Lines changed: 80 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,16 @@ public class SettingsScreenModSettingsButtonPatch : IPatchMethod
120120
private const string GeneralSettingsResizeHookMeta = "ritsulib_general_settings_content_resize_hook";
121121

122122
private const string PrewarmScheduledMeta = "ritsulib_mod_settings_prewarm_scheduled";
123+
private const double PrewarmInitialDelaySeconds = 3.0d;
124+
private const int PrewarmFrameDelay = 1;
123125

124126
private const string EntryLineNodeName = "RitsuLibModSettings";
125127

126128
private const string EntryDividerNodeName = "RitsuLibModSettingsDivider";
127129

130+
private static readonly ConditionalWeakTable<NSettingsScreen, ModSettingsMirrorPrewarmSession> PrewarmSessions =
131+
new();
132+
128133
/// <inheritdoc />
129134
public static string PatchId => "ritsulib_mod_settings_button";
130135

@@ -157,7 +162,7 @@ public static void Postfix(NSettingsScreen __instance)
157162
try
158163
{
159164
var line = EnsureEntryPoint(__instance);
160-
RefreshState(line);
165+
RefreshState(line, __instance);
161166
var generalPanel = __instance.GetNode<NSettingsPanel>("%GeneralSettings");
162167
ScheduleRefreshGeneralSettingsPanelSize(generalPanel);
163168
if (generalPanel.Content is { } generalVBox)
@@ -175,51 +180,100 @@ public static void Postfix(NSettingsScreen __instance)
175180
/// Pre-warms the mod settings UI while the user is still on the vanilla settings screen — before they
176181
/// click "Mod Settings (RitsuLib)". The first open otherwise runs a concentrated one-time
177182
/// initialization (reflection-based mirror registration, sidebar build, first page build) all at once,
178-
/// producing a visible stall. The work is spread across idle frames so the vanilla screen does not
179-
/// stall either, and is scheduled once per screen instance.
183+
/// producing a visible stall. Reflection and data registration run on a background worker; only Godot node
184+
/// creation and UI refresh stay on the main thread.
180185
/// 在用户仍处于原版设置界面时(即点击 “Mod Settings (RitsuLib)” 之前)预热 mod 设置 UI。否则首次打开会一次性执行
181-
/// 集中的一次性初始化(基于反射的镜像注册、侧边栏构建、首页构建),造成可见卡顿。该工作被分散到多个空闲帧,使原版界面
182-
/// 也不卡,并对每个界面实例只调度一次
186+
/// 集中的一次性初始化(基于反射的镜像注册、侧边栏构建、首页构建),造成可见卡顿。反射和数据注册在后台线程执行;
187+
/// 只有 Godot 节点创建和 UI 刷新留在主线程
183188
/// </summary>
184189
private static void TrySchedulePrewarm(NSettingsScreen screen)
185190
{
186191
if (screen.HasMeta(PrewarmScheduledMeta))
187192
return;
188193
screen.SetMeta(PrewarmScheduledMeta, true);
189-
Callable.From(() => PrewarmStep(screen, 0)).CallDeferred();
194+
ScheduleInitialPrewarmStep(screen);
190195
}
191196

192-
private static void PrewarmStep(NSettingsScreen screen, int step)
197+
private static void PrewarmStep(NSettingsScreen screen)
193198
{
194199
if (!GodotObject.IsInstanceValid(screen))
195200
return;
196201

197202
try
198203
{
199-
switch (step)
204+
var session = PrewarmSessions.GetValue(screen, CreatePrewarmSession);
205+
206+
if (!session.Resume())
200207
{
201-
case 0:
202-
RitsuLibModSettingsBootstrap.EnsureFrameworkPagesRegistered();
203-
break;
204-
case 1:
205-
ModSettingsMirrorRegistrarBootstrap.TryRegisterMirroredPages();
206-
RitsuLibModSettingsBootstrap.RefreshDynamicPages();
207-
break;
208-
case 2:
209-
{
210-
var stack = screen.GetAncestorOfType<NSubmenuStack>();
211-
if (stack != null)
212-
ModSettingsSubmenuPatch.Submenus.GetValue(stack, ModSettingsSubmenuPatch.CreateSubmenu);
213-
return;
214-
}
208+
SchedulePrewarmStep(screen, PrewarmFrameDelay);
209+
return;
215210
}
211+
212+
RitsuLibModSettingsBootstrap.RefreshDynamicPages();
213+
ScheduleSubmenuPrewarm(screen);
214+
PrewarmSessions.Remove(screen);
215+
}
216+
catch (Exception ex)
217+
{
218+
PrewarmSessions.Remove(screen);
219+
RitsuLibFramework.Logger.Warn($"[Settings] Mod settings prewarm failed: {ex.Message}");
220+
}
221+
}
222+
223+
private static void SchedulePrewarmStep(NSettingsScreen screen, int delayFrames)
224+
{
225+
if (delayFrames <= 0)
226+
{
227+
Callable.From(() => PrewarmStep(screen)).CallDeferred();
228+
return;
229+
}
230+
231+
Callable.From(() => SchedulePrewarmStep(screen, delayFrames - 1)).CallDeferred();
232+
}
233+
234+
private static void ScheduleSubmenuPrewarm(NSettingsScreen screen)
235+
{
236+
if (!GodotObject.IsInstanceValid(screen))
237+
return;
238+
239+
Callable.From(() => TryCreateSubmenuForPrewarm(screen)).CallDeferred();
240+
}
241+
242+
private static void TryCreateSubmenuForPrewarm(NSettingsScreen screen)
243+
{
244+
if (!GodotObject.IsInstanceValid(screen))
245+
return;
246+
247+
try
248+
{
249+
var stack = screen.GetAncestorOfType<NSubmenuStack>();
250+
if (stack == null)
251+
return;
252+
253+
var submenu = ModSettingsSubmenuPatch.Submenus.GetValue(stack, ModSettingsSubmenuPatch.CreateSubmenu);
254+
_ = submenu.WaitForFirstPageContentPrewarmedAsync();
216255
}
217256
catch (Exception ex)
218257
{
219-
RitsuLibFramework.Logger.Warn($"[Settings] Mod settings prewarm step {step} failed: {ex.Message}");
258+
RitsuLibFramework.Logger.Warn($"[Settings] Mod settings submenu prewarm failed: {ex.Message}");
259+
}
260+
}
261+
262+
private static void ScheduleInitialPrewarmStep(NSettingsScreen screen)
263+
{
264+
if (screen.GetTree() is not { } tree)
265+
{
266+
SchedulePrewarmStep(screen, PrewarmFrameDelay);
267+
return;
220268
}
221269

222-
Callable.From(() => PrewarmStep(screen, step + 1)).CallDeferred();
270+
var timer = tree.CreateTimer(PrewarmInitialDelaySeconds);
271+
timer.Timeout += () => SchedulePrewarmStep(screen, 0);
272+
}
273+
274+
private static ModSettingsMirrorPrewarmSession CreatePrewarmSession(NSettingsScreen _)
275+
{
276+
return ModSettingsMirrorRegistrarBootstrap.CreatePrewarmSession();
223277
}
224278

225279
private static MarginContainer EnsureEntryPoint(NSettingsScreen screen)
@@ -251,6 +305,7 @@ private static MarginContainer EnsureEntryPoint(NSettingsScreen screen)
251305

252306
void OpenSubmenu()
253307
{
308+
SchedulePrewarmStep(screen, 0);
254309
screen.GetAncestorOfType<NSubmenuStack>()?.PushSubmenuType(typeof(RitsuModSettingsSubmenu));
255310
}
256311
}
@@ -305,7 +360,7 @@ private static void ScheduleRefreshGeneralSettingsPanelSize(NSettingsPanel panel
305360
Callable.From(() => RefreshPanelSize(panel)).CallDeferred();
306361
}
307362

308-
private static void RefreshState(MarginContainer line)
363+
private static void RefreshState(MarginContainer line, NSettingsScreen screen)
309364
{
310365
line.Visible = true;
311366

Settings/ModSettings/Mirrors/ModSettingsMirrorRegistrarBootstrap.cs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ namespace STS2RitsuLib.Settings
22
{
33
internal static class ModSettingsMirrorRegistrarBootstrap
44
{
5+
private static volatile bool _backgroundPrewarmComplete;
6+
7+
public static bool IsBackgroundPrewarmComplete => _backgroundPrewarmComplete;
8+
9+
internal static void MarkBackgroundPrewarmComplete()
10+
{
11+
_backgroundPrewarmComplete = true;
12+
}
13+
514
public static int TryRegisterMirroredPages()
615
{
716
var added = 0;
@@ -12,5 +21,117 @@ public static int TryRegisterMirroredPages()
1221
added += BaseLibToRitsuGeneratedMirrorSource.TryRegisterMirroredPages();
1322
return added;
1423
}
24+
25+
public static ModSettingsMirrorPrewarmSession CreatePrewarmSession()
26+
{
27+
return new([
28+
new BackgroundPrewarmWork(() => BaseLibMirrorSource.TryRegisterMirroredPages()),
29+
new BackgroundPrewarmWork(() => ModConfigMirrorSource.TryRegisterMirroredPages()),
30+
new BackgroundPrewarmWork(RuntimeInteropMirrorSource.TryRegisterMirroredPages),
31+
new BackgroundPrewarmWork(RuntimeReflectionMirrorSource.TryRegisterMirroredPages),
32+
new BackgroundPrewarmWork(() => BaseLibToRitsuGeneratedMirrorSource.TryRegisterMirroredPages()),
33+
]);
34+
}
35+
36+
private sealed class BackgroundPrewarmWork(Func<int> register) : IModSettingsMirrorPrewarmWork
37+
{
38+
private volatile bool _complete;
39+
private Exception? _error;
40+
private bool _reportedFailure;
41+
private int _result;
42+
private bool _started;
43+
private Thread? _thread;
44+
45+
public bool IsComplete => _complete;
46+
47+
public int Resume()
48+
{
49+
if (!_started)
50+
StartWorker();
51+
52+
if (!_complete)
53+
return 0;
54+
55+
if (_error == null) return _result;
56+
if (_reportedFailure) return 0;
57+
_reportedFailure = true;
58+
RitsuLibFramework.Logger.Warn(
59+
$"[Settings] Mod settings background prewarm failed: {_error.Message}");
60+
61+
return 0;
62+
}
63+
64+
private void StartWorker()
65+
{
66+
_started = true;
67+
_thread = new(WorkerMain)
68+
{
69+
IsBackground = true,
70+
Name = "RitsuLib Mod Settings Prewarm",
71+
Priority = ThreadPriority.Lowest,
72+
};
73+
_thread.Start();
74+
}
75+
76+
private void WorkerMain()
77+
{
78+
try
79+
{
80+
try
81+
{
82+
Thread.CurrentThread.Priority = ThreadPriority.Lowest;
83+
}
84+
catch
85+
{
86+
// Best effort only.
87+
}
88+
89+
_result = register();
90+
}
91+
catch (Exception ex)
92+
{
93+
_error = ex;
94+
}
95+
finally
96+
{
97+
_complete = true;
98+
}
99+
}
100+
}
101+
}
102+
103+
internal interface IModSettingsMirrorPrewarmWork
104+
{
105+
bool IsComplete { get; }
106+
107+
int Resume();
108+
}
109+
110+
internal sealed class ModSettingsMirrorPrewarmSession(IReadOnlyList<IModSettingsMirrorPrewarmWork> workItems)
111+
{
112+
private int _workIndex;
113+
114+
public int Added { get; private set; }
115+
116+
public bool IsComplete => _workIndex >= workItems.Count;
117+
118+
public bool Resume()
119+
{
120+
if (IsComplete)
121+
return true;
122+
123+
while (_workIndex < workItems.Count)
124+
{
125+
var work = workItems[_workIndex];
126+
Added += work.Resume();
127+
if (!work.IsComplete)
128+
return false;
129+
130+
_workIndex++;
131+
}
132+
133+
ModSettingsMirrorRegistrarBootstrap.MarkBackgroundPrewarmComplete();
134+
return true;
135+
}
15136
}
16137
}

0 commit comments

Comments
 (0)