improved startup first frame boot
This commit is contained in:
@@ -12,7 +12,8 @@
|
||||
"mcp__manual-slop__get_file_summary",
|
||||
"mcp__manual-slop__get_tree",
|
||||
"mcp__manual-slop__list_directory",
|
||||
"mcp__manual-slop__py_get_skeleton"
|
||||
"mcp__manual-slop__py_get_skeleton",
|
||||
"Bash(uv run *)"
|
||||
]
|
||||
},
|
||||
"enableAllProjectMcpServers": true,
|
||||
|
||||
+11
-13
@@ -12,11 +12,9 @@ use_default_base_prompt = true
|
||||
|
||||
[projects]
|
||||
paths = [
|
||||
"C:/projects/gencpp/.ai/gencpp_sloppy.toml",
|
||||
"C:/projects/manual_slop/manual_slop.toml",
|
||||
"C:/projects/Pikuma/ps1-ai/pikuma_ps1.toml",
|
||||
"project.toml",
|
||||
]
|
||||
active = "C:/projects/Pikuma/ps1-ai/pikuma_ps1.toml"
|
||||
active = "project.toml"
|
||||
|
||||
[gui]
|
||||
separate_message_panel = true
|
||||
@@ -62,23 +60,23 @@ Diagnostics = false
|
||||
"Undo/Redo History" = false
|
||||
|
||||
[theme]
|
||||
palette = "solarized_dark"
|
||||
font_path = "C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf"
|
||||
palette = "Monokai"
|
||||
font_path = "fonts/MapleMono-Regular.ttf"
|
||||
font_size = 20.0
|
||||
scale = 1.0199999809265137
|
||||
transparency = 1.0
|
||||
child_transparency = 1.0
|
||||
|
||||
[theme.tone_mapping.solarized_light]
|
||||
brightness = 0.6899999976158142
|
||||
contrast = 0.8600000143051147
|
||||
gamma = 0.7699999809265137
|
||||
|
||||
[theme.tone_mapping.moss]
|
||||
brightness = 1.059999942779541
|
||||
contrast = 0.5799999833106995
|
||||
gamma = 1.059999942779541
|
||||
|
||||
[theme.tone_mapping.solarized_light]
|
||||
brightness = 0.6899999976158142
|
||||
contrast = 0.8600000143051147
|
||||
gamma = 0.7699999809265137
|
||||
|
||||
[theme.tone_mapping.Binks]
|
||||
brightness = 0.5600000023841858
|
||||
contrast = 0.7900000214576721
|
||||
@@ -97,8 +95,8 @@ api_key = "test-secret-key"
|
||||
|
||||
[paths]
|
||||
conductor_dir = "C:\\projects\\gencpp\\.ai\\conductor"
|
||||
logs_dir = "C:\\projects\\manual_slop\\logs"
|
||||
scripts_dir = "C:\\projects\\manual_slop\\scripts"
|
||||
logs_dir = "C:\\projects\\sloppy\\logs"
|
||||
scripts_dir = "C:\\projects\\sloppy\\scripts"
|
||||
|
||||
[rag]
|
||||
enabled = false
|
||||
|
||||
+19
-19
@@ -44,20 +44,20 @@ Collapsed=0
|
||||
DockId=0x00000010,0
|
||||
|
||||
[Window][Message]
|
||||
Pos=1448,29
|
||||
Size=1465,1840
|
||||
Pos=561,29
|
||||
Size=1138,1195
|
||||
Collapsed=0
|
||||
DockId=0x00000006,1
|
||||
|
||||
[Window][Response]
|
||||
Pos=0,29
|
||||
Size=1446,1840
|
||||
Size=559,1195
|
||||
Collapsed=0
|
||||
DockId=0x00000010,5
|
||||
|
||||
[Window][Tool Calls]
|
||||
Pos=1448,29
|
||||
Size=1465,1840
|
||||
Pos=561,29
|
||||
Size=1138,1195
|
||||
Collapsed=0
|
||||
DockId=0x00000006,3
|
||||
|
||||
@@ -77,7 +77,7 @@ DockId=0xAFC85805,2
|
||||
|
||||
[Window][Theme]
|
||||
Pos=0,29
|
||||
Size=1446,1840
|
||||
Size=559,1195
|
||||
Collapsed=0
|
||||
DockId=0x00000010,1
|
||||
|
||||
@@ -105,26 +105,26 @@ Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][Discussion Hub]
|
||||
Pos=1448,29
|
||||
Size=1465,1840
|
||||
Pos=561,29
|
||||
Size=1138,1195
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Window][Operations Hub]
|
||||
Pos=0,29
|
||||
Size=1446,1840
|
||||
Size=559,1195
|
||||
Collapsed=0
|
||||
DockId=0x00000010,4
|
||||
|
||||
[Window][Files & Media]
|
||||
Pos=0,29
|
||||
Size=1446,1840
|
||||
Size=559,1195
|
||||
Collapsed=0
|
||||
DockId=0x00000010,2
|
||||
|
||||
[Window][AI Settings]
|
||||
Pos=0,29
|
||||
Size=1446,1840
|
||||
Size=559,1195
|
||||
Collapsed=0
|
||||
DockId=0x00000010,3
|
||||
|
||||
@@ -140,8 +140,8 @@ Collapsed=0
|
||||
DockId=0x00000006,2
|
||||
|
||||
[Window][Log Management]
|
||||
Pos=1448,29
|
||||
Size=1465,1840
|
||||
Pos=561,29
|
||||
Size=1138,1195
|
||||
Collapsed=0
|
||||
DockId=0x00000006,2
|
||||
|
||||
@@ -410,7 +410,7 @@ DockId=0x00000006,1
|
||||
|
||||
[Window][Project Settings]
|
||||
Pos=0,29
|
||||
Size=1446,1840
|
||||
Size=559,1195
|
||||
Collapsed=0
|
||||
DockId=0x00000010,0
|
||||
|
||||
@@ -510,7 +510,7 @@ Pos=60,60
|
||||
Size=900,700
|
||||
Collapsed=0
|
||||
|
||||
[Window][###Text_Viewer]
|
||||
[Window][Text_Viewer]
|
||||
Pos=58,169
|
||||
Size=1801,1532
|
||||
Collapsed=0
|
||||
@@ -520,7 +520,7 @@ Pos=156,171
|
||||
Size=2176,1441
|
||||
Collapsed=0
|
||||
|
||||
[Window][###Text_Viewer_Unified]
|
||||
[Window][Text_Viewer_Unified]
|
||||
Pos=182,742
|
||||
Size=1163,908
|
||||
Collapsed=0
|
||||
@@ -829,13 +829,13 @@ Column 4 Weight=1.0000
|
||||
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
||||
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
||||
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,29 Size=2913,1840 Split=X
|
||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,29 Size=1699,1195 Split=X
|
||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2357,1183 Split=X
|
||||
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
||||
DockNode ID=0x00000005 Parent=0x0000000B SizeRef=1446,1681 Split=Y Selected=0x3F1379AF
|
||||
DockNode ID=0x00000005 Parent=0x0000000B SizeRef=573,1681 Split=Y Selected=0x3F1379AF
|
||||
DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x3F1379AF
|
||||
DockNode ID=0x00000011 Parent=0x00000005 SizeRef=983,184 Selected=0x432BAE4E
|
||||
DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1465,1681 Selected=0x2C0206CE
|
||||
DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1138,1681 Selected=0x2C0206CE
|
||||
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
||||
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=488,1183 Selected=0x3AEC3498
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@ active = "main"
|
||||
|
||||
[discussions.main]
|
||||
git_commit = ""
|
||||
last_updated = "2026-06-05T18:41:59"
|
||||
last_updated = "2026-06-06T13:21:40"
|
||||
history = []
|
||||
|
||||
+10
-1
@@ -421,13 +421,22 @@ def _classify_minimax_error(exc: Exception) -> ProviderError:
|
||||
if "400" in body_l or "bad request" in body_l: return ProviderError("unknown", "minimax", Exception(f"MiniMax Bad Request: {body}"))
|
||||
return ProviderError("unknown", "minimax", Exception(body))
|
||||
|
||||
def set_provider(provider: str, model: str) -> None:
|
||||
def set_provider(provider: str, model: str, validate: bool = True) -> None:
|
||||
"""
|
||||
Updates the active LLM provider and model name.
|
||||
|
||||
When validate is True (default), the model is checked against the provider's
|
||||
LIVE model list, which for gemini_cli/minimax means a blocking subprocess /
|
||||
network call (and importing the provider SDK). Pass validate=False during
|
||||
startup so the GUI's first frame is not blocked — AppController._fetch_models
|
||||
corrects the model against the live list shortly after, off the main thread.
|
||||
[C: src/app_controller.py:AppController._handle_reset_session, src/app_controller.py:AppController._init_ai_and_hooks, src/app_controller.py:AppController.current_model, src/app_controller.py:AppController.current_provider, src/app_controller.py:AppController.do_fetch, src/multi_agent_conductor.py:run_worker_lifecycle, src/orchestrator_pm.py:generate_tracks, tests/conftest.py:reset_ai_client, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking, tests/test_ai_client_cli.py:test_ai_client_send_gemini_cli, tests/test_api_events.py:test_send_emits_events_proper, tests/test_api_events.py:test_send_emits_tool_events, tests/test_deepseek_provider.py:test_deepseek_completion_logic, tests/test_deepseek_provider.py:test_deepseek_model_selection, tests/test_deepseek_provider.py:test_deepseek_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoner_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoning_logic, tests/test_deepseek_provider.py:test_deepseek_streaming, tests/test_deepseek_provider.py:test_deepseek_tool_calling, tests/test_gemini_cli_edge_cases.py:test_gemini_cli_loop_termination, tests/test_gemini_cli_integration.py:test_gemini_cli_full_integration, tests/test_gemini_cli_integration.py:test_gemini_cli_rejection_and_history, tests/test_gemini_cli_parity_regression.py:test_send_invokes_adapter_send, tests/test_gui2_mcp.py:test_mcp_tool_call_is_dispatched, tests/test_minimax_provider.py:test_minimax_default_model, tests/test_minimax_provider.py:test_minimax_model_selection, tests/test_mma_agent_focus_phase1.py:test_append_comms_has_source_tier_key, tests/test_rag_integration.py:test_rag_integration, tests/test_tier4_interceptor.py:test_ai_client_passes_qa_callback, tests/test_tier4_interceptor.py:test_gemini_provider_passes_qa_callback_to_run_script, tests/test_token_usage.py:test_token_usage_tracking]
|
||||
"""
|
||||
global _provider, _model
|
||||
_provider = provider
|
||||
if not validate:
|
||||
_model = model
|
||||
return
|
||||
if provider == "gemini_cli":
|
||||
valid_models = _list_gemini_cli_models()
|
||||
if model != "mock" and (model not in valid_models or model.startswith("deepseek")):
|
||||
|
||||
+43
-4
@@ -801,7 +801,7 @@ class AppController:
|
||||
Owns the application state and manages background services.
|
||||
"""
|
||||
|
||||
def __init__(self, log_to_stderr: bool = True):
|
||||
def __init__(self, defer_warmup: bool = False, log_to_stderr: Optional[bool] = None):
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
@@ -823,10 +823,22 @@ class AppController:
|
||||
|
||||
# --- Shared background pool + proactive warmup (startup_speedup_20260606) ---
|
||||
self._io_pool = make_io_pool()
|
||||
# Warmup progress is a diagnostic; keep stderr quiet unless explicitly asked.
|
||||
# Explicit log_to_stderr arg wins; otherwise default to the SLOP_WARMUP_DEBUG env flag.
|
||||
if log_to_stderr is None:
|
||||
log_to_stderr = bool(os.environ.get("SLOP_WARMUP_DEBUG"))
|
||||
self._warmup = WarmupManager(self._io_pool, log_to_stderr=log_to_stderr)
|
||||
# Hook warmup completion to stamp warmup_done_ts for startup_timeline().
|
||||
self._warmup.on_complete(self._on_warmup_complete_for_timeline)
|
||||
self._warmup.submit(self._compute_warmup_list())
|
||||
self._warmup_started: bool = False
|
||||
self._defer_warmup: bool = defer_warmup
|
||||
self._pending_fetch_provider: Optional[str] = None
|
||||
# The desktop GUI defers warmup until the first frame is painted (see
|
||||
# App._gui_func) so the ~2s of heavy SDK C-extension imports don't hold the
|
||||
# GIL while the window and font atlas are being created — that contention is
|
||||
# what made the window slow to appear. Headless/web/tests warm immediately.
|
||||
if not defer_warmup:
|
||||
self.start_warmup()
|
||||
|
||||
# --- Internal State ---
|
||||
self._ai_status: str = "idle"
|
||||
@@ -2132,6 +2144,13 @@ class AppController:
|
||||
"""
|
||||
[C: src/gui_2.py:App.run]
|
||||
"""
|
||||
# In the desktop GUI, model listing imports the provider SDKs (the same
|
||||
# ~2s C-extension load warmup pays for). Defer it until the first frame is
|
||||
# painted so it doesn't contend for the GIL during window creation; the
|
||||
# deferred fetch is fired from start_warmup().
|
||||
if self._defer_warmup and not self._warmup_started:
|
||||
self._pending_fetch_provider = provider
|
||||
return
|
||||
self.ai_status = "fetching models..."
|
||||
|
||||
def do_fetch() -> None:
|
||||
@@ -2213,9 +2232,26 @@ class AppController:
|
||||
"""
|
||||
return self._warmup.is_done()
|
||||
|
||||
def start_warmup(self) -> None:
|
||||
"""
|
||||
Submit the heavy-module warmup jobs to the io_pool (idempotent).
|
||||
|
||||
Separated from __init__ so the desktop GUI can call it AFTER the first
|
||||
frame is painted, keeping the ~2s of SDK C-extension imports off the GIL
|
||||
while the window is being created. Safe to call multiple times.
|
||||
"""
|
||||
if self._warmup_started:
|
||||
return
|
||||
self._warmup_started = True
|
||||
self._warmup.submit(self._compute_warmup_list())
|
||||
# Run any model fetch that was deferred while the window was being created.
|
||||
if self._pending_fetch_provider is not None:
|
||||
provider, self._pending_fetch_provider = self._pending_fetch_provider, None
|
||||
self._fetch_models(provider)
|
||||
|
||||
def wait_for_warmup(self, timeout: Optional[float] = None) -> bool:
|
||||
"""
|
||||
|
||||
|
||||
Block until warmup completes. Returns True on done, False on timeout.
|
||||
[SDM: src/app_controller.py:wait_for_warmup]
|
||||
"""
|
||||
@@ -2268,7 +2304,10 @@ class AppController:
|
||||
|
||||
def _init_ai_and_hooks(self, app: Any = None) -> None:
|
||||
from src import api_hooks
|
||||
ai_client.set_provider(self._current_provider, self._current_model)
|
||||
# validate=False: skip the live model-list lookup (network/subprocess +
|
||||
# provider-SDK import) on the main thread during startup. _fetch_models
|
||||
# corrects the model against the live list after the first frame, off-thread.
|
||||
ai_client.set_provider(self._current_provider, self._current_model, validate=False)
|
||||
if self._current_provider == "gemini_cli":
|
||||
if not ai_client._gemini_cli_adapter:
|
||||
ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=self.ui_gemini_cli_path)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# src/bg_shader.py
|
||||
import time
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
from typing import Optional
|
||||
from imgui_bundle import imgui, nanovg as nvg, hello_imgui
|
||||
|
||||
+146
-48
@@ -161,6 +161,75 @@ def truncate_entries(entries: list[dict[str, Any]], max_pairs: int) -> list[dict
|
||||
if count == target: return entries[i:]
|
||||
return entries
|
||||
|
||||
def _detect_refresh_rate_win32() -> float:
|
||||
"""Return the primary display's current refresh rate in Hz, or 0.0 on failure.
|
||||
|
||||
Uses user32.EnumDisplaySettingsW (ENUM_CURRENT_SETTINGS) which reads the value
|
||||
directly from the display driver in microseconds. The previous implementation
|
||||
shelled out to PowerShell + WMI (Get-CimInstance Win32_VideoController), which
|
||||
cost ~350ms on every startup and blocked the first frame.
|
||||
"""
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
class _DEVMODE(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dmDeviceName", wintypes.WCHAR * 32), ("dmSpecVersion", wintypes.WORD),
|
||||
("dmDriverVersion", wintypes.WORD), ("dmSize", wintypes.WORD),
|
||||
("dmDriverExtra", wintypes.WORD), ("dmFields", wintypes.DWORD),
|
||||
("dmStuff", ctypes.c_byte * 16), ("dmColor", wintypes.SHORT),
|
||||
("dmDuplex", wintypes.SHORT), ("dmYResolution", wintypes.SHORT),
|
||||
("dmTTOption", wintypes.SHORT), ("dmCollate", wintypes.SHORT),
|
||||
("dmFormName", wintypes.WCHAR * 32), ("dmLogPixels", wintypes.WORD),
|
||||
("dmBitsPerPel", wintypes.DWORD), ("dmPelsWidth", wintypes.DWORD),
|
||||
("dmPelsHeight", wintypes.DWORD), ("dmDisplayFlags", wintypes.DWORD),
|
||||
("dmDisplayFrequency", wintypes.DWORD), ("dmICMMethod", wintypes.DWORD),
|
||||
("dmICMIntent", wintypes.DWORD), ("dmMediaType", wintypes.DWORD),
|
||||
("dmDitherType", wintypes.DWORD), ("dmReserved1", wintypes.DWORD),
|
||||
("dmReserved2", wintypes.DWORD), ("dmPanningWidth", wintypes.DWORD),
|
||||
("dmPanningHeight", wintypes.DWORD),
|
||||
]
|
||||
dm = _DEVMODE()
|
||||
dm.dmSize = ctypes.sizeof(_DEVMODE)
|
||||
if ctypes.windll.user32.EnumDisplaySettingsW(None, -1, ctypes.byref(dm)):
|
||||
# dmDisplayFrequency is 0 or 1 for "default/hardware" on some drivers.
|
||||
if dm.dmDisplayFrequency > 1:
|
||||
return float(dm.dmDisplayFrequency)
|
||||
except Exception:
|
||||
pass
|
||||
return 0.0
|
||||
|
||||
def _resolve_font_path(font_path: str, assets_dir: Path) -> str:
|
||||
"""Normalize a configured font path to something hello_imgui can load.
|
||||
|
||||
hello_imgui resolves relative paths against the assets folder. A config may
|
||||
carry a stale ABSOLUTE path from a different project checkout (e.g.
|
||||
C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf after the repo
|
||||
moved to C:/projects/sloppy). In that case the absolute file does not exist
|
||||
and the load fails. This recovers by:
|
||||
|
||||
1. If the absolute path lives under the current assets folder -> relativize.
|
||||
2. If the absolute path exists on disk as-is -> keep it.
|
||||
3. Otherwise recover the basename under assets/fonts or assets.
|
||||
4. Final fallback: the bundled default Inter font.
|
||||
"""
|
||||
p = Path(font_path)
|
||||
if not p.is_absolute():
|
||||
return font_path # already relative; hello_imgui searches the assets folder
|
||||
try:
|
||||
if p.is_relative_to(assets_dir):
|
||||
return str(p.relative_to(assets_dir)).replace("\\", "/")
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
if p.exists():
|
||||
return font_path # absolute path to a real file elsewhere — load directly
|
||||
# Stale absolute path: recover by basename relative to the assets folder.
|
||||
name = p.name
|
||||
for rel in (f"fonts/{name}", name):
|
||||
if (assets_dir / rel).exists():
|
||||
return rel
|
||||
return "fonts/Inter-Regular.ttf"
|
||||
|
||||
class App:
|
||||
"""The main ImGui interface orchestrator for Manual Slop."""
|
||||
|
||||
@@ -172,7 +241,7 @@ class App:
|
||||
# --- Core Dependencies & State ---
|
||||
from src.startup_profiler import startup_profiler
|
||||
with startup_profiler.phase("app_init_AppController"):
|
||||
self.controller = app_controller.AppController()
|
||||
self.controller = app_controller.AppController(defer_warmup=True)
|
||||
self.controller._app = self
|
||||
with startup_profiler.phase("app_init_history_perfmon"):
|
||||
from src import history, performance_monitor
|
||||
@@ -187,7 +256,8 @@ class App:
|
||||
# --- Command Palette ---
|
||||
self.show_command_palette: bool = False
|
||||
# --- Initialization ---
|
||||
self.controller.init_state()
|
||||
with startup_profiler.phase("app_init_state"):
|
||||
self.controller.init_state()
|
||||
from src.hot_reloader import HotReloader, HotModule
|
||||
if 'src.gui_2' not in HotReloader.HOT_MODULES:
|
||||
HotReloader.register(HotModule(
|
||||
@@ -196,11 +266,13 @@ class App:
|
||||
state_keys=['active_discussion', 'show_windows', 'ui_file_paths', 'ui_screenshot_paths', 'disc_entries', 'disc_roles'],
|
||||
delegation_targets=['_render_main_interface', '_render_discussion_hub', '_render_files_and_media', '_render_ai_settings_hub', '_render_operations_hub', '_render_mma_dashboard']
|
||||
))
|
||||
self.workspace_manager = workspace_manager.WorkspaceManager(project_root=self.controller.active_project_root)
|
||||
self.disc_entries = self.controller.disc_entries
|
||||
self.disc_roles = self.controller.disc_roles
|
||||
self.workspace_profiles = self.workspace_manager.load_all_profiles()
|
||||
self.controller.start_services(self)
|
||||
with startup_profiler.phase("app_init_workspace"):
|
||||
self.workspace_manager = workspace_manager.WorkspaceManager(project_root=self.controller.active_project_root)
|
||||
self.disc_entries = self.controller.disc_entries
|
||||
self.disc_roles = self.controller.disc_roles
|
||||
self.workspace_profiles = self.workspace_manager.load_all_profiles()
|
||||
with startup_profiler.phase("app_init_start_services"):
|
||||
self.controller.start_services(self)
|
||||
# --- Controller Callbacks & Actions ---
|
||||
self.controller._predefined_callbacks['save_context_preset'] = self.save_context_preset
|
||||
self.controller._predefined_callbacks['load_context_preset'] = self.load_context_preset
|
||||
@@ -457,16 +529,13 @@ class App:
|
||||
user_scale = theme.get_current_scale()
|
||||
self.runner_params.dpi_aware_params.dpi_window_size_factor = user_scale
|
||||
|
||||
# Detect Monitor Refresh Rate for capping (Win32 only)
|
||||
# Detect Monitor Refresh Rate for capping (Win32 only).
|
||||
# Uses the native EnumDisplaySettings call (~0.3ms) instead of spawning a
|
||||
# PowerShell/WMI subprocess (~350ms) so the first frame is not blocked.
|
||||
fps_cap = 60.0
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
# Use PowerShell to get max refresh rate across all controllers
|
||||
cmd = "powershell -NoProfile -Command \"Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty CurrentRefreshRate\""
|
||||
out = subprocess.check_output(cmd, shell=True).decode().splitlines()
|
||||
rates = [float(r.strip()) for r in out if r.strip().isdigit()]
|
||||
if rates: fps_cap = max(rates)
|
||||
except Exception: pass
|
||||
rate = _detect_refresh_rate_win32()
|
||||
if rate: fps_cap = rate
|
||||
|
||||
# Enable idling with monitor refresh rate to effectively cap FPS
|
||||
self.runner_params.fps_idling.enable_idling = True
|
||||
@@ -476,11 +545,17 @@ class App:
|
||||
self.runner_params.imgui_window_params.show_menu_view_themes = True
|
||||
self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder
|
||||
self.runner_params.ini_filename = "manualslop_layout.ini"
|
||||
def _profiled_setup_style() -> None:
|
||||
with startup_profiler.phase("setup_imgui_style"):
|
||||
theme.apply_current()
|
||||
def _profiled_post_init() -> None:
|
||||
with startup_profiler.phase("post_init"):
|
||||
self._post_init()
|
||||
self.runner_params.callbacks.show_gui = self._gui_func
|
||||
self.runner_params.callbacks.show_menus = self._show_menus
|
||||
self.runner_params.callbacks.load_additional_fonts = self._load_fonts
|
||||
self.runner_params.callbacks.setup_imgui_style = theme.apply_current
|
||||
self.runner_params.callbacks.post_init = self._post_init
|
||||
self.runner_params.callbacks.setup_imgui_style = _profiled_setup_style
|
||||
self.runner_params.callbacks.post_init = _profiled_post_init
|
||||
self._fetch_models(self.current_provider)
|
||||
md_options = markdown_helper.get_renderer().options
|
||||
immapp.run(self.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=md_options))
|
||||
@@ -489,42 +564,39 @@ class App:
|
||||
session_logger.close_session()
|
||||
|
||||
def _load_fonts(self) -> None:
|
||||
# Set hello_imgui assets folder to the actual absolute path
|
||||
assets_dir = Path(__file__).parent.parent / "assets"
|
||||
if assets_dir.exists():
|
||||
hello_imgui.set_assets_folder(str(assets_dir.absolute()))
|
||||
from src.startup_profiler import startup_profiler
|
||||
with startup_profiler.phase("load_fonts"):
|
||||
# Set hello_imgui assets folder to the actual absolute path
|
||||
assets_dir = Path(__file__).parent.parent / "assets"
|
||||
if assets_dir.exists():
|
||||
hello_imgui.set_assets_folder(str(assets_dir.absolute()))
|
||||
|
||||
# Improved font rendering with oversampling
|
||||
config = imgui.ImFontConfig()
|
||||
config.oversample_h = 3
|
||||
config.oversample_v = 3
|
||||
# Improved font rendering with oversampling
|
||||
config = imgui.ImFontConfig()
|
||||
config.oversample_h = 3
|
||||
config.oversample_v = 3
|
||||
|
||||
font_path, font_size = theme.get_font_loading_params()
|
||||
|
||||
if font_path:
|
||||
p = Path(font_path)
|
||||
if p.is_absolute():
|
||||
font_path, font_size = theme.get_font_loading_params()
|
||||
|
||||
if font_path:
|
||||
font_path = _resolve_font_path(font_path, assets_dir)
|
||||
# Just try loading it directly; hello_imgui will look in the assets folder
|
||||
try:
|
||||
if p.is_relative_to(assets_dir):
|
||||
font_path = str(p.relative_to(assets_dir)).replace("\\", "/")
|
||||
except (ValueError, AttributeError):
|
||||
pass # Fallback to original font_path if relative_to fails or on old Python
|
||||
|
||||
# Just try loading it directly; hello_imgui will look in the assets folder
|
||||
try:
|
||||
self.main_font = hello_imgui.load_font_ttf_with_font_awesome_icons(font_path, font_size, config)
|
||||
except Exception as e:
|
||||
print(f"Failed to load main font {font_path}: {e}")
|
||||
with startup_profiler.phase("load_fonts.main_with_fontawesome"):
|
||||
self.main_font = hello_imgui.load_font_ttf_with_font_awesome_icons(font_path, font_size, config)
|
||||
except Exception as e:
|
||||
print(f"Failed to load main font {font_path}: {e}")
|
||||
self.main_font = None
|
||||
else:
|
||||
self.main_font = None
|
||||
else:
|
||||
self.main_font = None
|
||||
|
||||
try:
|
||||
params = hello_imgui.FontLoadingParams(font_config=config)
|
||||
self.mono_font = hello_imgui.load_font("fonts/MapleMono-Regular.ttf", font_size, params)
|
||||
except Exception as e:
|
||||
print(f"Failed to load mono font: {e}")
|
||||
self.mono_font = None
|
||||
try:
|
||||
with startup_profiler.phase("load_fonts.mono"):
|
||||
params = hello_imgui.FontLoadingParams(font_config=config)
|
||||
self.mono_font = hello_imgui.load_font("fonts/MapleMono-Regular.ttf", font_size, params)
|
||||
except Exception as e:
|
||||
print(f"Failed to load mono font: {e}")
|
||||
self.mono_font = None
|
||||
|
||||
def _handle_approve_mma_step(self, user_data=None) -> None:
|
||||
"""UI-level wrapper for approving a pending MMA step."""
|
||||
@@ -817,6 +889,32 @@ class App:
|
||||
# ---------------------------------------------------------------- gui
|
||||
|
||||
def _gui_func(self) -> None:
|
||||
# One-shot: log when immapp first hands control to our render callback. The
|
||||
# span init -> here is window/GL/context creation + the font/style/post_init
|
||||
# callbacks (all opaque C++); the span here -> mark_first_frame_rendered is
|
||||
# the cost of the first full-UI render. Splitting them attributes the gap.
|
||||
if not getattr(self, "_gui_func_entered", False):
|
||||
self._gui_func_entered = True
|
||||
try:
|
||||
init_ts = getattr(self.controller, "_init_start_ts", None)
|
||||
if init_ts is not None:
|
||||
sys.stderr.write(f"[startup] first _gui_func entry at {(time.time() - init_ts) * 1000:.1f}ms after init (window/GL + font/style/post_init callbacks done)\n")
|
||||
sys.stderr.flush()
|
||||
except Exception: pass
|
||||
|
||||
# One-shot: kick off the controller's heavy-module warmup on the shared
|
||||
# io_pool once the FIRST frame has actually been painted. Waiting one frame
|
||||
# keeps the ~2s of SDK C-extension imports from holding the GIL during
|
||||
# window creation and font-atlas building, so the window appears at full
|
||||
# speed; the SDKs are then warmed by the time the user sends their first
|
||||
# message. start_warmup() is idempotent.
|
||||
if not getattr(self, "_preload_started", False):
|
||||
if getattr(self, "_first_frame_painted", False):
|
||||
self.controller.start_warmup()
|
||||
self._preload_started = True
|
||||
else:
|
||||
self._first_frame_painted = True
|
||||
|
||||
io = imgui.get_io()
|
||||
if io.key_ctrl and io.key_alt and imgui.is_key_down(imgui.Key.r):
|
||||
self._trigger_hot_reload()
|
||||
|
||||
Reference in New Issue
Block a user