Compare commits
No commits in common. "main" and "go-rewrite" have entirely different histories.
main
...
go-rewrite
55 changed files with 198 additions and 497 deletions
23
HACKING.md
23
HACKING.md
|
@ -1,23 +0,0 @@
|
|||
# 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.
|
7
Makefile
7
Makefile
|
@ -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,10 +7,7 @@ TAGS := xreal noaudio drm drm_leasing drm_disable_input
|
|||
all: build
|
||||
|
||||
build:
|
||||
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) .
|
||||
go build -v -tags '$(TAGS)' -o $(OUTPUT) $(APP_DIR)
|
||||
|
||||
clean:
|
||||
rm -f $(OUTPUT)
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
# UnrealXR
|
||||
[](https://git.terah.dev/UnrealXR/unrealxr/actions)
|
||||
[](https://godoc.org/git.terah.dev/UnrealXR/unrealxr)
|
||||
[](https://goreportcard.com/report/git.terah.dev/UnrealXR/unrealxr/app)
|
||||
[](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.
|
||||
|
||||
|
@ -33,7 +29,3 @@ 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
11
app/LICENSE
|
@ -1,11 +0,0 @@
|
|||
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.
|
|
@ -6,12 +6,10 @@ import _ "embed"
|
|||
var InitialConfig []byte
|
||||
|
||||
type DisplayConfig struct {
|
||||
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"`
|
||||
Angle *int `yaml:"angle"`
|
||||
FOV *int `yaml:"fov"`
|
||||
Spacing *int `yaml:"spacing"`
|
||||
Count *int `yaml:"count"`
|
||||
}
|
||||
|
||||
type AppOverrides struct {
|
||||
|
@ -30,22 +28,16 @@ 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: getPtrToFloat32(0.5),
|
||||
RadiusMultiplier: getPtrToFloat32(2),
|
||||
UseCircularSpacing: getPtrToBool(true),
|
||||
Count: getPtrToInt(3),
|
||||
Angle: getPtrToInt(45),
|
||||
FOV: getPtrToInt(45),
|
||||
Spacing: getPtrToInt(1),
|
||||
Count: getPtrToInt(3),
|
||||
},
|
||||
Overrides: AppOverrides{
|
||||
AllowUnsupportedDevices: getPtrToBool(false),
|
||||
|
|
|
@ -9,9 +9,7 @@
|
|||
display:
|
||||
angle: 45 # Angle of the virtual displays
|
||||
fov: 45 # FOV of the 3D camera
|
||||
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.
|
||||
spacing: 1 # Spacing between 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.
|
@ -1,42 +0,0 @@
|
|||
//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
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//go:build linux && !fake_edid_patching
|
||||
// +build linux,!fake_edid_patching
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package edidtools
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//go:build darwin && !fake_edid_patching
|
||||
// +build darwin,!fake_edid_patching
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package edidtools
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//go:build windows && !fake_edid_patching
|
||||
// +build windows,!fake_edid_patching
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package edidtools
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
package edidtools
|
||||
|
||||
type DisplayQuirks struct {
|
||||
MaxWidth int
|
||||
MaxHeight int
|
||||
MaxRefreshRate int
|
||||
SensorInitDelay int
|
||||
ZVectorDisabled bool
|
||||
UsesMouseMovement bool
|
||||
MaxWidth int
|
||||
MaxHeight int
|
||||
MaxRefreshRate int
|
||||
SensorInitDelay int
|
||||
ZVectorDisabled bool
|
||||
}
|
||||
|
||||
type DisplayMetadata struct {
|
||||
|
|
25
app/main.go
25
app/main.go
|
@ -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))
|
||||
rl.SetTargetFPS(int32(displayMetadata.MaxRefreshRate * 2))
|
||||
rl.InitWindow(int32(displayMetadata.MaxWidth), int32(displayMetadata.MaxHeight), "UnrealXR")
|
||||
|
||||
atexit.Register(func() {
|
||||
|
@ -155,23 +155,22 @@ func mainEntrypoint(context.Context, *cli.Command) error {
|
|||
Y2: displayMetadata.MaxHeight,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
displayBuffer := openedDevice.CreateBuffer(displayMetadata.MaxWidth, displayMetadata.MaxHeight, libevdi.StridePixelFormatRGBA32, displayRect)
|
||||
|
||||
displayMetadata := &renderer.EvdiDisplayMetadata{
|
||||
EvdiNode: openedDevice,
|
||||
Rect: displayRect,
|
||||
Buffer: displayBuffer,
|
||||
EvdiNode: openedDevice,
|
||||
Rect: displayRect,
|
||||
Buffer: displayBuffer,
|
||||
ShouldRequestUpdate: true,
|
||||
}
|
||||
|
||||
displayMetadata.EventContext = &libevdi.EvdiEventContext{}
|
||||
openedDevice.RegisterEventHandler(displayMetadata.EventContext)
|
||||
displayMetadata.EventContext = &libevdi.EvdiEventContext{
|
||||
UpdateReadyHandler: func(bufferToBeUpdated int) {
|
||||
displayMetadata.IsUpdateReady = true
|
||||
},
|
||||
}
|
||||
|
||||
openedDevice.RegisterEventHandler(displayMetadata.EventContext)
|
||||
evdiCards[currentDisplay] = displayMetadata
|
||||
}
|
||||
|
||||
|
|
|
@ -17,10 +17,8 @@ import (
|
|||
)
|
||||
|
||||
type TextureModelPair struct {
|
||||
Texture rl.Texture2D
|
||||
Model rl.Model
|
||||
CurrentAngle float32
|
||||
CurrentDisplaySpacing float32
|
||||
Texture rl.Texture2D
|
||||
Model rl.Model
|
||||
}
|
||||
|
||||
func findMaxVerticalSize(fovyDeg float32, distance float32) float32 {
|
||||
|
@ -35,15 +33,6 @@ 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()
|
||||
|
@ -108,9 +97,7 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa
|
|||
|
||||
headset.RegisterEventListeners(arEventListner)
|
||||
|
||||
fovY := float32(*config.DisplayConfig.FOV)
|
||||
fovX := findHfovFromVfov(float64(fovY), float64(displayMetadata.MaxWidth), float64(displayMetadata.MaxHeight))
|
||||
|
||||
fovY := float32(45.0)
|
||||
verticalSize := findMaxVerticalSize(fovY, 5.0)
|
||||
|
||||
camera := rl.NewCamera3D(
|
||||
|
@ -133,23 +120,12 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa
|
|||
rl.CameraPerspective,
|
||||
)
|
||||
|
||||
horizontalSize := findOptimalHorizontalRes(float32(displayMetadata.MaxHeight), float32(displayMetadata.MaxWidth), verticalSize)
|
||||
coreMesh := rl.GenMeshPlane(horizontalSize, verticalSize, 1, 1)
|
||||
// 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()
|
||||
|
||||
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
|
||||
}
|
||||
coreMesh := rl.GenMeshPlane(findOptimalHorizontalRes(float32(displayMetadata.MaxHeight), float32(displayMetadata.MaxWidth), verticalSize), verticalSize, 1, 1)
|
||||
|
||||
movementVector := rl.Vector3{
|
||||
X: 0.0,
|
||||
|
@ -180,65 +156,37 @@ 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,
|
||||
CurrentAngle: currentAngle,
|
||||
CurrentDisplaySpacing: currentDisplaySpacing,
|
||||
Texture: texture,
|
||||
Model: model,
|
||||
}
|
||||
}
|
||||
|
||||
eventTimeoutDuration := 0 * time.Millisecond
|
||||
|
||||
for !rl.WindowShouldClose() {
|
||||
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)
|
||||
if hasSensorInitDelayQuirk {
|
||||
if time.Now().Sub(sensorInitStartTime) > time.Duration(displayMetadata.DeviceQuirks.SensorInitDelay)*time.Second {
|
||||
log.Info("Movement is now enabled.")
|
||||
hasSensorInitDelayQuirk = false
|
||||
}
|
||||
} else {
|
||||
rl.UpdateCamera(&camera, rl.CameraFirstPerson)
|
||||
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.BeginDrawing()
|
||||
|
@ -252,13 +200,13 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa
|
|||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to wait for display events: %s", err.Error())
|
||||
continue
|
||||
break
|
||||
}
|
||||
|
||||
if ready {
|
||||
if err := card.EvdiNode.HandleEvents(card.EventContext); err != nil {
|
||||
log.Errorf("Failed to handle display events: %s", err.Error())
|
||||
continue
|
||||
break
|
||||
}
|
||||
|
||||
card.EvdiNode.GrabPixels(card.Rect)
|
||||
|
@ -272,35 +220,20 @@ 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,
|
||||
worldPos,
|
||||
// rotate around X to make it vertical
|
||||
rl.Vector3{
|
||||
X: 0,
|
||||
Y: verticalSize / 2,
|
||||
Z: 0,
|
||||
},
|
||||
// rotate around X to make it vertical
|
||||
rl.Vector3{
|
||||
X: 1,
|
||||
Y: 0,
|
||||
Z: 0,
|
||||
},
|
||||
0,
|
||||
90,
|
||||
rl.Vector3{
|
||||
X: 1,
|
||||
Y: 1,
|
||||
|
@ -308,6 +241,8 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa
|
|||
},
|
||||
rl.White,
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
rl.EndMode3D()
|
||||
|
|
|
@ -5,8 +5,10 @@ import (
|
|||
)
|
||||
|
||||
type EvdiDisplayMetadata struct {
|
||||
EvdiNode *libevdi.EvdiNode
|
||||
Rect *libevdi.EvdiDisplayRect
|
||||
Buffer *libevdi.EvdiBuffer
|
||||
EventContext *libevdi.EvdiEventContext
|
||||
EvdiNode *libevdi.EvdiNode
|
||||
Rect *libevdi.EvdiDisplayRect
|
||||
Buffer *libevdi.EvdiBuffer
|
||||
EventContext *libevdi.EvdiEventContext
|
||||
ShouldRequestUpdate bool
|
||||
IsUpdateReady bool
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"fmt"
|
||||
|
||||
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
|
||||
"git.terah.dev/UnrealXR/unrealxr/ardriver/dummy"
|
||||
"git.terah.dev/UnrealXR/unrealxr/ardriver/xreal"
|
||||
)
|
||||
|
||||
|
@ -20,16 +19,5 @@ 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")
|
||||
}
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
//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
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
//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")
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
module git.terah.dev/UnrealXR/unrealxr/ardriver
|
||||
|
||||
go 1.24.3
|
|
@ -3,21 +3,127 @@
|
|||
|
||||
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 (
|
||||
xreal "git.terah.dev/UnrealXR/unrealxr/ardriver/xreal/xrealsrc"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
|
||||
)
|
||||
|
||||
var IsXrealEnabled = true
|
||||
var (
|
||||
IsXrealEnabled = true
|
||||
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 {
|
||||
*xreal.XrealDevice
|
||||
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
|
||||
}
|
||||
|
||||
func New() (*XrealDevice, error) {
|
||||
device := &XrealDevice{
|
||||
XrealDevice: &xreal.XrealDevice{},
|
||||
}
|
||||
|
||||
device := &XrealDevice{}
|
||||
err := device.Initialize()
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -35,7 +35,9 @@ func (device *XrealDevice) Poll() error {
|
|||
return fmt.Errorf("xreal is not enabled")
|
||||
}
|
||||
|
||||
func (device *XrealDevice) RegisterEventListeners(*commons.AREventListener) {}
|
||||
func (device *XrealDevice) RegisterEventListeners(*commons.AREventListener) error {
|
||||
return fmt.Errorf("xreal is not enabled")
|
||||
}
|
||||
|
||||
func New() (*XrealDevice, error) {
|
||||
return nil, fmt.Errorf("xreal is not enabled")
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
//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
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
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.
|
|
@ -3,6 +3,7 @@ package edidpatcher
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
|
@ -37,7 +38,7 @@ func PatchEDIDToBeSpecialized(edid []byte) ([]byte, error) {
|
|||
}
|
||||
|
||||
if newEDID[currentExtensionPosition+1] != 0x03 {
|
||||
fmt.Println("WARN: Incompatible version detected for ANSI CTA data section in EDID")
|
||||
log.Warn("Incompatible version detected for ANSI CTA data section in EDID")
|
||||
}
|
||||
|
||||
foundExtensionBase = currentExtensionPosition
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
module git.terah.dev/UnrealXR/unrealxr/edidpatcher
|
||||
|
||||
go 1.24.3
|
||||
|
||||
require github.com/google/uuid v1.6.0
|
|
@ -1,2 +0,0 @@
|
|||
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=
|
|
@ -1,19 +1,14 @@
|
|||
module git.terah.dev/UnrealXR/unrealxr/app
|
||||
module git.terah.dev/UnrealXR/unrealxr
|
||||
|
||||
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/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
|
||||
git.terah.dev/imterah/goevdi v1.14.11-0.20250626004148-bdbef2a68ff9
|
||||
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
|
||||
|
@ -28,7 +23,6 @@ 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
|
|
@ -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/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=
|
||||
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=
|
||||
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=
|
|
@ -27,13 +27,12 @@
|
|||
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 = ''
|
||||
|
@ -41,6 +40,5 @@
|
|||
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"
|
||||
'';
|
||||
}
|
||||
|
|
1
unrealxr
1
unrealxr
|
@ -1,2 +1 @@
|
|||
#!/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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue