chore: Get display EDID patching working

This commit is contained in:
Tera << 8 2025-06-07 20:22:17 -04:00
parent 0fd3b21d6d
commit 81b89a1a4f
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
7 changed files with 153 additions and 12 deletions

View file

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

View file

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

39
main.py
View file

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

31
render.py Normal file
View file

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

View file

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

View file

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

View file

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