feat(ui): expand PlatformInfo to full D-141 Tier 1 interface

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 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 10:46:44 +01:00
co-authored by Claude Sonnet 4.6
parent d7a4e3268a
commit 12d25ce17f
+204 -13
View File
@@ -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 (0100). -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,
}