@@ -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
0 commit comments