chore: Remove legacy Python codebase

This commit is contained in:
Tera << 8 2025-06-21 12:58:41 -04:00
parent 919d247934
commit 643fd3c61c
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
14 changed files with 24 additions and 1058 deletions

291
.gitignore vendored
View file

@ -1,281 +1,30 @@
# ---> Python # ---> Go
# Byte-compiled / optimized / DLL files # If you prefer the allow list template instead of the deny list, see community template:
__pycache__/ # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
*.py[cod] #
*$py.class # Binaries for programs and plugins
*.exe
# C extensions *.exe~
*.dll
*.so *.so
*.dylib
# Distribution / packaging # Test binary, built with `go test -c`
.Python *.test
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller # Output of the go coverage tool, specifically when used with LiteIDE
# Usually these files are written by a python script from a template *.out
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs # Dependency directories (remove the comment below to include it)
pip-log.txt # vendor/
pip-delete-this-directory.txt
# Unit test / coverage reports # Go workspace file
htmlcov/ go.work
.tox/ go.work.sum
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations # env file
*.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 .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 # ---> UnrealXR
# development dirs
data data
drivers

9
.gitmodules vendored
View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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,
}
}
}

188
main.py
View file

@ -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()

@ -1 +0,0 @@
Subproject commit 3673a4b34d386921fc323ddbd2ef0e000022e2d4

@ -1 +0,0 @@
Subproject commit 36ad789c9b5893653c523d4af6e24120ec19ab33

@ -1 +0,0 @@
Subproject commit 7c982c0d4e2773739f00eb91a2d2cbe87eb7c76d

235
render.py
View file

@ -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()

View file

@ -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

View file

@ -3,9 +3,12 @@
}: pkgs.mkShell { }: pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
# Runtime dependencies # Runtime dependencies
python3
pciutils pciutils
# UnrealXR build dependencies
go
gopls
# evdi build dependencies # evdi build dependencies
libdrm libdrm
linuxHeaders linuxHeaders
@ -37,11 +40,5 @@
mkdir -p "$PWD/data/config" "$PWD/data/data" mkdir -p "$PWD/data/config" "$PWD/data/data"
export UNREALXR_CONFIG_PATH="$PWD/data/config" export UNREALXR_CONFIG_PATH="$PWD/data/config"
export UNREALXR_DATA_PATH="$PWD/data/data" export UNREALXR_DATA_PATH="$PWD/data/data"
if [ ! -d ".venv" ]; then
python3 -m venv .venv
fi
source .venv/bin/activate
''; '';
} }