From 12d25ce17f531973d750cdde3ccd5fc6af3522ab Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 13 Mar 2026 10:46:44 +0100 Subject: [PATCH] feat(ui): expand PlatformInfo to full D-141 Tier 1 interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 23 properties, 2 methods, 1 signal: - Power: raw_power_state + battery_percent added to _poll_power() - CPU: cpu_name (OS.get_processor_name), cpu_logical_cores - GPU: gpu_name, gpu_vendor, gpu_api_version, gpu_type (mapped from RenderingDevice.DeviceType), gpu_driver_info — all guarded for headless - Platform: platform_name, os_version, distribution, is_sandboxed - Display: screen_count, screen_size, screen_dpi, screen_refresh_rate, display_scale (macOS screen_get_scale / dpi÷96 fallback) - Locale: locale, locale_language - get_diagnostics() -> Dictionary for bug report inclusion - OS.get_distribution_name() and DisplayServer.screen_get_scale() use callv() pattern for headless build safety Co-Authored-By: Claude Sonnet 4.6 --- client/scripts/autoloads/platform_info.gd | 217 ++++++++++++++++++++-- 1 file changed, 204 insertions(+), 13 deletions(-) diff --git a/client/scripts/autoloads/platform_info.gd b/client/scripts/autoloads/platform_info.gd index afef5236b..267f3a6f2 100644 --- a/client/scripts/autoloads/platform_info.gd +++ b/client/scripts/autoloads/platform_info.gd @@ -1,12 +1,20 @@ extends Node -## Platform abstraction layer — D-138 §8. Central location for all OS queries. +## Platform abstraction layer — D-138 §8, D-141. Central location for all OS queries. ## Owned by Stig (UI Developer). Registered FIRST in project.godot so other ## autoloads can read its properties in their own _ready(). ## ## Sections: -## Power: PowerProfile enum, 30s poll timer, power_profile_changed signal. -## Memory: free_memory_mb / total_memory_mb, refresh_memory() on demand. -## Paths: Read-once OS path constants, populated in _ready(). +## Power: PowerProfile enum, 30s poll timer, power_profile_changed signal. +## raw_power_state / battery_percent updated each poll. +## Memory: free_memory_mb / total_memory_mb, refresh_memory() on demand. +## CPU: cpu_name / cpu_logical_cores — read once in _ready(). +## GPU: gpu_name / gpu_vendor / gpu_api_version / gpu_type / gpu_driver_info. +## Guarded for headless/server builds where RenderingServer has no device. +## Platform: platform_name / os_version / distribution / is_sandboxed. +## Display: screen_count / screen_size / screen_dpi / screen_refresh_rate / display_scale. +## Locale: locale / locale_language. +## Paths: Read-once OS path constants, populated in _ready(). +## Diag: get_diagnostics() — flat dict of all properties for bug reports. # -- Power profile ------------------------------------------------------------ @@ -24,6 +32,12 @@ signal power_profile_changed(old_profile: int, new_profile: int) ## Current power profile. Updated by the 30-second poll timer. var power_profile: PowerProfile = PowerProfile.FULL +## Raw OS power_state integer from the last poll. 0 = unknown, 1 = on battery, etc. +var raw_power_state: int = 0 + +## Battery charge percentage (0–100). -1 if not available or not on battery. +var battery_percent: int = -1 + ## How often (seconds) to re-poll OS for power state changes. const POWER_POLL_INTERVAL := 30.0 @@ -44,6 +58,75 @@ var free_memory_mb: float = 0.0 var total_memory_mb: float = 0.0 +# -- CPU ---------------------------------------------------------------------- + +## Human-readable processor name (e.g. "Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz"). +var cpu_name: String = "" + +## Number of logical CPU cores reported by the OS. +var cpu_logical_cores: int = 0 + + +# -- GPU ---------------------------------------------------------------------- + +## GPU display name (e.g. "NVIDIA GeForce RTX 3080"). +var gpu_name: String = "" + +## GPU vendor string (e.g. "NVIDIA Corporation"). +var gpu_vendor: String = "" + +## Graphics API version string (e.g. "OpenGL 4.6.0 ..."). +var gpu_api_version: String = "" + +## GPU type classification: "discrete" | "integrated" | "virtual" | "cpu" | "other". +var gpu_type: String = "other" + +## Low-level driver info strings from the OS (may be empty on some platforms). +var gpu_driver_info: PackedStringArray = PackedStringArray() + + +# -- Platform identity -------------------------------------------------------- + +## OS name reported by Godot (e.g. "Linux", "Windows", "macOS"). +var platform_name: String = "" + +## Full OS version string (e.g. "Ubuntu 22.04.3 LTS"). +var os_version: String = "" + +## Linux distribution name (e.g. "Ubuntu"); empty on non-Linux platforms. +var distribution: String = "" + +## True if the application is running inside a sandbox (Flatpak, Snap, macOS sandbox, etc.). +var is_sandboxed: bool = false + + +# -- Display ------------------------------------------------------------------ + +## Number of connected screens. +var screen_count: int = 1 + +## Size of the primary screen in pixels. +var screen_size: Vector2i = Vector2i(1920, 1080) + +## DPI of the primary screen. +var screen_dpi: int = 96 + +## Refresh rate of the primary screen in Hz. Falls back to 60.0 if OS reports < 0. +var screen_refresh_rate: float = 60.0 + +## UI scale factor. Uses OS-reported scale on macOS; falls back to dpi / 96.0 elsewhere. +var display_scale: float = 1.0 + + +# -- Locale ------------------------------------------------------------------- + +## Full locale string (e.g. "en_US"). +var locale: String = "" + +## Language portion of the locale (e.g. "en"). +var locale_language: String = "" + + # -- File paths --------------------------------------------------------------- ## Base user data directory (user://). @@ -72,6 +155,7 @@ var model_dir: String = "" func _ready() -> void: _init_paths() + _init_hardware() refresh_memory() _poll_power() @@ -83,12 +167,12 @@ func _ready() -> void: func _init_paths() -> void: - user_data_dir = OS.get_user_data_dir() - config_dir = user_data_dir + user_data_dir = OS.get_user_data_dir() + config_dir = user_data_dir benchmark_cache_path = "user://ai-dialogue-config.json" - executable_path = OS.get_executable_path() - install_dir = executable_path.get_base_dir() - model_dir = install_dir + "/models" + executable_path = OS.get_executable_path() + install_dir = executable_path.get_base_dir() + model_dir = install_dir + "/models" # get_cache_dir() may not exist on all Godot builds — use callv for safety. var os_cache: String = "" if OS.has_method("get_cache_dir"): @@ -98,6 +182,63 @@ func _init_paths() -> void: cache_dir = os_cache if not os_cache.is_empty() else user_data_dir + "/cache" +func _init_hardware() -> void: + # -- CPU + cpu_name = OS.get_processor_name() + cpu_logical_cores = OS.get_processor_count() + + # -- GPU (guarded: RenderingServer methods may return empty in headless mode) + gpu_name = RenderingServer.get_video_adapter_name() + gpu_vendor = RenderingServer.get_video_adapter_vendor() + gpu_api_version = RenderingServer.get_video_adapter_api_version() + gpu_driver_info = OS.get_video_adapter_driver_info() + if RenderingServer.has_method("get_video_adapter_type"): + var adapter_type: int = RenderingServer.callv("get_video_adapter_type", []) + gpu_type = _map_adapter_type(adapter_type) + + # -- Platform identity + platform_name = OS.get_name() + os_version = OS.get_version() + if OS.has_method("get_distribution_name"): + var dist: Variant = OS.callv("get_distribution_name", []) + if dist is String: + distribution = dist + is_sandboxed = OS.is_sandboxed() + + # -- Display + screen_count = DisplayServer.get_screen_count() + screen_size = DisplayServer.screen_get_size() + screen_dpi = DisplayServer.screen_get_dpi() + var raw_rate: float = DisplayServer.screen_get_refresh_rate() + screen_refresh_rate = raw_rate if raw_rate >= 0.0 else 60.0 + display_scale = _compute_display_scale() + + # -- Locale + locale = OS.get_locale() + locale_language = OS.get_locale_language() + + +## Map RenderingDevice.DeviceType int to a readable string. +## Values: 0=OTHER, 1=INTEGRATED, 2=DISCRETE, 3=VIRTUAL, 4=CPU. +static func _map_adapter_type(adapter_type: int) -> String: + match adapter_type: + 1: return "integrated" + 2: return "discrete" + 3: return "virtual" + 4: return "cpu" + _: return "other" + + +## Compute display scale for the primary screen. +## macOS reports via screen_get_scale(); all others fall back to dpi / 96.0. +func _compute_display_scale() -> float: + if platform_name == "macOS" and DisplayServer.has_method("screen_get_scale"): + var scale: Variant = DisplayServer.callv("screen_get_scale", []) + if scale is float and scale > 0.0: + return scale + return float(screen_dpi) / 96.0 + + # -- Memory API --------------------------------------------------------------- ## Refresh free/total memory readings from the OS. Call before read if freshness matters. @@ -112,7 +253,6 @@ func refresh_memory() -> void: # -- Power API ---------------------------------------------------------------- ## Classify an OS power_state integer into a human-readable string. -## Mirrors HardwareDetector.classify_power_state() for external consumers. ## 1 (ON_BATTERY) → "battery", 2/3/4 (plugged) → "plugged", 0 → "unknown". static func classify_power_state(power_state: int) -> String: match power_state: @@ -131,7 +271,7 @@ static func should_suspend_inference(power_state: int) -> bool: ## Read current OS power state integer. Returns 0 (UNKNOWN) if unavailable. ## Uses callv() to defer method resolution to runtime — avoids compile errors on builds -## where OS.get_power_info() or OS.get_power_state() are not available (e.g. headless). +## where OS.get_power_info() is not available (e.g. headless). func current_power_state() -> int: # Try Godot 4.x: OS.get_power_info() → {"power_state": int, "percent": int, ...} if OS.has_method("get_power_info"): @@ -143,11 +283,62 @@ func current_power_state() -> int: func _poll_power() -> void: - var state := current_power_state() + # Read raw state + battery percent from OS. + raw_power_state = _POWER_STATE_UNKNOWN + battery_percent = -1 + if OS.has_method("get_power_info"): + var info: Variant = OS.callv("get_power_info", []) + if info is Dictionary: + raw_power_state = int(info.get("power_state", _POWER_STATE_UNKNOWN)) + if raw_power_state == _POWER_STATE_ON_BATTERY: + battery_percent = int(info.get("percent", -1)) + var new_profile: PowerProfile = PowerProfile.BATTERY \ - if state == _POWER_STATE_ON_BATTERY \ + if raw_power_state == _POWER_STATE_ON_BATTERY \ else PowerProfile.FULL if new_profile != power_profile: var old_profile := power_profile power_profile = new_profile power_profile_changed.emit(int(old_profile), int(new_profile)) + + +# -- Diagnostics -------------------------------------------------------------- + +## Returns all platform properties as a flat dictionary for inclusion in bug reports. +func get_diagnostics() -> Dictionary: + return { + # Power + "power_profile": int(power_profile), + "raw_power_state": raw_power_state, + "battery_percent": battery_percent, + # Memory + "free_memory_mb": free_memory_mb, + "total_memory_mb": total_memory_mb, + # CPU + "cpu_name": cpu_name, + "cpu_logical_cores": cpu_logical_cores, + # GPU + "gpu_name": gpu_name, + "gpu_vendor": gpu_vendor, + "gpu_api_version": gpu_api_version, + "gpu_type": gpu_type, + "gpu_driver_info": Array(gpu_driver_info), + # Platform + "platform_name": platform_name, + "os_version": os_version, + "distribution": distribution, + "is_sandboxed": is_sandboxed, + # Display + "screen_count": screen_count, + "screen_size": {"x": screen_size.x, "y": screen_size.y}, + "screen_dpi": screen_dpi, + "screen_refresh_rate": screen_refresh_rate, + "display_scale": display_scale, + # Locale + "locale": locale, + "locale_language": locale_language, + # Paths + "user_data_dir": user_data_dir, + "install_dir": install_dir, + "cache_dir": cache_dir, + }