diff --git a/.gitignore b/.gitignore index f3537de..3dbfbf8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ /dist/ /build/ /.github/ +*.idea +.DS_Store +venv/ diff --git a/src/api/api_server.py b/src/api/api_server.py index 878dd57..107a27f 100755 --- a/src/api/api_server.py +++ b/src/api/api_server.py @@ -24,7 +24,7 @@ from src.engines.engine_manager import EngineManager from src.render_queue import RenderQueue, JobNotFoundError from src.utilities.config import Config from src.utilities.misc_helper import system_safe_path, current_system_os, current_system_cpu, \ - current_system_os_version, num_to_alphanumeric + current_system_os_version, num_to_alphanumeric, get_gpu_info from src.utilities.status_utils import string_to_status from src.version import APP_VERSION diff --git a/src/init.py b/src/init.py index eab1b5e..68d9fce 100644 --- a/src/init.py +++ b/src/init.py @@ -7,8 +7,9 @@ import threading from collections import deque import cpuinfo +import psutil -from api.api_server import API_VERSION +from src.api.api_server import API_VERSION from src.api.api_server import start_server from src.api.preview_manager import PreviewManager from src.api.serverproxy_manager import ServerProxyManager @@ -16,7 +17,7 @@ from src.distributed_job_manager import DistributedJobManager from src.engines.engine_manager import EngineManager from src.render_queue import RenderQueue from src.utilities.config import Config -from src.utilities.misc_helper import (system_safe_path, current_system_cpu, current_system_os, +from src.utilities.misc_helper import (get_gpu_info, system_safe_path, current_system_cpu, current_system_os, current_system_os_version, check_for_updates) from src.utilities.zeroconf_server import ZeroconfServer from src.version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER @@ -115,6 +116,8 @@ def run(server_only=False) -> int: 'system_cpu_cores': multiprocessing.cpu_count(), 'system_os': current_system_os(), 'system_os_version': current_system_os_version(), + 'system_memory': round(psutil.virtual_memory().total / (1024**3)), # in GB + 'gpu_info': get_gpu_info(), 'api_version': API_VERSION} ZeroconfServer.start() logger.info(f"{APP_NAME} Render Server started - Hostname: {local_hostname}") diff --git a/src/ui/main_window.py b/src/ui/main_window.py index 787b3f3..53415a5 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -1,6 +1,8 @@ ''' app/ui/main_window.py ''' +import ast import datetime import io +import json import logging import os import sys @@ -15,7 +17,7 @@ from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QListWidget, QTab QTableWidgetItem, QLabel, QVBoxLayout, QHeaderView, QMessageBox, QGroupBox, QPushButton, QListWidgetItem, \ QFileDialog -from api.api_server import API_VERSION +from src.api.api_server import API_VERSION from src.render_queue import RenderQueue from src.utilities.misc_helper import get_time_elapsed, resources_dir, is_localhost from src.utilities.status_utils import RenderStatus @@ -54,6 +56,7 @@ class MainWindow(QMainWindow): self.server_info_ram = None self.server_info_cpu = None self.server_info_os = None + self.server_info_gpu = None self.server_info_hostname = None self.engine_browser_window = None self.server_info_group = None @@ -122,6 +125,7 @@ class MainWindow(QMainWindow): self.server_info_os = QLabel() self.server_info_cpu = QLabel() self.server_info_ram = QLabel() + self.server_info_gpu = QLabel() server_info_engines_button = QPushButton("Render Engines") server_info_engines_button.clicked.connect(self.engine_browser) server_info_layout = QVBoxLayout() @@ -129,6 +133,7 @@ class MainWindow(QMainWindow): server_info_layout.addWidget(self.server_info_os) server_info_layout.addWidget(self.server_info_cpu) server_info_layout.addWidget(self.server_info_ram) + server_info_layout.addWidget(self.server_info_gpu) server_info_layout.addWidget(server_info_engines_button) server_info_group.setLayout(server_info_layout) @@ -238,15 +243,42 @@ class MainWindow(QMainWindow): def update_server_info_display(self, hostname): """Updates the server information section of the UI.""" - self.server_info_hostname.setText(hostname or "unknown") + self.server_info_hostname.setText(f"Name: {hostname}") server_info = ZeroconfServer.get_hostname_properties(hostname) # Use the get method with defaults to avoid KeyError os_info = f"OS: {server_info.get('system_os', 'Unknown')} {server_info.get('system_os_version', '')}" - cpu_info = f"CPU: {server_info.get('system_cpu_brand', 'Unknown')} ({server_info.get('system_cpu_cores', 'Unknown')} cores)" + cleaned_cpu_name = server_info.get('system_cpu_brand', 'Unknown').replace(' CPU','').replace('(TM)','').replace('(R)', '') + cpu_info = f"CPU: {cleaned_cpu_name} ({server_info.get('system_cpu_cores', 'Unknown')} cores)" + memory_info = f"RAM: {server_info.get('system_memory', 'Unknown')} GB" + + # Get and format GPU info + try: + gpu_list = ast.literal_eval(server_info.get('gpu_info', [])) + + # Format all GPUs + gpu_info_parts = [] + for gpu in gpu_list: + gpu_name = gpu.get('name', 'Unknown').replace('(TM)','').replace('(R)', '') + gpu_memory = gpu.get('memory', 'Unknown') + + # Add " GB" suffix if memory is a number + if isinstance(gpu_memory, (int, float)) or (isinstance(gpu_memory, str) and gpu_memory.isdigit()): + gpu_memory_str = f"{gpu_memory} GB" + else: + gpu_memory_str = str(gpu_memory) + + gpu_info_parts.append(f"{gpu_name} ({gpu_memory_str})") + + gpu_info = f"GPU: {', '.join(gpu_info_parts)}" if gpu_info_parts else "GPU: Unknown" + except Exception as e: + logger.error(f"Error parsing GPU info: {e}") + gpu_info = "GPU: Unknown" self.server_info_os.setText(os_info.strip()) self.server_info_cpu.setText(cpu_info) + self.server_info_ram.setText(memory_info) + self.server_info_gpu.setText(gpu_info) def fetch_jobs(self, clear_table=False): diff --git a/src/utilities/misc_helper.py b/src/utilities/misc_helper.py index f7fa4ef..f433e74 100644 --- a/src/utilities/misc_helper.py +++ b/src/utilities/misc_helper.py @@ -1,6 +1,8 @@ import logging +import json import os import platform +import re import shutil import socket import string @@ -225,3 +227,121 @@ def iso_datestring_to_formatted_datestring(iso_date_string): formatted_date = date_local.strftime('%Y-%m-%d %I:%M %p') return formatted_date + +def get_gpu_info(): + """Cross-platform GPU information retrieval""" + + def get_windows_gpu_info(): + """Get GPU info on Windows""" + try: + result = subprocess.run( + ['wmic', 'path', 'win32_videocontroller', 'get', 'name,AdapterRAM', '/format:list'], + capture_output=True, text=True, timeout=5 + ) + + # Virtual adapters to exclude + virtual_adapters = [ + 'virtual', 'rdp', 'hyper-v', 'microsoft basic', 'basic display', + 'vga compatible', 'dummy', 'nvfbc', 'nvencode' + ] + + gpus = [] + current_gpu = None + + for line in result.stdout.strip().split('\n'): + line = line.strip() + if not line: + continue + + if line.startswith('Name='): + if current_gpu and current_gpu.get('name'): + gpus.append(current_gpu) + gpu_name = line.replace('Name=', '').strip() + + # Skip virtual adapters + if any(virtual in gpu_name.lower() for virtual in virtual_adapters): + current_gpu = None + else: + current_gpu = {'name': gpu_name, 'memory': 'Integrated'} + + elif line.startswith('AdapterRAM=') and current_gpu: + vram_bytes_str = line.replace('AdapterRAM=', '').strip() + if vram_bytes_str and vram_bytes_str != '0': + try: + vram_gb = int(vram_bytes_str) / (1024**3) + current_gpu['memory'] = round(vram_gb, 2) + except: + pass + + if current_gpu and current_gpu.get('name'): + gpus.append(current_gpu) + + return gpus if gpus else [{'name': 'Unknown GPU', 'memory': 'Unknown'}] + except Exception as e: + logger.error(f"Failed to get Windows GPU info: {e}") + return [{'name': 'Unknown GPU', 'memory': 'Unknown'}] + + def get_macos_gpu_info(): + """Get GPU info on macOS (works with Apple Silicon)""" + try: + result = subprocess.run(['system_profiler', 'SPDisplaysDataType', '-json'], + capture_output=True, text=True, timeout=5) + data = json.loads(result.stdout) + + gpus = [] + displays = data.get('SPDisplaysDataType', []) + for display in displays: + if 'sppci_model' in display: + gpus.append({ + 'name': display.get('sppci_model', 'Unknown GPU'), + 'memory': display.get('sppci_vram', 'Integrated'), + }) + return gpus if gpus else [{'name': 'Apple Silicon GPU', 'memory': 'Integrated'}] + except Exception as e: + print(f"Failed to get macOS GPU info: {e}") + return [{'name': 'Unknown GPU', 'memory': 'Unknown'}] + + def get_linux_gpu_info(): + gpus = [] + try: + # Run plain lspci and filter for GPU-related lines + output = subprocess.check_output( + ["lspci"], universal_newlines=True, stderr=subprocess.DEVNULL + ) + for line in output.splitlines(): + if any(keyword in line.lower() for keyword in ["vga", "3d", "display"]): + # Extract the part after the colon (vendor + model) + if ":" in line: + name_part = line.split(":", 1)[1].strip() + # Clean up common extras like (rev xx) or (prog-if ...) + name = name_part.split("(")[0].split("controller:")[-1].strip() + vendor = "Unknown" + if "nvidia" in name.lower(): + vendor = "NVIDIA" + elif "amd" in name.lower() or "ati" in name.lower(): + vendor = "AMD" + elif "intel" in name.lower(): + vendor = "Intel" + + gpus.append({ + "name": name, + "vendor": vendor, + "memory": "Unknown" + }) + except FileNotFoundError: + print("lspci not found. Install pciutils: sudo apt install pciutils") + return [] + except Exception as e: + print(f"Error running lspci: {e}") + return [] + + return gpus + + system = platform.system() + + if system == 'Darwin': # macOS + return get_macos_gpu_info() + elif system == 'Windows': + return get_windows_gpu_info() + else: # Assume Linux or other + return get_linux_gpu_info()