diff --git a/libunreal/edid.py b/libunreal/edid.py index b44363e..c358ee7 100644 --- a/libunreal/edid.py +++ b/libunreal/edid.py @@ -1,4 +1,7 @@ +from loguru import logger + import dataclasses +import uuid @dataclasses.dataclass class EvdiDisplaySpec: @@ -6,3 +9,76 @@ class EvdiDisplaySpec: 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 index a4507f7..2ddbedf 100644 --- a/libunreal/linux/__init__.py +++ b/libunreal/linux/__init__.py @@ -5,6 +5,9 @@ from libunreal.supported_devices import supported_devices from libunreal.edid import EvdiDisplaySpec import pyedid +def upload_new_device_edid(display_spec: EvdiDisplaySpec, edid: bytes | bytearray): + pass + def fetch_xr_glass_edid(allow_unsupported_devices) -> EvdiDisplaySpec: # Scan for all VGA devices and their IDs pci_device_comand = subprocess.run(["lspci"], capture_output=True) @@ -69,6 +72,13 @@ def fetch_xr_glass_edid(allow_unsupported_devices) -> EvdiDisplaySpec: max_refresh = int(manufacturer_supported_devices[edid.name]["max_refresh"]) - return EvdiDisplaySpec(raw_edid_file, max_width, max_height, max_refresh) + return EvdiDisplaySpec(raw_edid_file, 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: EvdiDisplaySpec, 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/main.py b/main.py index 925317f..0fc9658 100755 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +from sys import platform import atexit import json import os @@ -9,8 +10,8 @@ import PyEvdi import pyedid import time +from render import render_loop import libunreal -from libunreal.edid import EvdiDisplaySpec default_configuration: dict[str, str | int] = { "display_angle": 45, @@ -42,7 +43,7 @@ def find_suitable_evdi_card() -> int: used_cards.append(i) return i - raise ValueError("Failed to allocate available device") + raise ValueError("Failed to allocate virtual display device") @logger.catch def main(): @@ -104,7 +105,7 @@ def main(): # Get the display EDID logger.info("Attempting to read display EDID file") - edid: EvdiDisplaySpec | None = None + edid: libunreal.EvdiDisplaySpec | 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 @@ -127,18 +128,39 @@ def main(): max_refresh = max(max_refresh, int(resolution[2])) if max_width == 0 or max_height == 0: - raise ValueError("Could not determine maximum width and height from EDID file, and the ") + 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.EvdiDisplaySpec(edid_file, 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") - logger.info("Initializing virtual displays") + 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) + logger.debug("dumping custom fw") + + with open("/tmp/fw.bin", "wb") as fw_dump: + fw_dump.write(patched_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.") + + logger.info("Initializing virtual displays") cards = [] for i in range(int(configuration["display_count"])): @@ -147,13 +169,14 @@ def main(): 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)}") + logger.debug(f"Initialized card #{str(i+1)}") atexit.register(lambda: card.close()) logger.info("Initialized displays") + logger.info("Beginning rendering") - time.sleep(10) + render_loop(edid, cards) if __name__ == "__main__": print("Welcome to UnrealXR!\n") main() diff --git a/render.py b/render.py new file mode 100644 index 0000000..9bf4083 --- /dev/null +++ b/render.py @@ -0,0 +1,31 @@ +from loguru import logger +import PyEvdi +import pyray + +from libunreal import EvdiDisplaySpec + +def render_loop(display_metadata: EvdiDisplaySpec, cards: list[PyEvdi.Card]): + pyray.init_window(display_metadata.max_width, display_metadata.max_height, "UnrealXR") + + while not pyray.window_should_close(): + # Implement fullscreen toggle + if pyray.is_key_pressed(pyray.KeyboardKey.KEY_F11): + display = pyray.get_current_monitor() + + if pyray.is_window_fullscreen(): + pyray.set_window_size(display_metadata.max_width, display_metadata.max_height) + else: + pyray.set_window_size(pyray.get_monitor_width(display), pyray.get_monitor_height(display)) + + pyray.toggle_fullscreen() + # Ctrl-C to quit + elif pyray.is_key_down(pyray.KeyboardKey.KEY_LEFT_CONTROL) and pyray.is_key_down(pyray.KeyboardKey.KEY_C): + break + + pyray.begin_drawing() + pyray.clear_background(pyray.BLACK) + pyray.draw_text("Hello world", 190, 200, 20, pyray.VIOLET) + pyray.end_drawing() + + logger.info("Goodbye!") + pyray.close_window() diff --git a/requirements.txt b/requirements.txt index d978da6..7c56f4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ pybind11==2.13.6 pyedid==1.0.3 loguru==0.7.3 platformdirs==4.3.8 +raylib==5.5.0.2 diff --git a/shell.nix b/shell.nix index d448fde..a9e7e2f 100644 --- a/shell.nix +++ b/shell.nix @@ -16,12 +16,13 @@ xorg.libXcursor xorg.libXrandr xorg.libXinerama + xorg.libX11 waylandpp libxkbcommon ]; shellHook = '' - export LD_LIBRARY_PATH="$PWD/evdi/library:$LD_LIBRARY_PATH" + export LD_LIBRARY_PATH="$PWD/evdi/library:${pkgs.lib.makeLibraryPath [ pkgs.xorg.libX11 pkgs.libGL ]}:$LD_LIBRARY_PATH" mkdir -p "$PWD/data/config" "$PWD/data/data" export UNREALXR_CONFIG_PATH="$PWD/data/config" export UNREALXR_DATA_PATH="$PWD/data/data" diff --git a/unrealxr b/unrealxr index 672da8e..3872682 100755 --- a/unrealxr +++ b/unrealxr @@ -1,2 +1 @@ -#!/usr/bin/env bash -sudo LD_LIBRARY_PATH=$LD_LIBRARY_PATH UNREALXR_CONFIG_PATH=$UNREALXR_CONFIG_PATH UNREALXR_DATA_PATH=$UNREALXR_DATA_PATH python3 main.py +sudo LD_LIBRARY_PATH="$LD_LIBRARY_PATH" UNREALXR_CONFIG_PATH="$UNREALXR_CONFIG_PATH" UNREALXR_DATA_PATH="$UNREALXR_DATA_PATH" python3 main.py