Compare commits

...
Sign in to create a new pull request.

12 commits

Author SHA1 Message Date
0efaf19b40
feature: Implement display rotation 2025-06-27 22:08:51 -04:00
6911afb6e0
fix: Adds missing script headers on unrealxr startup script 2025-06-27 12:50:29 -04:00
30e90af276
feature: Attempt to decrease load by decreasing max frame rate of window
We don't really need to run at twice the speed of the display's refresh
rate as Raylib manages the target time between frames well.
2025-06-27 12:48:25 -04:00
59dbdf9eb0
chore: Make devshell default to debug log level 2025-06-27 12:29:47 -04:00
f16b35f102
chore: Add reference to hacking guide in the README 2025-06-27 12:27:27 -04:00
8b482cecd5
feature: Add dummy mode 2025-06-27 12:26:54 -04:00
f79657afc2
chore: Add shields 2025-06-26 19:13:52 -04:00
77792a82a6
chore: Seperate Go modules into seperate directories
This is done to help potential dependents reduce download sizes and only
download what they need.
2025-06-26 18:57:29 -04:00
eb33ec116e
chore: Remove culling hacks as they don't work :( 2025-06-26 18:25:37 -04:00
9f9c8b4be7
feature: Add primitive version of multiple displays 2025-06-26 17:44:36 -04:00
322e00b3b9
chore: Remove unused fields in the renderer metadata struct 2025-06-26 00:30:39 -04:00
5ea88e8c4a Merge pull request 'Rewrite entire codebase into Go' (#2) from go-rewrite into main
Reviewed-on: #2
2025-06-25 21:49:50 -04:00
55 changed files with 497 additions and 198 deletions

23
HACKING.md Normal file
View file

@ -0,0 +1,23 @@
# UnrealXR Development Documentation
## Dummies and You
The dummy/development build generally isn't recommended for use if you're developing headset or OS drivers. It's intended for development on the 3D environment itself only (ie. virtual display positioning, widgets, etc.)
To use it, all you need to do is run the Makefile with the `dev` target:
```bash
make dev
```
In the code, this mode is typically referred to as "dummy mode" (`dummy_ar.go`) or "fake mode" (ie. `patching_tools_fake_patching.go`).
To control the X11 window, you can use the arrow keys to move the camera around. You **cannot** use the mouse due to the nature of this application. If you use the mouse for this, it would make development really difficult as you're using virtual displays and you need your mouse to interact with the desktop.
If you're adding new headset drivers, but you still want to test in a window, you still can. It's not the dummy mode, but you can compile UnrealXR without the DRM flags (`drm drm_leasing drm_disable_input`) manually. This may differ in the future, but for now, the command is:
```bash
cd app; go build -v -tags "headset_driver_goes_here noaudio" -o ../uxr .; cd ..
```
Replace `headset_driver_goes_here` with the name of your headset driver. For example, `xreal` is the Xreal driver.

View file

@ -1,5 +1,5 @@
APP_DIR := ./app
OUTPUT := uxr
OUTPUT := ./uxr
TAGS := xreal noaudio drm drm_leasing drm_disable_input
.PHONY: all build clean
@ -7,7 +7,10 @@ TAGS := xreal noaudio drm drm_leasing drm_disable_input
all: build
build:
go build -v -tags '$(TAGS)' -o $(OUTPUT) $(APP_DIR)
cd $(APP_DIR) && go build -v -tags '$(TAGS)' -o ../$(OUTPUT) .
dev:
cd $(APP_DIR) && go build -v -tags 'noaudio dummy_ar fake_edid_patching' -o ../$(OUTPUT) .
clean:
rm -f $(OUTPUT)

View file

@ -1,4 +1,8 @@
# UnrealXR
[![Build Status](https://git.terah.dev/UnrealXR/unrealxr/actions/workflows/build.yml/badge.svg)](https://git.terah.dev/UnrealXR/unrealxr/actions)
[![GoDoc](https://godoc.org/git.terah.dev/UnrealXR/unrealxr?status.svg)](https://godoc.org/git.terah.dev/UnrealXR/unrealxr)
[![Go Report Card](https://goreportcard.com/badge/git.terah.dev/UnrealXR/unrealxr/app)](https://goreportcard.com/report/git.terah.dev/UnrealXR/unrealxr/app)
[![License](https://img.shields.io/badge/license-BSD--3--Clause-green)](https://git.terah.dev/imterah/goevdi/src/branch/main/app/LICENSE)
UnrealXR is a spatial multi-display renderer for the Xreal line of devices, enabling immersive, simultaneous viewing of multiple desktops and applications in 3D space.
@ -29,3 +33,7 @@ If you're using Nix/NixOS, all you need to do is use `nix-shell` to enter the de
## Building
Just run `make` in the root directory.
## Development Guide
See [HACKING.md](https://git.terah.dev/UnrealXR/unrealxr/src/branch/main/HACKING.md).

11
app/LICENSE Normal file
View file

@ -0,0 +1,11 @@
Copyright (c) 2025 imterah.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -6,10 +6,12 @@ import _ "embed"
var InitialConfig []byte
type DisplayConfig struct {
Angle *int `yaml:"angle"`
FOV *int `yaml:"fov"`
Spacing *int `yaml:"spacing"`
Count *int `yaml:"count"`
Angle *int `yaml:"angle"`
FOV *int `yaml:"fov"`
Spacing *float32 `yaml:"spacing"`
RadiusMultiplier *float32 `yaml:"circle_radius_multiplier"`
UseCircularSpacing *bool `yaml:"use_circular_spacing"`
Count *int `yaml:"count"`
}
type AppOverrides struct {
@ -28,16 +30,22 @@ func getPtrToInt(int int) *int {
return &int
}
func getPtrToFloat32(float32 float32) *float32 {
return &float32
}
func getPtrToBool(bool bool) *bool {
return &bool
}
var DefaultConfig = &Config{
DisplayConfig: DisplayConfig{
Angle: getPtrToInt(45),
FOV: getPtrToInt(45),
Spacing: getPtrToInt(1),
Count: getPtrToInt(3),
Angle: getPtrToInt(45),
FOV: getPtrToInt(45),
Spacing: getPtrToFloat32(0.5),
RadiusMultiplier: getPtrToFloat32(2),
UseCircularSpacing: getPtrToBool(true),
Count: getPtrToInt(3),
},
Overrides: AppOverrides{
AllowUnsupportedDevices: getPtrToBool(false),

View file

@ -9,7 +9,9 @@
display:
angle: 45 # Angle of the virtual displays
fov: 45 # FOV of the 3D camera
spacing: 1 # Spacing between virtual displays
spacing: 0.5 # Raw spacing between virtual displays. Does not use circles in the layout. Purely flat plane.
circle_radius_multiplier: 2 # Multiplier for the radius of the circle used to calculate the spacing between virtual displays. "Rounded" plane of sorts.
use_circular_spacing: true # If true, uses a circular layout for the virtual displays.
count: 3 # Count of virtual displays
overrides:
allow_unsupported_devices: false # If true, allows unsupported devices to be used as long as they're a compatible vendor (Xreal)

Binary file not shown.

View file

@ -0,0 +1,42 @@
//go:build fake_edid_patching
// +build fake_edid_patching
package edidtools
import (
_ "embed"
"fmt"
"github.com/charmbracelet/log"
)
//go:embed bin/xreal-air-edid.bin
var edidFirmware []byte
// Attempts to fetch the EDID firmware for any supported XR glasses device
func FetchXRGlassEDID(allowUnsupportedDevices bool) (*DisplayMetadata, error) {
log.Warn("Not actually fetching EDID firmware in fake patching build -- using embedded firmware")
parsedEDID, err := ParseEDID(edidFirmware, allowUnsupportedDevices)
if err != nil {
return nil, fmt.Errorf("failed to parse embedded EDID firmware: %w", err)
}
parsedEDID.DeviceQuirks.ZVectorDisabled = false
parsedEDID.DeviceQuirks.SensorInitDelay = 0
parsedEDID.DeviceQuirks.UsesMouseMovement = true
return parsedEDID, nil
}
// Loads custom firmware for a supported XR glass device
func LoadCustomEDIDFirmware(displayMetadata *DisplayMetadata, edidFirmware []byte) error {
log.Warn("Not actually patching EDID firmware in fake patching build -- ignoring")
return nil
}
// Unloads custom firmware for a supported XR glass device
func UnloadCustomEDIDFirmware(displayMetadata *DisplayMetadata) error {
log.Warn("Not actually unloading EDID firmware in fake patching build -- ignoring")
return nil
}

View file

@ -1,5 +1,5 @@
//go:build linux
// +build linux
//go:build linux && !fake_edid_patching
// +build linux,!fake_edid_patching
package edidtools

View file

@ -1,5 +1,5 @@
//go:build darwin
// +build darwin
//go:build darwin && !fake_edid_patching
// +build darwin,!fake_edid_patching
package edidtools

View file

@ -1,5 +1,5 @@
//go:build windows
// +build windows
//go:build windows && !fake_edid_patching
// +build windows,!fake_edid_patching
package edidtools

View file

@ -1,11 +1,12 @@
package edidtools
type DisplayQuirks struct {
MaxWidth int
MaxHeight int
MaxRefreshRate int
SensorInitDelay int
ZVectorDisabled bool
MaxWidth int
MaxHeight int
MaxRefreshRate int
SensorInitDelay int
ZVectorDisabled bool
UsesMouseMovement bool
}
type DisplayMetadata struct {

View file

@ -1,14 +1,19 @@
module git.terah.dev/UnrealXR/unrealxr
module git.terah.dev/UnrealXR/unrealxr/app
go 1.24.3
replace git.terah.dev/UnrealXR/unrealxr/ardriver => ../ardriver
replace git.terah.dev/UnrealXR/unrealxr/edidpatcher => ../edidpatcher
require (
git.terah.dev/UnrealXR/raylib-go/raylib v0.55.2-0.20250623002739-1468af2636e1
git.terah.dev/imterah/goevdi v1.14.11-0.20250626004148-bdbef2a68ff9
git.terah.dev/UnrealXR/unrealxr/ardriver v0.0.0-00010101000000-000000000000
git.terah.dev/UnrealXR/unrealxr/edidpatcher v0.0.0-00010101000000-000000000000
git.terah.dev/imterah/goevdi/libevdi v0.1.0-evdi1.14.10
github.com/anoopengineer/edidparser v0.0.0-20240602223913-86ca9ed3d2b0
github.com/charmbracelet/log v0.4.2
github.com/goccy/go-yaml v1.18.0
github.com/google/uuid v1.6.0
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
github.com/tebeka/atexit v0.3.0
github.com/urfave/cli/v3 v3.3.8
@ -23,6 +28,7 @@ require (
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect

View file

@ -1,7 +1,7 @@
git.terah.dev/UnrealXR/raylib-go/raylib v0.55.2-0.20250623002739-1468af2636e1 h1:cO/veKc8mon3Zlueq1sEc047B04fk/hoBJSn+btMLDI=
git.terah.dev/UnrealXR/raylib-go/raylib v0.55.2-0.20250623002739-1468af2636e1/go.mod h1:ZRirF2UuVWSbl2ux7oyHwXcinni9msejCvtIsXbT8yY=
git.terah.dev/imterah/goevdi v1.14.11-0.20250626004148-bdbef2a68ff9 h1:TYcPZ62CR3keYf/dE9KyV5X5krh+riDyZ3fnhkeSRyA=
git.terah.dev/imterah/goevdi v1.14.11-0.20250626004148-bdbef2a68ff9/go.mod h1:4scjAuFakx/2gTRSeCtTNHnj1v9FdF3XiOMmWsz4FDs=
git.terah.dev/imterah/goevdi/libevdi v0.1.0-evdi1.14.10 h1:M+Wja0b6Ks3mZXMGoBs3KZXsKRRWolUelITLQ/kaIFw=
git.terah.dev/imterah/goevdi/libevdi v0.1.0-evdi1.14.10/go.mod h1:7zdodqq+tECNHTljD5l7x0PvcGglLCa+mD2eQj0vkEg=
github.com/anoopengineer/edidparser v0.0.0-20240602223913-86ca9ed3d2b0 h1:rTfysyBCL7LPbq9GFpQbllvKT8vEI93lQUwksMMxHMI=
github.com/anoopengineer/edidparser v0.0.0-20240602223913-86ca9ed3d2b0/go.mod h1:fEt61NePh3ZMxA+g3iC4CaGzY9lEsHRUkYJY2x0lBAw=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=

View file

@ -118,7 +118,7 @@ func mainEntrypoint(context.Context, *cli.Command) error {
bufio.NewReader(os.Stdin).ReadBytes('\n') // Wait for Enter key press before continuing
log.Info("Initializing XR headset")
rl.SetTargetFPS(int32(displayMetadata.MaxRefreshRate * 2))
rl.SetTargetFPS(int32(displayMetadata.MaxRefreshRate))
rl.InitWindow(int32(displayMetadata.MaxWidth), int32(displayMetadata.MaxHeight), "UnrealXR")
atexit.Register(func() {
@ -155,22 +155,23 @@ func mainEntrypoint(context.Context, *cli.Command) error {
Y2: displayMetadata.MaxHeight,
}
displayBuffer := openedDevice.CreateBuffer(displayMetadata.MaxWidth, displayMetadata.MaxHeight, libevdi.StridePixelFormatRGBA32, displayRect)
displayBuffer, err := openedDevice.CreateBuffer(displayMetadata.MaxWidth, displayMetadata.MaxHeight, libevdi.StridePixelFormatRGBA32, displayRect)
if err != nil {
log.Errorf("Failed to create buffer for display %d: %s", currentDisplay, err.Error())
atexit.Exit(1)
return nil
}
displayMetadata := &renderer.EvdiDisplayMetadata{
EvdiNode: openedDevice,
Rect: displayRect,
Buffer: displayBuffer,
ShouldRequestUpdate: true,
}
displayMetadata.EventContext = &libevdi.EvdiEventContext{
UpdateReadyHandler: func(bufferToBeUpdated int) {
displayMetadata.IsUpdateReady = true
},
EvdiNode: openedDevice,
Rect: displayRect,
Buffer: displayBuffer,
}
displayMetadata.EventContext = &libevdi.EvdiEventContext{}
openedDevice.RegisterEventHandler(displayMetadata.EventContext)
evdiCards[currentDisplay] = displayMetadata
}

View file

@ -17,8 +17,10 @@ import (
)
type TextureModelPair struct {
Texture rl.Texture2D
Model rl.Model
Texture rl.Texture2D
Model rl.Model
CurrentAngle float32
CurrentDisplaySpacing float32
}
func findMaxVerticalSize(fovyDeg float32, distance float32) float32 {
@ -33,6 +35,15 @@ func findOptimalHorizontalRes(verticalDisplayRes float32, horizontalDisplayRes f
return horizontalSize
}
func findHfovFromVfov(vfovDeg, w, h float64) float64 {
vfovRad := vfovDeg * math.Pi / 180
ar := w / h
hfovRad := 2 * math.Atan(math.Tan(vfovRad/2)*ar)
return hfovRad * 180 / math.Pi
}
func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.DisplayMetadata, evdiCards []*EvdiDisplayMetadata) {
log.Info("Initializing AR driver")
headset, err := ardriver.GetDevice()
@ -97,7 +108,9 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa
headset.RegisterEventListeners(arEventListner)
fovY := float32(45.0)
fovY := float32(*config.DisplayConfig.FOV)
fovX := findHfovFromVfov(float64(fovY), float64(displayMetadata.MaxWidth), float64(displayMetadata.MaxHeight))
verticalSize := findMaxVerticalSize(fovY, 5.0)
camera := rl.NewCamera3D(
@ -120,12 +133,23 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa
rl.CameraPerspective,
)
// Disable front and back face culling. It caused issues involving the entire virtual display dissappearing
// If this issue still happens I *am* going to cry
rl.DisableBackfaceCulling()
rl.DisableDepthTest()
horizontalSize := findOptimalHorizontalRes(float32(displayMetadata.MaxHeight), float32(displayMetadata.MaxWidth), verticalSize)
coreMesh := rl.GenMeshPlane(horizontalSize, verticalSize, 1, 1)
coreMesh := rl.GenMeshPlane(findOptimalHorizontalRes(float32(displayMetadata.MaxHeight), float32(displayMetadata.MaxWidth), verticalSize), verticalSize, 1, 1)
var radius float32
if *config.DisplayConfig.UseCircularSpacing == true {
radiusX := (horizontalSize / 2) / float32(math.Tan((float64(fovX)*math.Pi/180.0)/2))
radiusY := (verticalSize / 2) / float32(math.Tan((float64(fovY)*math.Pi/180.0)/2))
if radiusY > radiusX {
radius = radiusY
} else {
radius = radiusX
}
radius *= *config.DisplayConfig.RadiusMultiplier
}
movementVector := rl.Vector3{
X: 0.0,
@ -156,37 +180,65 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa
rects := make([]*TextureModelPair, len(evdiCards))
displayAngle := float32(*config.DisplayConfig.Angle)
displaySpacing := *config.DisplayConfig.Spacing + horizontalSize
highestPossibleAngleOnBothSides := float32((*config.DisplayConfig.Count)-1) * displayAngle
highestPossibleDisplaySpacingOnBothSides := float32((*config.DisplayConfig.Count)-1) * displaySpacing
for i, card := range evdiCards {
currentAngle := (-highestPossibleAngleOnBothSides) + (displayAngle * float32(i+1))
currentDisplaySpacing := (-highestPossibleDisplaySpacingOnBothSides) + (displaySpacing * float32(i+1))
log.Debugf("display #%d: currentAngle=%f, currentDisplaySpacing=%f", i, currentAngle, currentDisplaySpacing)
image := rl.NewImage(card.Buffer.Buffer, int32(displayMetadata.MaxWidth), int32(displayMetadata.MaxHeight), 1, rl.UncompressedR8g8b8a8)
texture := rl.LoadTextureFromImage(image)
model := rl.LoadModelFromMesh(coreMesh)
// spin up/down
pitchRad := float32(-90 * rl.Deg2rad)
// spin left/right
yawRad := currentAngle * rl.Deg2rad
rotX := rl.MatrixRotateX(pitchRad)
rotY := rl.MatrixRotateY(yawRad)
transform := rl.MatrixMultiply(rotX, rotY)
model.Transform = transform
rl.SetMaterialTexture(model.Materials, rl.MapAlbedo, texture)
rects[i] = &TextureModelPair{
Texture: texture,
Model: model,
Texture: texture,
Model: model,
CurrentAngle: currentAngle,
CurrentDisplaySpacing: currentDisplaySpacing,
}
}
eventTimeoutDuration := 0 * time.Millisecond
for !rl.WindowShouldClose() {
if hasSensorInitDelayQuirk {
if time.Now().Sub(sensorInitStartTime) > time.Duration(displayMetadata.DeviceQuirks.SensorInitDelay)*time.Second {
log.Info("Movement is now enabled.")
hasSensorInitDelayQuirk = false
if !displayMetadata.DeviceQuirks.UsesMouseMovement {
if hasSensorInitDelayQuirk {
if time.Now().Sub(sensorInitStartTime) > time.Duration(displayMetadata.DeviceQuirks.SensorInitDelay)*time.Second {
log.Info("Movement is now enabled.")
hasSensorInitDelayQuirk = false
}
} else {
lookVector.X = (currentYaw - previousYaw) * 6.5
lookVector.Y = -(currentPitch - previousPitch) * 6.5
if !hasZVectorDisabledQuirk {
lookVector.Z = (currentRoll - previousRoll) * 6.5
}
rl.UpdateCameraPro(&camera, movementVector, lookVector, 0)
}
} else {
lookVector.X = (currentYaw - previousYaw) * 6.5
lookVector.Y = -(currentPitch - previousPitch) * 6.5
if !hasZVectorDisabledQuirk {
lookVector.Z = (currentRoll - previousRoll) * 6.5
}
rl.UpdateCameraPro(&camera, movementVector, lookVector, 0)
rl.UpdateCamera(&camera, rl.CameraFirstPerson)
}
rl.BeginDrawing()
@ -200,13 +252,13 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa
if err != nil {
log.Errorf("Failed to wait for display events: %s", err.Error())
break
continue
}
if ready {
if err := card.EvdiNode.HandleEvents(card.EventContext); err != nil {
log.Errorf("Failed to handle display events: %s", err.Error())
break
continue
}
card.EvdiNode.GrabPixels(card.Rect)
@ -220,20 +272,35 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa
card.EvdiNode.RequestUpdate(card.Buffer)
}
worldPos := rl.Vector3{
X: 0,
Y: verticalSize / 2,
Z: 0,
}
if *config.DisplayConfig.UseCircularSpacing == true {
yawRad := float32(rl.Deg2rad * rect.CurrentAngle)
// WTF?
posX := float32(math.Sin(float64(yawRad))) * radius
posZ := -float32(math.Cos(float64(yawRad))) * radius
worldPos.X = posX
worldPos.Z = posZ + radius
} else {
worldPos.X = rect.CurrentDisplaySpacing
}
rl.DrawModelEx(
rect.Model,
rl.Vector3{
X: 0,
Y: verticalSize / 2,
Z: 0,
},
worldPos,
// rotate around X to make it vertical
rl.Vector3{
X: 1,
X: 0,
Y: 0,
Z: 0,
},
90,
0,
rl.Vector3{
X: 1,
Y: 1,
@ -241,8 +308,6 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa
},
rl.White,
)
break
}
rl.EndMode3D()

View file

@ -5,10 +5,8 @@ import (
)
type EvdiDisplayMetadata struct {
EvdiNode *libevdi.EvdiNode
Rect *libevdi.EvdiDisplayRect
Buffer *libevdi.EvdiBuffer
EventContext *libevdi.EvdiEventContext
ShouldRequestUpdate bool
IsUpdateReady bool
EvdiNode *libevdi.EvdiNode
Rect *libevdi.EvdiDisplayRect
Buffer *libevdi.EvdiBuffer
EventContext *libevdi.EvdiEventContext
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
"git.terah.dev/UnrealXR/unrealxr/ardriver/dummy"
"git.terah.dev/UnrealXR/unrealxr/ardriver/xreal"
)
@ -19,5 +20,16 @@ func GetDevice() (commons.ARDevice, error) {
return device, nil
}
if dummy.IsDummyDeviceEnabled {
device, err := dummy.New()
if err != nil {
fmt.Printf("failed to initialize dummy device: %w\n", err)
return nil, err
}
return device, nil
}
return nil, fmt.Errorf("failed to initialize any device")
}

View file

@ -0,0 +1,40 @@
//go:build dummy_ar
// +build dummy_ar
package dummy
import (
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
)
var IsDummyDeviceEnabled = true
// Implements commons.ARDevice
type DummyDevice struct {
}
func (device *DummyDevice) Initialize() error {
return nil
}
func (device *DummyDevice) End() error {
return nil
}
func (device *DummyDevice) IsPollingLibrary() bool {
return false
}
func (device *DummyDevice) IsEventBasedLibrary() bool {
return false
}
func (device *DummyDevice) Poll() error {
return nil
}
func (device *DummyDevice) RegisterEventListeners(*commons.AREventListener) {}
func New() (*DummyDevice, error) {
return &DummyDevice{}, nil
}

View file

@ -0,0 +1,42 @@
//go:build !dummy_ar
// +build !dummy_ar
package dummy
import (
"fmt"
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
)
var IsDummyDeviceEnabled = false
// Implements commons.ARDevice
type DummyDevice struct {
}
func (device *DummyDevice) Initialize() error {
return fmt.Errorf("dummy device is not enabled")
}
func (device *DummyDevice) End() error {
return fmt.Errorf("dummy device is not enabled")
}
func (device *DummyDevice) IsPollingLibrary() bool {
return false
}
func (device *DummyDevice) IsEventBasedLibrary() bool {
return false
}
func (device *DummyDevice) Poll() error {
return fmt.Errorf("dummy device is not enabled")
}
func (device *DummyDevice) RegisterEventListeners(*commons.AREventListener) {}
func New() (*DummyDevice, error) {
return nil, fmt.Errorf("dummy device is not enabled")
}

3
ardriver/go.mod Normal file
View file

@ -0,0 +1,3 @@
module git.terah.dev/UnrealXR/unrealxr/ardriver
go 1.24.3

View file

@ -3,127 +3,21 @@
package xreal
// #cgo CFLAGS: -w -I./Fusion/
// #cgo pkg-config: json-c libusb-1.0 hidapi-libusb
// #include "go_ffi.h"
// #include "device.h"
// #include "device_imu.h"
// #include "device_mcu.h"
import "C"
import (
"fmt"
"sync"
"time"
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
xreal "git.terah.dev/UnrealXR/unrealxr/ardriver/xreal/xrealsrc"
)
var (
IsXrealEnabled = true
deviceEventHandlerMutex = sync.Mutex{}
deviceEventListener *commons.AREventListener
)
var IsXrealEnabled = true
//export goIMUEventHandler
func goIMUEventHandler(_ C.uint64_t, event_type C.device_imu_event_type, ahrs *C.struct_device_imu_ahrs_t) {
if deviceEventListener == nil {
return
}
if event_type != C.DEVICE_IMU_EVENT_UPDATE {
return
}
orientation := C.device_imu_get_orientation(ahrs)
euler := C.device_imu_get_euler(orientation)
deviceEventListener.PitchCallback(float32(euler.pitch))
deviceEventListener.RollCallback(float32(euler.roll))
deviceEventListener.YawCallback(float32(euler.yaw))
}
// Implements commons.ARDevice
type XrealDevice struct {
eventListener *commons.AREventListener
imuDevice *C.struct_device_imu_t
deviceIsOpen bool
}
func (device *XrealDevice) Initialize() error {
if device.deviceIsOpen {
return fmt.Errorf("device is already open")
}
device.imuDevice = &C.struct_device_imu_t{}
// (*[0]byte) is a FUBAR way to cast a pointer to a function, but unsafe.Pointer doesn't work:
// cannot use unsafe.Pointer(_Cgo_ptr(_Cfpvar_fp_imuEventHandler)) (value of type unsafe.Pointer) as *[0]byte value in variable declaration
if C.DEVICE_IMU_ERROR_NO_ERROR != C.device_imu_open(device.imuDevice, (*[0]byte)(C.imuEventHandler)) {
return fmt.Errorf("failed to open IMU device")
}
C.device_imu_clear(device.imuDevice)
C.device_imu_calibrate(device.imuDevice, 1000, true, true, false)
device.deviceIsOpen = true
// let's hope this doesn't cause race conditions
go func() {
for device.eventListener == nil {
time.Sleep(time.Millisecond * 10)
}
for {
if !device.deviceIsOpen {
break
}
// I'm sorry.
deviceEventHandlerMutex.Lock()
deviceEventListener = device.eventListener
status := C.device_imu_read(device.imuDevice, -1)
deviceEventHandlerMutex.Unlock()
if status != C.DEVICE_IMU_ERROR_NO_ERROR {
break
}
}
device.deviceIsOpen = false
C.device_imu_close(device.imuDevice)
}()
return nil
}
func (device *XrealDevice) End() error {
if !device.deviceIsOpen {
return fmt.Errorf("device is not open")
}
C.device_imu_close(device.imuDevice)
device.deviceIsOpen = false
return nil
}
func (device *XrealDevice) IsPollingLibrary() bool {
return false
}
func (device *XrealDevice) IsEventBasedLibrary() bool {
return true
}
func (device *XrealDevice) Poll() error {
return fmt.Errorf("not supported")
}
func (device *XrealDevice) RegisterEventListeners(listener *commons.AREventListener) {
device.eventListener = listener
*xreal.XrealDevice
}
func New() (*XrealDevice, error) {
device := &XrealDevice{}
device := &XrealDevice{
XrealDevice: &xreal.XrealDevice{},
}
err := device.Initialize()
if err != nil {

View file

@ -35,9 +35,7 @@ func (device *XrealDevice) Poll() error {
return fmt.Errorf("xreal is not enabled")
}
func (device *XrealDevice) RegisterEventListeners(*commons.AREventListener) error {
return fmt.Errorf("xreal is not enabled")
}
func (device *XrealDevice) RegisterEventListeners(*commons.AREventListener) {}
func New() (*XrealDevice, error) {
return nil, fmt.Errorf("xreal is not enabled")

View file

@ -0,0 +1,122 @@
//go:build xreal
// +build xreal
package xreal
// #cgo CFLAGS: -w
// #cgo pkg-config: json-c libusb-1.0 hidapi-libusb
// #include "go_ffi.h"
// #include "device.h"
// #include "device_imu.h"
// #include "device_mcu.h"
import "C"
import (
"fmt"
"sync"
"time"
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
)
var (
deviceEventHandlerMutex = sync.Mutex{}
deviceEventListener *commons.AREventListener
)
//export goIMUEventHandler
func goIMUEventHandler(_ C.uint64_t, event_type C.device_imu_event_type, ahrs *C.struct_device_imu_ahrs_t) {
if deviceEventListener == nil {
return
}
if event_type != C.DEVICE_IMU_EVENT_UPDATE {
return
}
orientation := C.device_imu_get_orientation(ahrs)
euler := C.device_imu_get_euler(orientation)
deviceEventListener.PitchCallback(float32(euler.pitch))
deviceEventListener.RollCallback(float32(euler.roll))
deviceEventListener.YawCallback(float32(euler.yaw))
}
// Implements commons.ARDevice
type XrealDevice struct {
eventListener *commons.AREventListener
imuDevice *C.struct_device_imu_t
deviceIsOpen bool
}
func (device *XrealDevice) Initialize() error {
if device.deviceIsOpen {
return fmt.Errorf("device is already open")
}
device.imuDevice = &C.struct_device_imu_t{}
// (*[0]byte) is a FUBAR way to cast a pointer to a function, but unsafe.Pointer doesn't work:
// cannot use unsafe.Pointer(_Cgo_ptr(_Cfpvar_fp_imuEventHandler)) (value of type unsafe.Pointer) as *[0]byte value in variable declaration
if C.DEVICE_IMU_ERROR_NO_ERROR != C.device_imu_open(device.imuDevice, (*[0]byte)(C.imuEventHandler)) {
return fmt.Errorf("failed to open IMU device")
}
C.device_imu_clear(device.imuDevice)
C.device_imu_calibrate(device.imuDevice, 1000, true, true, false)
device.deviceIsOpen = true
// let's hope this doesn't cause race conditions
go func() {
for device.eventListener == nil {
time.Sleep(time.Millisecond * 10)
}
for {
if !device.deviceIsOpen {
break
}
// I'm sorry.
deviceEventHandlerMutex.Lock()
deviceEventListener = device.eventListener
status := C.device_imu_read(device.imuDevice, -1)
deviceEventHandlerMutex.Unlock()
if status != C.DEVICE_IMU_ERROR_NO_ERROR {
break
}
}
device.deviceIsOpen = false
C.device_imu_close(device.imuDevice)
}()
return nil
}
func (device *XrealDevice) End() error {
if !device.deviceIsOpen {
return fmt.Errorf("device is not open")
}
C.device_imu_close(device.imuDevice)
device.deviceIsOpen = false
return nil
}
func (device *XrealDevice) IsPollingLibrary() bool {
return false
}
func (device *XrealDevice) IsEventBasedLibrary() bool {
return true
}
func (device *XrealDevice) Poll() error {
return fmt.Errorf("not supported")
}
func (device *XrealDevice) RegisterEventListeners(listener *commons.AREventListener) {
device.eventListener = listener
}

11
edidpatcher/LICENSE Normal file
View file

@ -0,0 +1,11 @@
Copyright (c) 2025 imterah.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -3,7 +3,6 @@ package edidpatcher
import (
"fmt"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
@ -38,7 +37,7 @@ func PatchEDIDToBeSpecialized(edid []byte) ([]byte, error) {
}
if newEDID[currentExtensionPosition+1] != 0x03 {
log.Warn("Incompatible version detected for ANSI CTA data section in EDID")
fmt.Println("WARN: Incompatible version detected for ANSI CTA data section in EDID")
}
foundExtensionBase = currentExtensionPosition

5
edidpatcher/go.mod Normal file
View file

@ -0,0 +1,5 @@
module git.terah.dev/UnrealXR/unrealxr/edidpatcher
go 1.24.3
require github.com/google/uuid v1.6.0

2
edidpatcher/go.sum Normal file
View file

@ -0,0 +1,2 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

View file

@ -27,12 +27,13 @@
xorg.libXinerama
xorg.libX11
# raylib X11 build dependencies (for dev-only non-XR builds)
libxkbcommon
# nreal driver build dependencies
hidapi
json_c
udev
libusb1
opencv
];
shellHook = ''
@ -40,5 +41,6 @@
mkdir -p "$PWD/data/config" "$PWD/data/data"
export UNREALXR_CONFIG_PATH="$PWD/data/config"
export UNREALXR_DATA_PATH="$PWD/data/data"
export UNREALXR_LOG_LEVEL="debug"
'';
}

View file

@ -1 +1,2 @@
#!/usr/bin/env bash
sudo UNREALXR_LOG_LEVEL="$UNREALXR_LOG_LEVEL" LD_LIBRARY_PATH="$LD_LIBRARY_PATH" UNREALXR_CONFIG_PATH="$UNREALXR_CONFIG_PATH" UNREALXR_DATA_PATH="$UNREALXR_DATA_PATH" WAYLAND_DISPLAY="$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" XDG_RUNTIME_DIR="/user/run/0" ./uxr