extends Node ## 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. ## 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 ------------------------------------------------------------ ## High-level power classification. POWER_SAVER reserved for future OS API. enum PowerProfile { FULL = 0, # Plugged in (charged, charging, or no battery) — no restrictions BATTERY = 1, # On battery — AI inference should be suspended per D-138 §8 Layer 3 POWER_SAVER = 2 # System-level power-saver mode (future: no cross-platform API yet) } ## Emitted when the detected power profile changes. 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 # OS power_state integer constants (Godot 4 — same values as POWERSTATE_* enum). const _POWER_STATE_UNKNOWN := 0 const _POWER_STATE_ON_BATTERY := 1 const _POWER_STATE_NO_BATTERY := 2 const _POWER_STATE_CHARGING := 3 const _POWER_STATE_CHARGED := 4 # -- Memory ------------------------------------------------------------------- ## Free physical RAM in megabytes. Call refresh_memory() before reading if staleness matters. var free_memory_mb: float = 0.0 ## Total physical RAM in megabytes. Populated once in _ready(); doesn't change at runtime. 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://). var user_data_dir: String = "" ## Directory where settings.cfg is written. Same as user_data_dir in Godot 4. var config_dir: String = "" ## Full path to AI benchmark cache (ai-dialogue-config.json). var benchmark_cache_path: String = "" ## Directory where the game binary lives (install location). var install_dir: String = "" ## Full path to the game executable. var executable_path: String = "" ## OS-provided cache directory, or user_data_dir/cache as fallback. var cache_dir: String = "" ## Directory where the bundled Gemma 2 model lives (derived from install_dir). var model_dir: String = "" # -- Lifecycle ---------------------------------------------------------------- func _ready() -> void: _init_paths() _init_hardware() refresh_memory() _poll_power() var timer := Timer.new() timer.wait_time = POWER_POLL_INTERVAL timer.autostart = true timer.timeout.connect(_poll_power) add_child(timer) func _init_paths() -> void: 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" # 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"): var result: Variant = OS.callv("get_cache_dir", []) if result is String: os_cache = result 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. func refresh_memory() -> void: var mem := OS.get_memory_info() # "available" = Linux MemAvailable / Windows ullAvailPhys (includes reclaimable pages). # Fall back to "free" on platforms that don't provide "available". free_memory_mb = float(mem.get("available", mem.get("free", 0))) / (1024.0 * 1024.0) total_memory_mb = float(mem.get("physical", 0)) / (1024.0 * 1024.0) # -- Power API ---------------------------------------------------------------- ## Classify an OS power_state integer into a human-readable string. ## 1 (ON_BATTERY) → "battery", 2/3/4 (plugged) → "plugged", 0 → "unknown". static func classify_power_state(power_state: int) -> String: match power_state: _POWER_STATE_ON_BATTERY: return "battery" _POWER_STATE_NO_BATTERY, _POWER_STATE_CHARGING, _POWER_STATE_CHARGED: return "plugged" _: return "unknown" ## True only when the OS reports the device is on battery — used by Layer 3 suspend. static func should_suspend_inference(power_state: int) -> bool: return power_state == _POWER_STATE_ON_BATTERY ## 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() 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"): var info: Variant = OS.callv("get_power_info", []) if info is Dictionary: return int(info.get("power_state", _POWER_STATE_UNKNOWN)) # Fallback: unknown power state → treat as FULL (no inference suspension). return _POWER_STATE_UNKNOWN func _poll_power() -> void: # 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 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, "config_dir": config_dir, "benchmark_cache_path": benchmark_cache_path, "install_dir": install_dir, "executable_path": executable_path, "cache_dir": cache_dir, "model_dir": model_dir, }