From 322e00b3b97242c39ad644f09e19cbc97c86d94f Mon Sep 17 00:00:00 2001 From: imterah Date: Thu, 26 Jun 2025 00:30:39 -0400 Subject: [PATCH 01/11] chore: Remove unused fields in the renderer metadata struct --- app/main.go | 15 +++++---------- app/renderer/struct.go | 2 -- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/app/main.go b/app/main.go index 471b548..436c1ab 100644 --- a/app/main.go +++ b/app/main.go @@ -158,19 +158,14 @@ func mainEntrypoint(context.Context, *cli.Command) error { displayBuffer := openedDevice.CreateBuffer(displayMetadata.MaxWidth, displayMetadata.MaxHeight, libevdi.StridePixelFormatRGBA32, displayRect) 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 } diff --git a/app/renderer/struct.go b/app/renderer/struct.go index 0acbde4..af19e77 100644 --- a/app/renderer/struct.go +++ b/app/renderer/struct.go @@ -9,6 +9,4 @@ type EvdiDisplayMetadata struct { Rect *libevdi.EvdiDisplayRect Buffer *libevdi.EvdiBuffer EventContext *libevdi.EvdiEventContext - ShouldRequestUpdate bool - IsUpdateReady bool } From 9f9c8b4be739a6abd4f44042d3b763c7bb179a7f Mon Sep 17 00:00:00 2001 From: imterah Date: Thu, 26 Jun 2025 17:44:36 -0400 Subject: [PATCH 02/11] feature: Add primitive version of multiple displays --- app/config/config.go | 14 +++++++++----- app/config/default_config.yml | 2 +- app/main.go | 8 +++++++- app/renderer/renderer.go | 34 ++++++++++++++++++++++++---------- app/renderer/struct.go | 8 ++++---- go.mod | 2 +- go.sum | 2 ++ 7 files changed, 48 insertions(+), 22 deletions(-) diff --git a/app/config/config.go b/app/config/config.go index 307a14b..1222abf 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -6,10 +6,10 @@ 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"` + Count *int `yaml:"count"` } type AppOverrides struct { @@ -28,6 +28,10 @@ func getPtrToInt(int int) *int { return &int } +func getPtrToFloat32(float32 float32) *float32 { + return &float32 +} + func getPtrToBool(bool bool) *bool { return &bool } @@ -36,7 +40,7 @@ var DefaultConfig = &Config{ DisplayConfig: DisplayConfig{ Angle: getPtrToInt(45), FOV: getPtrToInt(45), - Spacing: getPtrToInt(1), + Spacing: getPtrToFloat32(0.5), Count: getPtrToInt(3), }, Overrides: AppOverrides{ diff --git a/app/config/default_config.yml b/app/config/default_config.yml index a367a51..4938843 100644 --- a/app/config/default_config.yml +++ b/app/config/default_config.yml @@ -9,7 +9,7 @@ display: angle: 45 # Angle of the virtual displays fov: 45 # FOV of the 3D camera - spacing: 1 # Spacing between virtual displays + spacing: 0.5 # 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) diff --git a/app/main.go b/app/main.go index 436c1ab..239c49e 100644 --- a/app/main.go +++ b/app/main.go @@ -155,7 +155,13 @@ 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, diff --git a/app/renderer/renderer.go b/app/renderer/renderer.go index d1ebbf9..cfc4739 100644 --- a/app/renderer/renderer.go +++ b/app/renderer/renderer.go @@ -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 { @@ -125,7 +127,8 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa rl.DisableBackfaceCulling() rl.DisableDepthTest() - coreMesh := rl.GenMeshPlane(findOptimalHorizontalRes(float32(displayMetadata.MaxHeight), float32(displayMetadata.MaxWidth), verticalSize), verticalSize, 1, 1) + horizontalSize := findOptimalHorizontalRes(float32(displayMetadata.MaxHeight), float32(displayMetadata.MaxWidth), verticalSize) + coreMesh := rl.GenMeshPlane(horizontalSize, verticalSize, 1, 1) movementVector := rl.Vector3{ X: 0.0, @@ -156,7 +159,18 @@ 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) @@ -165,8 +179,10 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa rl.SetMaterialTexture(model.Materials, rl.MapAlbedo, texture) rects[i] = &TextureModelPair{ - Texture: texture, - Model: model, + Texture: texture, + Model: model, + CurrentAngle: currentAngle, + CurrentDisplaySpacing: currentDisplaySpacing, } } @@ -200,13 +216,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) @@ -223,7 +239,7 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa rl.DrawModelEx( rect.Model, rl.Vector3{ - X: 0, + X: rect.CurrentDisplaySpacing, Y: verticalSize / 2, Z: 0, }, @@ -241,8 +257,6 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa }, rl.White, ) - - break } rl.EndMode3D() diff --git a/app/renderer/struct.go b/app/renderer/struct.go index af19e77..d975d27 100644 --- a/app/renderer/struct.go +++ b/app/renderer/struct.go @@ -5,8 +5,8 @@ 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 } diff --git a/go.mod b/go.mod index aedc610..e631900 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.24.3 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 github.com/anoopengineer/edidparser v0.0.0-20240602223913-86ca9ed3d2b0 github.com/charmbracelet/log v0.4.2 github.com/goccy/go-yaml v1.18.0 @@ -15,6 +14,7 @@ require ( ) require ( + git.terah.dev/imterah/goevdi/libevdi v0.1.0-evdi1.14.10 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect diff --git a/go.sum b/go.sum index aad411c..a5b5c74 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ git.terah.dev/UnrealXR/raylib-go/raylib v0.55.2-0.20250623002739-1468af2636e1 h1 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= From eb33ec116e652890d449cefb874502d0e2e2a5fe Mon Sep 17 00:00:00 2001 From: imterah Date: Thu, 26 Jun 2025 18:25:37 -0400 Subject: [PATCH 03/11] chore: Remove culling hacks as they don't work :( --- app/renderer/renderer.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/renderer/renderer.go b/app/renderer/renderer.go index cfc4739..b6fd79f 100644 --- a/app/renderer/renderer.go +++ b/app/renderer/renderer.go @@ -122,11 +122,6 @@ 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) From 77792a82a6d202d69932e27cd6587257f915140a Mon Sep 17 00:00:00 2001 From: imterah Date: Thu, 26 Jun 2025 18:57:29 -0400 Subject: [PATCH 04/11] chore: Seperate Go modules into seperate directories This is done to help potential dependents reduce download sizes and only download what they need. --- Makefile | 4 ++-- app/LICENSE | 11 +++++++++++ go.mod => app/go.mod | 12 +++++++++--- go.sum => app/go.sum | 2 -- LICENSE => ardriver/LICENSE | 0 ardriver/go.mod | 3 +++ edidpatcher/LICENSE | 11 +++++++++++ edidpatcher/edidpatcher.go | 3 +-- edidpatcher/go.mod | 5 +++++ edidpatcher/go.sum | 2 ++ 10 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 app/LICENSE rename go.mod => app/go.mod (74%) rename go.sum => app/go.sum (96%) rename LICENSE => ardriver/LICENSE (100%) create mode 100644 ardriver/go.mod create mode 100644 edidpatcher/LICENSE create mode 100644 edidpatcher/go.mod create mode 100644 edidpatcher/go.sum diff --git a/Makefile b/Makefile index 0d040fb..96717c8 100644 --- a/Makefile +++ b/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,7 +7,7 @@ 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) . clean: rm -f $(OUTPUT) diff --git a/app/LICENSE b/app/LICENSE new file mode 100644 index 0000000..ea08705 --- /dev/null +++ b/app/LICENSE @@ -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. diff --git a/go.mod b/app/go.mod similarity index 74% rename from go.mod rename to app/go.mod index e631900..73ed20d 100644 --- a/go.mod +++ b/app/go.mod @@ -1,20 +1,25 @@ -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/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 ) require ( - git.terah.dev/imterah/goevdi/libevdi v0.1.0-evdi1.14.10 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect @@ -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 diff --git a/go.sum b/app/go.sum similarity index 96% rename from go.sum rename to app/go.sum index a5b5c74..4bee4dd 100644 --- a/go.sum +++ b/app/go.sum @@ -1,7 +1,5 @@ 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= diff --git a/LICENSE b/ardriver/LICENSE similarity index 100% rename from LICENSE rename to ardriver/LICENSE diff --git a/ardriver/go.mod b/ardriver/go.mod new file mode 100644 index 0000000..efd8c49 --- /dev/null +++ b/ardriver/go.mod @@ -0,0 +1,3 @@ +module git.terah.dev/UnrealXR/unrealxr/ardriver + +go 1.24.3 diff --git a/edidpatcher/LICENSE b/edidpatcher/LICENSE new file mode 100644 index 0000000..ea08705 --- /dev/null +++ b/edidpatcher/LICENSE @@ -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. diff --git a/edidpatcher/edidpatcher.go b/edidpatcher/edidpatcher.go index a56b386..097b59a 100644 --- a/edidpatcher/edidpatcher.go +++ b/edidpatcher/edidpatcher.go @@ -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 diff --git a/edidpatcher/go.mod b/edidpatcher/go.mod new file mode 100644 index 0000000..1774e05 --- /dev/null +++ b/edidpatcher/go.mod @@ -0,0 +1,5 @@ +module git.terah.dev/UnrealXR/unrealxr/edidpatcher + +go 1.24.3 + +require github.com/google/uuid v1.6.0 diff --git a/edidpatcher/go.sum b/edidpatcher/go.sum new file mode 100644 index 0000000..7790d7c --- /dev/null +++ b/edidpatcher/go.sum @@ -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= From f79657afc275b45e4d4660271736a870b9a764bd Mon Sep 17 00:00:00 2001 From: imterah Date: Thu, 26 Jun 2025 19:13:52 -0400 Subject: [PATCH 05/11] chore: Add shields --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 785e5a7..af2441f 100644 --- a/README.md +++ b/README.md @@ -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. From 8b482cecd5d60f05f5c7dd0a56b2248308066d8a Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 27 Jun 2025 12:26:54 -0400 Subject: [PATCH 06/11] feature: Add dummy mode --- HACKING.md | 23 ++++ Makefile | 3 + app/edidtools/bin/xreal-air-edid.bin | Bin 0 -> 128 bytes app/edidtools/patching_tools_fake_patching.go | 42 ++++++ app/edidtools/patching_tools_linux.go | 4 +- app/edidtools/patching_tools_macos.go | 4 +- app/edidtools/patching_tools_win.go | 4 +- app/edidtools/struct.go | 11 +- app/renderer/renderer.go | 28 ++-- ardriver/ardriver.go | 12 ++ ardriver/dummy/dummy_ar.go | 40 ++++++ ardriver/dummy/dummy_ar_disabled.go | 42 ++++++ ardriver/xreal/xreal.go | 120 +---------------- ardriver/xreal/xreal_disabled.go | 4 +- ardriver/xreal/{ => xrealsrc}/Fusion.h | 0 ardriver/xreal/{ => xrealsrc}/FusionAhrs.c | 0 ardriver/xreal/{ => xrealsrc}/FusionAhrs.h | 0 ardriver/xreal/{ => xrealsrc}/FusionAxes.h | 0 .../xreal/{ => xrealsrc}/FusionCalibration.h | 0 ardriver/xreal/{ => xrealsrc}/FusionCompass.c | 0 ardriver/xreal/{ => xrealsrc}/FusionCompass.h | 0 .../xreal/{ => xrealsrc}/FusionConvention.h | 0 ardriver/xreal/{ => xrealsrc}/FusionMath.h | 0 ardriver/xreal/{ => xrealsrc}/FusionOffset.c | 0 ardriver/xreal/{ => xrealsrc}/FusionOffset.h | 0 ardriver/xreal/{ => xrealsrc}/crc32.c | 0 ardriver/xreal/{ => xrealsrc}/crc32.h | 0 ardriver/xreal/{ => xrealsrc}/device.c | 0 ardriver/xreal/{ => xrealsrc}/device.h | 0 ardriver/xreal/{ => xrealsrc}/device_imu.c | 0 ardriver/xreal/{ => xrealsrc}/device_imu.h | 0 ardriver/xreal/{ => xrealsrc}/device_mcu.c | 0 ardriver/xreal/{ => xrealsrc}/device_mcu.h | 0 ardriver/xreal/{ => xrealsrc}/go_ffi.c | 0 ardriver/xreal/{ => xrealsrc}/go_ffi.h | 0 ardriver/xreal/{ => xrealsrc}/hid_ids.c | 0 ardriver/xreal/{ => xrealsrc}/hid_ids.h | 0 ardriver/xreal/xrealsrc/xreal_bindings.go | 122 ++++++++++++++++++ .../{ => xrealsrc}/xreal_debug_logging.go | 0 shell.nix | 5 +- 40 files changed, 323 insertions(+), 141 deletions(-) create mode 100644 HACKING.md create mode 100644 app/edidtools/bin/xreal-air-edid.bin create mode 100644 app/edidtools/patching_tools_fake_patching.go create mode 100644 ardriver/dummy/dummy_ar.go create mode 100644 ardriver/dummy/dummy_ar_disabled.go rename ardriver/xreal/{ => xrealsrc}/Fusion.h (100%) rename ardriver/xreal/{ => xrealsrc}/FusionAhrs.c (100%) rename ardriver/xreal/{ => xrealsrc}/FusionAhrs.h (100%) rename ardriver/xreal/{ => xrealsrc}/FusionAxes.h (100%) rename ardriver/xreal/{ => xrealsrc}/FusionCalibration.h (100%) rename ardriver/xreal/{ => xrealsrc}/FusionCompass.c (100%) rename ardriver/xreal/{ => xrealsrc}/FusionCompass.h (100%) rename ardriver/xreal/{ => xrealsrc}/FusionConvention.h (100%) rename ardriver/xreal/{ => xrealsrc}/FusionMath.h (100%) rename ardriver/xreal/{ => xrealsrc}/FusionOffset.c (100%) rename ardriver/xreal/{ => xrealsrc}/FusionOffset.h (100%) rename ardriver/xreal/{ => xrealsrc}/crc32.c (100%) rename ardriver/xreal/{ => xrealsrc}/crc32.h (100%) rename ardriver/xreal/{ => xrealsrc}/device.c (100%) rename ardriver/xreal/{ => xrealsrc}/device.h (100%) rename ardriver/xreal/{ => xrealsrc}/device_imu.c (100%) rename ardriver/xreal/{ => xrealsrc}/device_imu.h (100%) rename ardriver/xreal/{ => xrealsrc}/device_mcu.c (100%) rename ardriver/xreal/{ => xrealsrc}/device_mcu.h (100%) rename ardriver/xreal/{ => xrealsrc}/go_ffi.c (100%) rename ardriver/xreal/{ => xrealsrc}/go_ffi.h (100%) rename ardriver/xreal/{ => xrealsrc}/hid_ids.c (100%) rename ardriver/xreal/{ => xrealsrc}/hid_ids.h (100%) create mode 100644 ardriver/xreal/xrealsrc/xreal_bindings.go rename ardriver/xreal/{ => xrealsrc}/xreal_debug_logging.go (100%) diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 0000000..2b0e0b9 --- /dev/null +++ b/HACKING.md @@ -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. diff --git a/Makefile b/Makefile index 96717c8..d1c6536 100644 --- a/Makefile +++ b/Makefile @@ -9,5 +9,8 @@ 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) . + clean: rm -f $(OUTPUT) diff --git a/app/edidtools/bin/xreal-air-edid.bin b/app/edidtools/bin/xreal-air-edid.bin new file mode 100644 index 0000000000000000000000000000000000000000..ca9ea41d323c859ce33aa7d42723cf3d20614ab1 GIT binary patch literal 128 zcmZSh4+aco?nZ_TX=!O3ij2$+JnR)*^Byb@@tCPB5D>z^z`%$Gn5-Hk3N3UUB6M6C f8Z1f}80474(M4EF&_#d-{b6v-EaFmt0fu`3e-$19 literal 0 HcmV?d00001 diff --git a/app/edidtools/patching_tools_fake_patching.go b/app/edidtools/patching_tools_fake_patching.go new file mode 100644 index 0000000..e0175d5 --- /dev/null +++ b/app/edidtools/patching_tools_fake_patching.go @@ -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 +} diff --git a/app/edidtools/patching_tools_linux.go b/app/edidtools/patching_tools_linux.go index cea8cbd..7323021 100644 --- a/app/edidtools/patching_tools_linux.go +++ b/app/edidtools/patching_tools_linux.go @@ -1,5 +1,5 @@ -//go:build linux -// +build linux +//go:build linux && !fake_edid_patching +// +build linux,!fake_edid_patching package edidtools diff --git a/app/edidtools/patching_tools_macos.go b/app/edidtools/patching_tools_macos.go index d41de0b..625baf4 100644 --- a/app/edidtools/patching_tools_macos.go +++ b/app/edidtools/patching_tools_macos.go @@ -1,5 +1,5 @@ -//go:build darwin -// +build darwin +//go:build darwin && !fake_edid_patching +// +build darwin,!fake_edid_patching package edidtools diff --git a/app/edidtools/patching_tools_win.go b/app/edidtools/patching_tools_win.go index e9f515f..54d2a90 100644 --- a/app/edidtools/patching_tools_win.go +++ b/app/edidtools/patching_tools_win.go @@ -1,5 +1,5 @@ -//go:build windows -// +build windows +//go:build windows && !fake_edid_patching +// +build windows,!fake_edid_patching package edidtools diff --git a/app/edidtools/struct.go b/app/edidtools/struct.go index 361c440..e0b57d7 100644 --- a/app/edidtools/struct.go +++ b/app/edidtools/struct.go @@ -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 { diff --git a/app/renderer/renderer.go b/app/renderer/renderer.go index b6fd79f..8e2f3e3 100644 --- a/app/renderer/renderer.go +++ b/app/renderer/renderer.go @@ -184,20 +184,24 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa 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() diff --git a/ardriver/ardriver.go b/ardriver/ardriver.go index 913e40a..51c45dd 100644 --- a/ardriver/ardriver.go +++ b/ardriver/ardriver.go @@ -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") } diff --git a/ardriver/dummy/dummy_ar.go b/ardriver/dummy/dummy_ar.go new file mode 100644 index 0000000..d7bd00a --- /dev/null +++ b/ardriver/dummy/dummy_ar.go @@ -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 +} diff --git a/ardriver/dummy/dummy_ar_disabled.go b/ardriver/dummy/dummy_ar_disabled.go new file mode 100644 index 0000000..8686db5 --- /dev/null +++ b/ardriver/dummy/dummy_ar_disabled.go @@ -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") +} diff --git a/ardriver/xreal/xreal.go b/ardriver/xreal/xreal.go index 08343b3..7e2dce4 100644 --- a/ardriver/xreal/xreal.go +++ b/ardriver/xreal/xreal.go @@ -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 { diff --git a/ardriver/xreal/xreal_disabled.go b/ardriver/xreal/xreal_disabled.go index c6ca903..e9d1372 100644 --- a/ardriver/xreal/xreal_disabled.go +++ b/ardriver/xreal/xreal_disabled.go @@ -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") diff --git a/ardriver/xreal/Fusion.h b/ardriver/xreal/xrealsrc/Fusion.h similarity index 100% rename from ardriver/xreal/Fusion.h rename to ardriver/xreal/xrealsrc/Fusion.h diff --git a/ardriver/xreal/FusionAhrs.c b/ardriver/xreal/xrealsrc/FusionAhrs.c similarity index 100% rename from ardriver/xreal/FusionAhrs.c rename to ardriver/xreal/xrealsrc/FusionAhrs.c diff --git a/ardriver/xreal/FusionAhrs.h b/ardriver/xreal/xrealsrc/FusionAhrs.h similarity index 100% rename from ardriver/xreal/FusionAhrs.h rename to ardriver/xreal/xrealsrc/FusionAhrs.h diff --git a/ardriver/xreal/FusionAxes.h b/ardriver/xreal/xrealsrc/FusionAxes.h similarity index 100% rename from ardriver/xreal/FusionAxes.h rename to ardriver/xreal/xrealsrc/FusionAxes.h diff --git a/ardriver/xreal/FusionCalibration.h b/ardriver/xreal/xrealsrc/FusionCalibration.h similarity index 100% rename from ardriver/xreal/FusionCalibration.h rename to ardriver/xreal/xrealsrc/FusionCalibration.h diff --git a/ardriver/xreal/FusionCompass.c b/ardriver/xreal/xrealsrc/FusionCompass.c similarity index 100% rename from ardriver/xreal/FusionCompass.c rename to ardriver/xreal/xrealsrc/FusionCompass.c diff --git a/ardriver/xreal/FusionCompass.h b/ardriver/xreal/xrealsrc/FusionCompass.h similarity index 100% rename from ardriver/xreal/FusionCompass.h rename to ardriver/xreal/xrealsrc/FusionCompass.h diff --git a/ardriver/xreal/FusionConvention.h b/ardriver/xreal/xrealsrc/FusionConvention.h similarity index 100% rename from ardriver/xreal/FusionConvention.h rename to ardriver/xreal/xrealsrc/FusionConvention.h diff --git a/ardriver/xreal/FusionMath.h b/ardriver/xreal/xrealsrc/FusionMath.h similarity index 100% rename from ardriver/xreal/FusionMath.h rename to ardriver/xreal/xrealsrc/FusionMath.h diff --git a/ardriver/xreal/FusionOffset.c b/ardriver/xreal/xrealsrc/FusionOffset.c similarity index 100% rename from ardriver/xreal/FusionOffset.c rename to ardriver/xreal/xrealsrc/FusionOffset.c diff --git a/ardriver/xreal/FusionOffset.h b/ardriver/xreal/xrealsrc/FusionOffset.h similarity index 100% rename from ardriver/xreal/FusionOffset.h rename to ardriver/xreal/xrealsrc/FusionOffset.h diff --git a/ardriver/xreal/crc32.c b/ardriver/xreal/xrealsrc/crc32.c similarity index 100% rename from ardriver/xreal/crc32.c rename to ardriver/xreal/xrealsrc/crc32.c diff --git a/ardriver/xreal/crc32.h b/ardriver/xreal/xrealsrc/crc32.h similarity index 100% rename from ardriver/xreal/crc32.h rename to ardriver/xreal/xrealsrc/crc32.h diff --git a/ardriver/xreal/device.c b/ardriver/xreal/xrealsrc/device.c similarity index 100% rename from ardriver/xreal/device.c rename to ardriver/xreal/xrealsrc/device.c diff --git a/ardriver/xreal/device.h b/ardriver/xreal/xrealsrc/device.h similarity index 100% rename from ardriver/xreal/device.h rename to ardriver/xreal/xrealsrc/device.h diff --git a/ardriver/xreal/device_imu.c b/ardriver/xreal/xrealsrc/device_imu.c similarity index 100% rename from ardriver/xreal/device_imu.c rename to ardriver/xreal/xrealsrc/device_imu.c diff --git a/ardriver/xreal/device_imu.h b/ardriver/xreal/xrealsrc/device_imu.h similarity index 100% rename from ardriver/xreal/device_imu.h rename to ardriver/xreal/xrealsrc/device_imu.h diff --git a/ardriver/xreal/device_mcu.c b/ardriver/xreal/xrealsrc/device_mcu.c similarity index 100% rename from ardriver/xreal/device_mcu.c rename to ardriver/xreal/xrealsrc/device_mcu.c diff --git a/ardriver/xreal/device_mcu.h b/ardriver/xreal/xrealsrc/device_mcu.h similarity index 100% rename from ardriver/xreal/device_mcu.h rename to ardriver/xreal/xrealsrc/device_mcu.h diff --git a/ardriver/xreal/go_ffi.c b/ardriver/xreal/xrealsrc/go_ffi.c similarity index 100% rename from ardriver/xreal/go_ffi.c rename to ardriver/xreal/xrealsrc/go_ffi.c diff --git a/ardriver/xreal/go_ffi.h b/ardriver/xreal/xrealsrc/go_ffi.h similarity index 100% rename from ardriver/xreal/go_ffi.h rename to ardriver/xreal/xrealsrc/go_ffi.h diff --git a/ardriver/xreal/hid_ids.c b/ardriver/xreal/xrealsrc/hid_ids.c similarity index 100% rename from ardriver/xreal/hid_ids.c rename to ardriver/xreal/xrealsrc/hid_ids.c diff --git a/ardriver/xreal/hid_ids.h b/ardriver/xreal/xrealsrc/hid_ids.h similarity index 100% rename from ardriver/xreal/hid_ids.h rename to ardriver/xreal/xrealsrc/hid_ids.h diff --git a/ardriver/xreal/xrealsrc/xreal_bindings.go b/ardriver/xreal/xrealsrc/xreal_bindings.go new file mode 100644 index 0000000..e3b435f --- /dev/null +++ b/ardriver/xreal/xrealsrc/xreal_bindings.go @@ -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 +} diff --git a/ardriver/xreal/xreal_debug_logging.go b/ardriver/xreal/xrealsrc/xreal_debug_logging.go similarity index 100% rename from ardriver/xreal/xreal_debug_logging.go rename to ardriver/xreal/xrealsrc/xreal_debug_logging.go diff --git a/shell.nix b/shell.nix index b1e1b78..4ad5044 100644 --- a/shell.nix +++ b/shell.nix @@ -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 = '' From f16b35f102c8a3d03a9144e47eaed4f9f558503e Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 27 Jun 2025 12:27:27 -0400 Subject: [PATCH 07/11] chore: Add reference to hacking guide in the README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index af2441f..501a32b 100644 --- a/README.md +++ b/README.md @@ -33,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). From 59dbdf9eb0b4153de22bd9499ce633f8afff3a63 Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 27 Jun 2025 12:29:47 -0400 Subject: [PATCH 08/11] chore: Make devshell default to debug log level --- shell.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/shell.nix b/shell.nix index 4ad5044..23e7db2 100644 --- a/shell.nix +++ b/shell.nix @@ -41,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" ''; } From 30e90af2761354314681839ef19849ce8b2a6948 Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 27 Jun 2025 12:48:25 -0400 Subject: [PATCH 09/11] 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. --- app/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.go b/app/main.go index 239c49e..f84df7a 100644 --- a/app/main.go +++ b/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 * 2)) + rl.SetTargetFPS(int32(displayMetadata.MaxRefreshRate)) rl.InitWindow(int32(displayMetadata.MaxWidth), int32(displayMetadata.MaxHeight), "UnrealXR") atexit.Register(func() { From 6911afb6e0845bc28b312c1180339d6e5c20813a Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 27 Jun 2025 12:50:29 -0400 Subject: [PATCH 10/11] fix: Adds missing script headers on unrealxr startup script --- unrealxr | 1 + 1 file changed, 1 insertion(+) diff --git a/unrealxr b/unrealxr index 512cfa2..5c13589 100755 --- a/unrealxr +++ b/unrealxr @@ -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 From 0efaf19b40ec6b1560f63f5376e2961311c89bb0 Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 27 Jun 2025 22:08:51 -0400 Subject: [PATCH 11/11] feature: Implement display rotation --- app/config/config.go | 20 ++++++----- app/config/default_config.yml | 4 ++- app/renderer/renderer.go | 68 ++++++++++++++++++++++++++++++----- 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/app/config/config.go b/app/config/config.go index 1222abf..5938650 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -6,10 +6,12 @@ import _ "embed" var InitialConfig []byte type DisplayConfig struct { - Angle *int `yaml:"angle"` - FOV *int `yaml:"fov"` - Spacing *float32 `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 { @@ -38,10 +40,12 @@ func getPtrToBool(bool bool) *bool { var DefaultConfig = &Config{ DisplayConfig: DisplayConfig{ - Angle: getPtrToInt(45), - FOV: getPtrToInt(45), - Spacing: getPtrToFloat32(0.5), - 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), diff --git a/app/config/default_config.yml b/app/config/default_config.yml index 4938843..3daeee3 100644 --- a/app/config/default_config.yml +++ b/app/config/default_config.yml @@ -9,7 +9,9 @@ display: angle: 45 # Angle of the virtual displays fov: 45 # FOV of the 3D camera - spacing: 0.5 # 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) diff --git a/app/renderer/renderer.go b/app/renderer/renderer.go index 8e2f3e3..79cda44 100644 --- a/app/renderer/renderer.go +++ b/app/renderer/renderer.go @@ -35,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() @@ -99,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( @@ -125,6 +136,21 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa horizontalSize := findOptimalHorizontalRes(float32(displayMetadata.MaxHeight), float32(displayMetadata.MaxWidth), verticalSize) coreMesh := rl.GenMeshPlane(horizontalSize, 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, Y: 0.0, @@ -171,6 +197,17 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa 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{ @@ -235,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: rect.CurrentDisplaySpacing, - 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,