chore: Add initial implementation of spatial virtual displays

This commit is contained in:
Tera << 8 2025-06-20 20:36:21 -04:00
parent 00988c3c28
commit 919d247934
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
2 changed files with 117 additions and 11 deletions

View file

@ -21,7 +21,7 @@ import libunreal
default_configuration: dict[str, str | int] = { default_configuration: dict[str, str | int] = {
"display_angle": 45, "display_angle": 45,
"display_pixel_spacing": 45, "display_spacing": 1,
"display_count": 3, "display_count": 3,
"allow_unsupported_devices": False, "allow_unsupported_devices": False,
"allow_unsupported_vendors": False, "allow_unsupported_vendors": False,
@ -164,7 +164,7 @@ def main():
# Raylib gets confused if there's multiple dri devices so we initialize the window before anything # Raylib gets confused if there's multiple dri devices so we initialize the window before anything
logger.info("Initializing XR headset") logger.info("Initializing XR headset")
pyray.set_target_fps(edid.max_refresh_rate) pyray.set_target_fps(edid.max_refresh_rate*2) # we need more headroom...
pyray.init_window(edid.max_width, edid.max_height, "UnrealXR") pyray.init_window(edid.max_width, edid.max_height, "UnrealXR")
logger.info("Initializing virtual displays") logger.info("Initializing virtual displays")
@ -181,7 +181,7 @@ def main():
atexit.register(lambda: card.close()) atexit.register(lambda: card.close())
logger.info("Initialized displays. Entering rendering loop") logger.info("Initialized displays. Entering rendering loop")
render_loop(edid, cards) render_loop(edid, configuration, cards)
if __name__ == "__main__": if __name__ == "__main__":
print("Welcome to UnrealXR!\n") print("Welcome to UnrealXR!\n")

122
render.py
View file

@ -1,12 +1,21 @@
from dataclasses import dataclass
from io import BufferedWriter
from sys import int_info
from typing import Union
import ctypes
import time import time
import math import math
from loguru import logger from loguru import logger
from raylib import rl
import PyEvdi import PyEvdi
import pyray import pyray
from libunreal import UnrealXRDisplayMetadata, MCUCallbackWrapper, start_mcu_event_listener from libunreal import UnrealXRDisplayMetadata, MCUCallbackWrapper, start_mcu_event_listener
vertical_size = 0.0
horizontal_sizing_constant = 1
previous_pitch = 0.0 previous_pitch = 0.0
previous_yaw = 0.0 previous_yaw = 0.0
previous_roll = 0.0 previous_roll = 0.0
@ -19,6 +28,15 @@ has_gotten_pitch_callback_before = False
has_gotten_yaw_callback_before = False has_gotten_yaw_callback_before = False
has_gotten_roll_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): def pitch_callback(new_pitch: float):
global current_pitch global current_pitch
global previous_pitch global previous_pitch
@ -64,7 +82,20 @@ def text_message(message: str):
def stub_brightness_function(brightness: int): def stub_brightness_function(brightness: int):
pass pass
def render_loop(display_metadata: UnrealXRDisplayMetadata, cards: list[PyEvdi.Card]): 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") logger.info("Starting sensor event listener")
mcu_callbacks = MCUCallbackWrapper(roll_callback, pitch_callback, yaw_callback, text_message, stub_brightness_function, stub_brightness_function) mcu_callbacks = MCUCallbackWrapper(roll_callback, pitch_callback, yaw_callback, text_message, stub_brightness_function, stub_brightness_function)
@ -78,13 +109,16 @@ def render_loop(display_metadata: UnrealXRDisplayMetadata, cards: list[PyEvdi.Ca
logger.info("Initialized sensors") logger.info("Initialized sensors")
camera = pyray.Camera3D() camera = pyray.Camera3D()
camera.position = pyray.Vector3(10.0, 10.0, 10.0)
camera.target = pyray.Vector3(0.0, 0.0, 0.0)
camera.up = pyray.Vector3(0.0, 1.0, 0.0)
camera.fovy = 45.0 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 camera.projection = pyray.CameraProjection.CAMERA_PERSPECTIVE
cube_position = pyray.Vector3(0.0, 0.0, 0.0) 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() movement_vector = pyray.Vector3()
look_vector = pyray.Vector3() look_vector = pyray.Vector3()
@ -93,7 +127,7 @@ def render_loop(display_metadata: UnrealXRDisplayMetadata, cards: list[PyEvdi.Ca
has_sensor_init_delay_quirk = False has_sensor_init_delay_quirk = False
sensor_init_start_time = time.time() sensor_init_start_time = time.time()
if "z_vector_disabled" in display_metadata.device_quirks: 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") logger.warning("QUIRK: The Z vector has been disabled for your specific device")
has_z_vector_disabled_quirk = True has_z_vector_disabled_quirk = True
@ -103,6 +137,58 @@ def render_loop(display_metadata: UnrealXRDisplayMetadata, cards: list[PyEvdi.Ca
sensor_init_start_time = time.time() sensor_init_start_time = time.time()
has_sensor_init_delay_quirk = True 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(): while not pyray.window_should_close():
if has_sensor_init_delay_quirk: if has_sensor_init_delay_quirk:
if time.time() - sensor_init_start_time >= int(display_metadata.device_quirks["sensor_init_delay"]): if time.time() - sensor_init_start_time >= int(display_metadata.device_quirks["sensor_init_delay"]):
@ -111,7 +197,7 @@ def render_loop(display_metadata: UnrealXRDisplayMetadata, cards: list[PyEvdi.Ca
has_sensor_init_delay_quirk = False has_sensor_init_delay_quirk = False
else: else:
look_vector.x = (current_yaw-previous_yaw)*6.5 look_vector.x = (current_yaw-previous_yaw)*6.5
look_vector.y = (current_pitch-previous_pitch)*6.5 look_vector.y = -(current_pitch-previous_pitch)*6.5
if not has_z_vector_disabled_quirk: if not has_z_vector_disabled_quirk:
look_vector.z = (current_roll-previous_roll)*6.5 look_vector.z = (current_roll-previous_roll)*6.5
@ -121,7 +207,27 @@ def render_loop(display_metadata: UnrealXRDisplayMetadata, cards: list[PyEvdi.Ca
pyray.begin_drawing() pyray.begin_drawing()
pyray.clear_background(pyray.BLACK) pyray.clear_background(pyray.BLACK)
pyray.begin_mode_3d(camera) pyray.begin_mode_3d(camera)
pyray.draw_cube(cube_position, 2.0, 2.0, 2.0, pyray.ORANGE)
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_mode_3d()
pyray.end_drawing() pyray.end_drawing()