From 643fd3c61cad5d4290dd4225f898e1f795a9e575 Mon Sep 17 00:00:00 2001 From: imterah Date: Sat, 21 Jun 2025 12:58:41 -0400 Subject: [PATCH] chore: Remove legacy Python codebase --- .gitignore | 291 +++------------------------------ .gitmodules | 9 - libunreal/__init__.py | 10 -- libunreal/edid.py | 86 ---------- libunreal/linux/__init__.py | 84 ---------- libunreal/mcu_driver.py | 145 ---------------- libunreal/supported_devices.py | 13 -- main.py | 188 --------------------- modules/evdi | 1 - modules/nreal-driver | 1 - modules/raylib-python-cffi | 1 - render.py | 235 -------------------------- requirements.txt | 7 - shell.nix | 11 +- 14 files changed, 24 insertions(+), 1058 deletions(-) delete mode 100644 .gitmodules delete mode 100644 libunreal/__init__.py delete mode 100644 libunreal/edid.py delete mode 100644 libunreal/linux/__init__.py delete mode 100644 libunreal/mcu_driver.py delete mode 100644 libunreal/supported_devices.py delete mode 100755 main.py delete mode 160000 modules/evdi delete mode 160000 modules/nreal-driver delete mode 160000 modules/raylib-python-cffi delete mode 100644 render.py delete mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 8e93e62..dcbec21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,281 +1,30 @@ -# ---> Python -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll *.so +*.dylib -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST +# Test binary, built with `go test -c` +*.test -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec +# Output of the go coverage tool, specifically when used with LiteIDE +*.out -# Installer logs -pip-log.txt -pip-delete-this-directory.txt +# Dependency directories (remove the comment below to include it) +# vendor/ -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ +# Go workspace file +go.work +go.work.sum -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock -#poetry.toml - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments +# env file .env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - -# ---> C -# Prerequisites -*.d - -# Object files -*.o -*.ko -*.obj -*.elf - -# Linker output -*.ilk -*.map -*.exp - -# Precompiled Headers -*.gch -*.pch - -# Libraries -*.lib -*.a -*.la -*.lo - -# Shared objects (inc. Windows DLLs) -*.dll -*.so -*.so.* -*.dylib - -# Executables -*.exe -*.out -*.app -*.i*86 -*.x86_64 -*.hex - -# Debug files -*.dSYM/ -*.su -*.idb -*.pdb - -# Kernel Module Compile Results -*.mod* -*.cmd -.tmp_versions/ -modules.order -Module.symvers -Mkfile.old -dkms.conf - -# ---> C++ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app # ---> UnrealXR +# development dirs data -drivers diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 72cd7c1..0000000 --- a/.gitmodules +++ /dev/null @@ -1,9 +0,0 @@ -[submodule "modules/raylib-python-cffi"] - path = modules/raylib-python-cffi - url = https://git.terah.dev/UnrealXR/raylib-python-cffi.git -[submodule "modules/evdi"] - path = modules/evdi - url = https://github.com/DisplayLink/evdi.git -[submodule "modules/nreal-driver"] - path = modules/nreal-driver - url = https://git.terah.dev/UnrealXR/nrealAirLinuxDriver.git diff --git a/libunreal/__init__.py b/libunreal/__init__.py deleted file mode 100644 index c1abd7d..0000000 --- a/libunreal/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from libunreal.supported_devices import * -from libunreal.mcu_driver import * -from libunreal.edid import * - -from sys import platform - -if platform == "linux" or platform == "linux2": - from libunreal.linux import * -else: - raise OSError("Unsupported operating system") diff --git a/libunreal/edid.py b/libunreal/edid.py deleted file mode 100644 index f103585..0000000 --- a/libunreal/edid.py +++ /dev/null @@ -1,86 +0,0 @@ -from loguru import logger - -from dataclasses import dataclass -import uuid - -@dataclass -class UnrealXRDisplayMetadata: - edid: bytes - device_vendor: str - device_quirks: dict[str, str | int] - max_width: int - max_height: int - max_refresh_rate: int - linux_drm_card: str - linux_drm_connector: str - -calculated_msft_payload_size = 22+4 - -def calculate_checksum(block): - checksum = (-sum(block[:-1])) & 0xFF - return checksum - -def patch_edid_to_be_specialized(edid_data: bytes | bytearray) -> bytes | bytearray: - mutable_edid = bytearray(edid_data) - is_enhanced_mode = len(edid_data) > 128 - - found_extension_base = 0 - extension_base_existed = False - - if is_enhanced_mode: - for i in range(128, len(edid_data), 128): - if edid_data[i] == 0x02: - logger.warning("Detected existing ANSI CTA data section. Patching in place but untested! Please report any issues you discover") - - if edid_data[i+1] != 0x03: - logger.warning("Incompatible version detected for ANSI CTA data section in EDID") - - found_extension_base = i - extension_base_existed = True - - if found_extension_base == 0: - found_extension_base = len(edid_data) - mutable_edid.extend([0]*128) - else: - mutable_edid.extend([0]*128) - found_extension_base = 128 - - generated_uuid = uuid.uuid4() - - mutable_edid[found_extension_base] = 0x02 - mutable_edid[found_extension_base+1] = 0x03 - - if extension_base_existed and mutable_edid[found_extension_base+2] != calculated_msft_payload_size and mutable_edid[found_extension_base+2] != 0: - # We try our best to move our data into place - current_base = mutable_edid[found_extension_base+2] - mutable_edid[found_extension_base+2] = calculated_msft_payload_size+1 - - mutable_edid[found_extension_base+4:found_extension_base+current_base-1] = [0]*(current_base-1) - mutable_edid[found_extension_base+calculated_msft_payload_size:found_extension_base+127] = mutable_edid[found_extension_base+current_base:found_extension_base+127] - else: - mutable_edid[found_extension_base+2] = calculated_msft_payload_size - - if not extension_base_existed: - mutable_edid[126] += 1 - mutable_edid[127] = calculate_checksum(mutable_edid[:128]) - - mutable_edid[found_extension_base+3] = 0 # We don't know any of these properties - - # Implemented using https://learn.microsoft.com/en-us/windows-hardware/drivers/display/specialized-monitors-edid-extension - # VST & Length - mutable_edid[found_extension_base+4] = 0x3 << 5 | 0x15 # 0x3: vendor specific tag; 0x15: length - # Assigned IEEE OUI - mutable_edid[found_extension_base+5] = 0x5C - mutable_edid[found_extension_base+6] = 0x12 - mutable_edid[found_extension_base+7] = 0xCA - # Actual data - mutable_edid[found_extension_base+8] = 0x2 # Using version 0x2 for better compatibility - mutable_edid[found_extension_base+9] = 0x7 # Using VR tag for better compatibility even though it probably doesn't matter - mutable_edid[found_extension_base+10:found_extension_base+10+16] = generated_uuid.bytes - - mutable_edid[found_extension_base+127] = calculate_checksum(mutable_edid[found_extension_base:found_extension_base+127]) - - if isinstance(edid_data, bytes): - return bytes(mutable_edid) - else: - return mutable_edid diff --git a/libunreal/linux/__init__.py b/libunreal/linux/__init__.py deleted file mode 100644 index cecdae4..0000000 --- a/libunreal/linux/__init__.py +++ /dev/null @@ -1,84 +0,0 @@ -import subprocess -import os - -from libunreal.supported_devices import supported_devices -from libunreal.edid import UnrealXRDisplayMetadata -import pyedid - -def upload_new_device_edid(display_spec: UnrealXRDisplayMetadata, edid: bytes | bytearray): - pass - -def fetch_xr_glass_edid(allow_unsupported_devices) -> UnrealXRDisplayMetadata: - # Scan for all VGA devices and their IDs - pci_device_comand = subprocess.run(["lspci"], capture_output=True) - - if pci_device_comand.returncode != 0: - raise OSError("Failed to scan PCI devices") - - pci_devices: list[str] = pci_device_comand.stdout.decode("utf-8").split("\n") - pci_devices = pci_devices[:-1] - - vga_devices: list[str] = [] - - for pci_device in pci_devices: - if "VGA compatible controller:" in pci_device: - vga_devices.append(pci_device[:pci_device.index(" ")]) - - # Attempt to find any XR glasses - for vga_device in vga_devices: - card_devices = list(os.listdir(f"/sys/devices/pci0000:00/0000:{vga_device}/drm/")) - - for card_device in card_devices: - if "card" not in card_device: - continue - - monitors = list(os.listdir(f"/sys/devices/pci0000:00/0000:{vga_device}/drm/{card_device}/")) - - for monitor in monitors: - if card_device not in monitor: - continue - - with open(f"/sys/devices/pci0000:00/0000:{vga_device}/drm/{card_device}/{monitor}/edid", "rb") as edid: - raw_edid_file = edid.read() - - if len(raw_edid_file) == 0: - continue - - edid = pyedid.parse_edid(raw_edid_file) - - for manufacturer, manufacturer_supported_devices in supported_devices.items(): - if edid.manufacturer_pnp_id == manufacturer and (edid.name in manufacturer_supported_devices or allow_unsupported_devices): - max_width = 0 - max_height = 0 - max_refresh = 0 - - for resolution in edid.resolutions: - if resolution[0] > max_width and resolution[1] > max_height: - max_width = resolution[0] - max_height = resolution[1] - - max_refresh = max(max_refresh, int(resolution[2])) - - if max_width == 0 or max_height == 0: - if "max_width" not in manufacturer_supported_devices[edid.name] or "max_height" not in manufacturer_supported_devices[edid.name]: - raise ValueError("Couldn't determine maximum width and height, and the maximum width and height isn't defined in the device quirks section") - - max_width = int(manufacturer_supported_devices[edid.name]["max_width"]) - max_height = int(manufacturer_supported_devices[edid.name]["max_height"]) - - if max_refresh == 0: - if "max_refresh" not in manufacturer_supported_devices[edid.name]: - raise ValueError("Couldn't determine maximum refresh rate, and the maximum refresh rate isn't defined in the device quirks section") - - max_refresh = int(manufacturer_supported_devices[edid.name]["max_refresh"]) - - return UnrealXRDisplayMetadata(raw_edid_file, edid.manufacturer_pnp_id, manufacturer_supported_devices[edid.name], max_width, max_height, max_refresh, card_device, monitor.replace(f"{card_device}-", "")) - - raise ValueError("Could not find supported device. Check if the device is plugged in. If it is plugged in and working correctly, check the README or open an issue.") - -def upload_edid_firmware(display: UnrealXRDisplayMetadata, fw: bytes | bytearray): - if display.linux_drm_connector == "" or display.linux_drm_card == "": - raise ValueError("Linux DRM connector and/or Linux DRM card not specified!") - - with open(f"/sys/kernel/debug/dri/{display.linux_drm_card.replace("card", "")}/{display.linux_drm_connector}/edid_override", "wb") as kernel_edid: - kernel_edid.write(fw) diff --git a/libunreal/mcu_driver.py b/libunreal/mcu_driver.py deleted file mode 100644 index ec426c5..0000000 --- a/libunreal/mcu_driver.py +++ /dev/null @@ -1,145 +0,0 @@ -from tempfile import TemporaryDirectory -from dataclasses import dataclass -from os import path, environ -from typing import Callable -from loguru import logger -from shutil import which -from time import sleep -from enum import Enum -import subprocess -import threading -import socket -import struct -import signal -import atexit -import os - -class MCUCommandTypes(Enum): - ROLL = 0 - PITCH = 1 - YAW = 2 - GENERIC_MESSAGE = 3 - BRIGHTNESS_UP = 4 - BRIGHTNESS_DOWN = 5 - -@dataclass -class MCUCallbackWrapper: - OnRollUpdate: Callable[[float], None] - OnPitchUpdate: Callable[[float], None] - OnYawUpdate: Callable[[float], None] - OnTextMessageRecieved: Callable[[str], None] - OnBrightnessUp: Callable[[int], None] - OnBrightnessDown: Callable[[int], None] - -vendor_to_driver_table: dict[str, str] = { - "MRG": "xreal_ar_driver", -} - -def find_executable_path_from_driver_name(driver_name) -> str: - # First try the normal driver path - try: - driver_path = path.join("drivers", driver_name) - - file = open(driver_path) - file.close() - - return driver_path - except OSError: - # Then search the system path - driver_path = which(driver_name) - - if driver_path == "": - raise OSError("Could not find driver executable in driver directory or in PATH") - - return driver_path - -def start_mcu_event_listener(driver_vendor: str, events: MCUCallbackWrapper): - driver_executable = find_executable_path_from_driver_name(vendor_to_driver_table[driver_vendor]) - - created_temp_dir = TemporaryDirectory() - sock_path = path.join(created_temp_dir.name, "mcu_socket") - - def on_socket_event(sock: socket.socket): - while True: - message_type = sock.recv(1) - - if message_type[0] == MCUCommandTypes.ROLL.value: - roll_data = sock.recv(4) - roll_value = struct.unpack("!f", roll_data)[0] - - if not isinstance(roll_value, float): - logger.warning("Expected roll value to be a float but got other type instead") - continue - - events.OnRollUpdate(roll_value) - elif message_type[0] == MCUCommandTypes.PITCH.value: - pitch_data = sock.recv(4) - pitch_value = struct.unpack("!f", pitch_data)[0] - - if not isinstance(pitch_value, float): - logger.warning("Expected pitch value to be a float but got other type instead") - continue - - events.OnPitchUpdate(pitch_value) - elif message_type[0] == MCUCommandTypes.YAW.value: - yaw_data = sock.recv(4) - yaw_value = struct.unpack("!f", yaw_data)[0] - - if not isinstance(yaw_value, float): - logger.warning("Expected yaw value to be a float but got other type instead") - continue - - events.OnYawUpdate(yaw_value) - elif message_type[0] == MCUCommandTypes.GENERIC_MESSAGE.value: - length_bytes = sock.recv(4) - - msg_len = struct.unpack("!I", length_bytes)[0] - msg_bytes = sock.recv(msg_len) - - msg = msg_bytes.decode("utf-8", errors="replace") - events.OnTextMessageRecieved(msg) - elif message_type[0] == MCUCommandTypes.BRIGHTNESS_UP.value: - brightness_bytes = sock.recv(1) - events.OnBrightnessUp(int.from_bytes(brightness_bytes, byteorder='big')) - elif message_type[0] == MCUCommandTypes.BRIGHTNESS_DOWN.value: - brightness_bytes = sock.recv(1) - events.OnBrightnessDown(int.from_bytes(brightness_bytes, byteorder='big')) - else: - logger.warning(f"Unknown message type recieved: {str(message_type[0])}") - - def start_socket_handout(): - server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - server.bind(sock_path) - server.listen(1) - - sock: socket.socket | None = None - - while True: - sock, _ = server.accept() - - threaded_connection_processing = threading.Thread(target=on_socket_event, args=(sock,), daemon=True) - threaded_connection_processing.start() - - created_temp_dir.cleanup() - - def spawn_child_process(): - custom_env = environ.copy() - custom_env["UNREALXR_NREAL_DRIVER_SOCK"] = sock_path - - process = subprocess.Popen([driver_executable], env=custom_env) - - def kill_child(): - if process.pid is None: - pass - else: - os.kill(process.pid, signal.SIGTERM) - - atexit.register(kill_child) - - threaded_socket_handout = threading.Thread(target=start_socket_handout, daemon=True) - threaded_socket_handout.start() - - sleep(0.01) # Give the socket server time to initialize - - threaded_child_process = threading.Thread(target=spawn_child_process, daemon=True) - threaded_child_process.start() diff --git a/libunreal/supported_devices.py b/libunreal/supported_devices.py deleted file mode 100644 index 7eafd62..0000000 --- a/libunreal/supported_devices.py +++ /dev/null @@ -1,13 +0,0 @@ -# Sourced from "https://uefi.org/uefi-pnp-export" -supported_devices: dict[str, dict[str, dict[str, str | int]]] = { - "MRG": { - # Quirks section - "Air": { - "max_width": 1920, - "max_height": 1080, - "max_refresh": 120, - "sensor_init_delay": 10, - "z_vector_disabled": True, - } - } -} diff --git a/main.py b/main.py deleted file mode 100755 index ca51df5..0000000 --- a/main.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python3 -from sys import platform -import logging -import atexit -import json -import os - -# Silence pyray init messages -raylib_python_logger = logging.getLogger("raylib") -raylib_python_logger.setLevel(logging.ERROR) - -from platformdirs import user_data_dir, user_config_dir -from loguru import logger -import PyEvdi -import pyedid -import pyray -import time - -from render import render_loop -import libunreal - -default_configuration: dict[str, str | int] = { - "display_angle": 45, - "display_spacing": 1, - "display_count": 3, - "allow_unsupported_devices": False, - "allow_unsupported_vendors": False, - "override_default_edid": False, - "edid_override_path": "/file/here", - "override_width": 0, - "override_height": 0, - "override_refresh_rate": 0, -} - -def initialize_configuration(): - pass - -used_cards: list[int] = [] - -def find_suitable_evdi_card() -> int: - for i in range(20): - if PyEvdi.check_device(i) == PyEvdi.AVAILABLE and i not in used_cards: - used_cards.append(i) - return i - - PyEvdi.add_device() - - for i in range(20): - if PyEvdi.check_device(i) == PyEvdi.AVAILABLE and i not in used_cards: - used_cards.append(i) - return i - - raise ValueError("Failed to allocate virtual display device") - -@logger.catch -def main(): - configuration = {} - - logger.info("Loading configuration") - config_dir = os.environ["UNREALXR_CONFIG_PATH"] if "UNREALXR_CONFIG_PATH" in os.environ else "" - data_dir = os.environ["UNREALXR_DATA_PATH"] if "UNREALXR_DATA_PATH" in os.environ else "" - - # Use OS defaults if we weren't overriden in env - if config_dir == "": - config_dir = user_config_dir("UnrealXR", "Tera") - - if data_dir == "": - data_dir = user_data_dir("UnrealXR", "Tera") - - try: - os.stat(data_dir) - except OSError: - os.makedirs(data_dir) - - # Read config and create it if it doesn't exist - config_path = os.path.join(config_dir, "config.json") - - try: - os.stat(config_path) - - with open(config_path, "r") as config_file: - configuration = json.load(config_file) - except OSError: - try: - os.makedirs(config_dir) - except OSError: - pass - - with open(config_path, "w") as config_file: - json.dump(default_configuration, config_file, indent=4) - - configuration = default_configuration - - # Set unbound values (ie. if user is using an older version) - for key, default_value in default_configuration.items(): - if key not in configuration: - # Add quotes if we're a string - value = default_value - - if isinstance(value, str): - value = f'"{value}"' - - logger.warning(f"Setting unbound key '{key}' with default value {value}. You might want to define this! If not, this warning can be safely ignored.") - configuration[key] = default_value - - # Initialize logging to files - logger.add(os.path.join(data_dir, "unrealxr.log"), format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}") - logger.info("Loaded configuration") - - if os.geteuid() != 0: - raise OSError("You are not running as root! Running as root is necessary to talk to the EVDI service") - - # Get the display EDID - logger.info("Attempting to read display EDID file") - edid: libunreal.UnrealXRDisplayMetadata | None = None - - if configuration["override_default_edid"] or configuration["allow_unsupported_vendors"]: - # We need to parse it to get the maximum width, height, and refresh rate for EVDI's calculations - with open(configuration["edid_override_path"], "rb") as edid_file: - edid_file = edid_file.read() - parsed_edid_file = pyedid.parse_edid(edid_file) - - max_width = int(configuration["override_width"]) - max_height = int(configuration["override_height"]) - max_refresh = int(configuration["override_refresh_rate"]) - - if configuration["override_width"] == 0 or configuration["override_height"] == 0 or configuration["override_refresh_rate"] == 0: - for resolution in parsed_edid_file.resolutions: - if configuration["override_width"] == 0 or configuration["override_height"] == 0: - if resolution[0] > max_width and resolution[1] > max_height: - max_width = resolution[0] - max_height = resolution[1] - - if configuration["override_refresh_rate"] == 0: - max_refresh = max(max_refresh, int(resolution[2])) - - if max_width == 0 or max_height == 0: - raise ValueError("Could not determine maximum width and/or height from EDID file, and the width and/or height overrides aren't set!") - - if max_refresh == 0: - raise ValueError("Could not determine maximum refresh rate from EDID file, and the refresh rate overrides aren't set!") - - edid = libunreal.UnrealXRDisplayMetadata(edid_file, parsed_edid_file.name if parsed_edid_file.name else "", {}, max_width, max_height, max_refresh, "", "") - else: - edid = libunreal.fetch_xr_glass_edid(configuration["allow_unsupported_devices"]) - - assert(edid is not None) - logger.info("Got EDID file") - - if platform == "linux" or platform == "linux2": - # TODO: implement EDID patching for overridden displays - logger.info("Patching EDID firmware") - patched_edid = libunreal.patch_edid_to_be_specialized(edid.edid) - libunreal.upload_edid_firmware(edid, patched_edid) - - def unload_custom_fw(): - with open(f"/sys/kernel/debug/dri/{edid.linux_drm_card.replace("card", "")}/{edid.linux_drm_connector}/edid_override", "w") as kernel_edid: - kernel_edid.write("reset") - - logger.info("Please unplug and plug in your XR device to restore it back to normal settings.") - - atexit.register(unload_custom_fw) - input("Press the Enter key to continue loading after you unplug and plug in your XR device.") - - # Raylib gets confused if there's multiple dri devices so we initialize the window before anything - logger.info("Initializing XR headset") - pyray.set_target_fps(edid.max_refresh_rate*2) # we need more headroom... - pyray.init_window(edid.max_width, edid.max_height, "UnrealXR") - - logger.info("Initializing virtual displays") - cards = [] - - for i in range(int(configuration["display_count"])): - suitable_card_id = find_suitable_evdi_card() - card = PyEvdi.Card(suitable_card_id) - card.connect(edid.edid, len(edid.edid), edid.max_width*edid.max_height, edid.max_width*edid.max_height*edid.max_refresh_rate) - cards.append(card) - - logger.debug(f"Initialized card #{str(i+1)}") - - atexit.register(lambda: card.close()) - - logger.info("Initialized displays. Entering rendering loop") - render_loop(edid, configuration, cards) - -if __name__ == "__main__": - print("Welcome to UnrealXR!\n") - main() diff --git a/modules/evdi b/modules/evdi deleted file mode 160000 index 3673a4b..0000000 --- a/modules/evdi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3673a4b34d386921fc323ddbd2ef0e000022e2d4 diff --git a/modules/nreal-driver b/modules/nreal-driver deleted file mode 160000 index 36ad789..0000000 --- a/modules/nreal-driver +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 36ad789c9b5893653c523d4af6e24120ec19ab33 diff --git a/modules/raylib-python-cffi b/modules/raylib-python-cffi deleted file mode 160000 index 7c982c0..0000000 --- a/modules/raylib-python-cffi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7c982c0d4e2773739f00eb91a2d2cbe87eb7c76d diff --git a/render.py b/render.py deleted file mode 100644 index 21cdffe..0000000 --- a/render.py +++ /dev/null @@ -1,235 +0,0 @@ -from dataclasses import dataclass -from io import BufferedWriter -from sys import int_info -from typing import Union -import ctypes -import time -import math - -from loguru import logger -from raylib import rl -import PyEvdi -import pyray - -from libunreal import UnrealXRDisplayMetadata, MCUCallbackWrapper, start_mcu_event_listener - -vertical_size = 0.0 -horizontal_sizing_constant = 1 - -previous_pitch = 0.0 -previous_yaw = 0.0 -previous_roll = 0.0 - -current_pitch = 0.0 -current_yaw = 0.0 -current_roll = 0.0 - -has_gotten_pitch_callback_before = False -has_gotten_yaw_callback_before = False -has_gotten_roll_callback_before = False - -@dataclass -class RectMetadata: - card: PyEvdi.Card - buffer_ptr: pyray.ffi.CData | None - texture: Union[pyray.Texture, None] - model: Union[pyray.Model, None] - angle: int - relative_position: int - -def pitch_callback(new_pitch: float): - global current_pitch - global previous_pitch - global has_gotten_pitch_callback_before - - if not has_gotten_pitch_callback_before: - has_gotten_pitch_callback_before = True - previous_pitch = new_pitch - current_pitch = new_pitch - else: - previous_pitch = current_pitch - current_pitch = new_pitch - -def yaw_callback(new_yaw: float): - global current_yaw - global previous_yaw - global has_gotten_yaw_callback_before - - if not has_gotten_yaw_callback_before: - has_gotten_yaw_callback_before = True - previous_yaw = new_yaw - current_yaw = new_yaw - else: - previous_yaw = current_yaw - current_yaw = new_yaw - -def roll_callback(new_roll: float): - global current_roll - global previous_roll - global has_gotten_roll_callback_before - - if not has_gotten_roll_callback_before: - has_gotten_roll_callback_before = True - previous_roll = new_roll - roll = new_roll - else: - previous_roll = current_roll - current_roll = new_roll - -def text_message(message: str): - logger.debug(f"Got message from AR's MCU: {message}") - -def stub_brightness_function(brightness: int): - pass - -def find_max_vertical_size(fovy_deg: float, distance: float) -> float: - fovy_rad = math.radians(fovy_deg) - return 2 * distance * math.tan(fovy_rad / 2) - -def find_optimal_horizonal_res(vertical_display_res: int, horizontal_display_res: int) -> float: - aspect_ratio = horizontal_display_res/vertical_display_res - horizontal_size = vertical_size * aspect_ratio - horizontal_size = horizontal_size * horizontal_sizing_constant - - return horizontal_size - -def render_loop(display_metadata: UnrealXRDisplayMetadata, config: dict[str, str | int], cards: list[PyEvdi.Card]): - global vertical_size - global core_mesh - logger.info("Starting sensor event listener") - - mcu_callbacks = MCUCallbackWrapper(roll_callback, pitch_callback, yaw_callback, text_message, stub_brightness_function, stub_brightness_function) - start_mcu_event_listener(display_metadata.device_vendor, mcu_callbacks) - - logger.info("Beginning sensor initialization. Awaiting first sensor update") - - while (not has_gotten_pitch_callback_before) or (not has_gotten_yaw_callback_before) or (not has_gotten_roll_callback_before): - time.sleep(0.01) - - logger.info("Initialized sensors") - - camera = pyray.Camera3D() - camera.fovy = 45.0 - - vertical_size = find_max_vertical_size(camera.fovy, 5.0) - - camera.position = pyray.Vector3(0.0, vertical_size/2, 5.0) - camera.target = pyray.Vector3(0.0, vertical_size/2, 0.0) - camera.up = pyray.Vector3(0.0, 1.0, 0.0) - camera.projection = pyray.CameraProjection.CAMERA_PERSPECTIVE - - core_mesh = pyray.gen_mesh_plane(find_optimal_horizonal_res(display_metadata.max_height, display_metadata.max_width), vertical_size, 1, 1) - - movement_vector = pyray.Vector3() - look_vector = pyray.Vector3() - - has_z_vector_disabled_quirk = False - has_sensor_init_delay_quirk = False - sensor_init_start_time = time.time() - - if "z_vector_disabled" in display_metadata.device_quirks and bool(display_metadata.device_quirks["z_vector_disabled"]): - logger.warning("QUIRK: The Z vector has been disabled for your specific device") - has_z_vector_disabled_quirk = True - - if "sensor_init_delay" in display_metadata.device_quirks: - logger.warning(f"QUIRK: Waiting {str(display_metadata.device_quirks["sensor_init_delay"])} second(s) before reading sensors") - logger.warning("|| MOVEMENT WILL NOT BE OPERATIONAL DURING THIS TIME. ||") - sensor_init_start_time = time.time() - has_sensor_init_delay_quirk = True - - rects: list[RectMetadata] = [] - - if int(config["display_count"]) >= 2: - display_angle = int(config["display_angle"]) - display_spacing = int(config["display_spacing"]) - total_displays = int(config["display_count"]) - - highest_possible_angle_on_both_sides = (total_displays-1)*display_angle - highest_possible_pixel_spacing_on_both_sides = (total_displays-1)*display_spacing - - for i in range(total_displays): - current_angle = (-highest_possible_angle_on_both_sides)+(display_angle*i) - current_display_spacing = (-highest_possible_pixel_spacing_on_both_sides)+(display_spacing*i) - - rect_metadata = RectMetadata(cards[i], None, None, None, current_angle, current_display_spacing) - - has_acquired_fb = False - - def fb_acquire_handler(evdi_buffer: PyEvdi.Buffer): - nonlocal has_acquired_fb - - if has_acquired_fb: - return - - has_acquired_fb = True - logger.info(f"Acquired buffer for card #{i+1} with ID {evdi_buffer.id}") - - address = ctypes.pythonapi.PyCapsule_GetPointer - address.restype = ctypes.c_void_p - address.argtypes = [ctypes.py_object, ctypes.c_char_p] - - buffer_void_ptr = address(evdi_buffer.bytes, None) - rect_metadata.buffer_ptr = pyray.ffi.cast("void *", buffer_void_ptr) - - pyray_image = pyray.Image() - - pyray_image.data = rect_metadata.buffer_ptr - pyray_image.width = display_metadata.max_width - pyray_image.height = display_metadata.max_height - pyray_image.mipmaps = 1 - pyray_image.format = pyray.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 - - rect_metadata.texture = pyray.load_texture_from_image(pyray_image) - rect_metadata.model = pyray.load_model_from_mesh(core_mesh) - - pyray.set_material_texture(rect_metadata.model.materials[0], pyray.MaterialMapIndex.MATERIAL_MAP_ALBEDO, rect_metadata.texture) - - cards[i].acquire_framebuffer_handler = fb_acquire_handler - cards[i].handle_events(1000) - - rects.append(rect_metadata) - - while not pyray.window_should_close(): - if has_sensor_init_delay_quirk: - if time.time() - sensor_init_start_time >= int(display_metadata.device_quirks["sensor_init_delay"]): - # Unset the quirk state - logger.info("Movement is now enabled.") - has_sensor_init_delay_quirk = False - else: - look_vector.x = (current_yaw-previous_yaw)*6.5 - look_vector.y = -(current_pitch-previous_pitch)*6.5 - - if not has_z_vector_disabled_quirk: - look_vector.z = (current_roll-previous_roll)*6.5 - - pyray.update_camera_pro(camera, movement_vector, look_vector, 0.0) - - pyray.begin_drawing() - pyray.clear_background(pyray.BLACK) - pyray.begin_mode_3d(camera) - - for rect_count in range(len(rects)): - rect = rects[rect_count] - - if rect.buffer_ptr is None or rect.texture is None or rect.model is None: - continue - - cards[rect_count].handle_events(1) - pyray.update_texture(rect.texture, rect.buffer_ptr) - - pyray.draw_model_ex( - rect.model, - pyray.Vector3(0, vertical_size/2, 0), - pyray.Vector3(1, 0, 0), # rotate around X to make it vertical - 90, - pyray.Vector3(1, 1, 1), - pyray.WHITE - ) - - break - - pyray.end_mode_3d() - pyray.end_drawing() - - logger.info("Goodbye!") - pyray.close_window() diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c2a938b..0000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -setuptools==80.9.0 -pybind11==2.13.6 -pyedid==1.0.3 -loguru==0.7.3 -platformdirs==4.3.8 -cffi=1.17.1 -wheel=0.45.1 diff --git a/shell.nix b/shell.nix index 1cd42af..b1e1b78 100644 --- a/shell.nix +++ b/shell.nix @@ -3,9 +3,12 @@ }: pkgs.mkShell { buildInputs = with pkgs; [ # Runtime dependencies - python3 pciutils + # UnrealXR build dependencies + go + gopls + # evdi build dependencies libdrm linuxHeaders @@ -37,11 +40,5 @@ mkdir -p "$PWD/data/config" "$PWD/data/data" export UNREALXR_CONFIG_PATH="$PWD/data/config" export UNREALXR_DATA_PATH="$PWD/data/data" - - if [ ! -d ".venv" ]; then - python3 -m venv .venv - fi - - source .venv/bin/activate ''; }