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 dataclasses
import uuid
@dataclasses.dataclass @dataclasses.dataclass
class EvdiDisplaySpec: class EvdiDisplaySpec:
@ -6,3 +9,76 @@ class EvdiDisplaySpec:
max_width: int max_width: int
max_height: int max_height: int
max_refresh_rate: 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 from libunreal.edid import EvdiDisplaySpec
import pyedid import pyedid
def upload_new_device_edid(display_spec: EvdiDisplaySpec, edid: bytes | bytearray):
pass
def fetch_xr_glass_edid(allow_unsupported_devices) -> EvdiDisplaySpec: def fetch_xr_glass_edid(allow_unsupported_devices) -> EvdiDisplaySpec:
# Scan for all VGA devices and their IDs # Scan for all VGA devices and their IDs
pci_device_comand = subprocess.run(["lspci"], capture_output=True) 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"]) 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.") 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 #!/usr/bin/env python3
from sys import platform
import atexit import atexit
import json import json
import os import os
@ -9,8 +10,8 @@ import PyEvdi
import pyedid import pyedid
import time import time
from render import render_loop
import libunreal import libunreal
from libunreal.edid import EvdiDisplaySpec
default_configuration: dict[str, str | int] = { default_configuration: dict[str, str | int] = {
"display_angle": 45, "display_angle": 45,
@ -42,7 +43,7 @@ def find_suitable_evdi_card() -> int:
used_cards.append(i) used_cards.append(i)
return i return i
raise ValueError("Failed to allocate available device") raise ValueError("Failed to allocate virtual display device")
@logger.catch @logger.catch
def main(): def main():
@ -104,7 +105,7 @@ def main():
# Get the display EDID # Get the display EDID
logger.info("Attempting to read display EDID file") 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"]: 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 # 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])) max_refresh = max(max_refresh, int(resolution[2]))
if max_width == 0 or max_height == 0: 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: if max_refresh == 0:
raise ValueError("Could not determine maximum refresh rate from EDID file, and the refresh rate overrides aren't set!") 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: else:
edid = libunreal.fetch_xr_glass_edid(configuration["allow_unsupported_devices"]) edid = libunreal.fetch_xr_glass_edid(configuration["allow_unsupported_devices"])
assert(edid is not None) assert(edid is not None)
logger.info("Got EDID file") 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 = [] cards = []
for i in range(int(configuration["display_count"])): 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) 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) cards.append(card)
logger.debug(f"Initialized card #{str(i)}") logger.debug(f"Initialized card #{str(i+1)}")
atexit.register(lambda: card.close()) atexit.register(lambda: card.close())
logger.info("Initialized displays") logger.info("Initialized displays")
logger.info("Beginning rendering")
time.sleep(10) render_loop(edid, cards)
if __name__ == "__main__": if __name__ == "__main__":
print("Welcome to UnrealXR!\n") print("Welcome to UnrealXR!\n")
main() 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 pyedid==1.0.3
loguru==0.7.3 loguru==0.7.3
platformdirs==4.3.8 platformdirs==4.3.8
raylib==5.5.0.2

View file

@ -16,12 +16,13 @@
xorg.libXcursor xorg.libXcursor
xorg.libXrandr xorg.libXrandr
xorg.libXinerama xorg.libXinerama
xorg.libX11
waylandpp waylandpp
libxkbcommon libxkbcommon
]; ];
shellHook = '' 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" 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"

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