chore: Get display EDID patching working
This commit is contained in:
parent
0fd3b21d6d
commit
81b89a1a4f
7 changed files with 153 additions and 12 deletions
|
@ -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
|
||||||
|
|
|
@ -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
39
main.py
|
@ -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
31
render.py
Normal 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()
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
3
unrealxr
3
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
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue