feature: Get initial multi-display working in Go

This commit is contained in:
Tera << 8 2025-06-25 21:48:40 -04:00
parent 243d595a35
commit 47e693a7b9
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
5 changed files with 261 additions and 35 deletions

View file

@ -8,6 +8,7 @@ import (
"os/signal"
"path"
"syscall"
"time"
libconfig "git.terah.dev/UnrealXR/unrealxr/app/config"
"git.terah.dev/UnrealXR/unrealxr/app/edidtools"
@ -154,15 +155,28 @@ func mainEntrypoint(context.Context, *cli.Command) error {
Y2: displayMetadata.MaxHeight,
}
displayBuffer := openedDevice.CreateBuffer(displayMetadata.MaxWidth, displayMetadata.MaxHeight, 4, displayRect)
displayBuffer := openedDevice.CreateBuffer(displayMetadata.MaxWidth, displayMetadata.MaxHeight, libevdi.StridePixelFormatRGBA32, displayRect)
evdiCards[currentDisplay] = &renderer.EvdiDisplayMetadata{
EvdiNode: openedDevice,
Rect: displayRect,
Buffer: displayBuffer,
displayMetadata := &renderer.EvdiDisplayMetadata{
EvdiNode: openedDevice,
Rect: displayRect,
Buffer: displayBuffer,
ShouldRequestUpdate: true,
}
displayMetadata.EventContext = &libevdi.EvdiEventContext{
UpdateReadyHandler: func(bufferToBeUpdated int) {
displayMetadata.IsUpdateReady = true
},
}
openedDevice.RegisterEventHandler(displayMetadata.EventContext)
evdiCards[currentDisplay] = displayMetadata
}
// HACK: sometimes the buffer doesn't get initialized properly if we don't wait a bit...
time.Sleep(time.Millisecond * 100)
log.Info("Initialized displays. Entering rendering loop")
renderer.EnterRenderLoop(config, displayMetadata, evdiCards)

View file

@ -1,7 +1,10 @@
package renderer
import (
"image/color"
"math"
"time"
"unsafe"
libconfig "git.terah.dev/UnrealXR/unrealxr/app/config"
"git.terah.dev/UnrealXR/unrealxr/app/edidtools"
@ -9,8 +12,27 @@ import (
arcommons "git.terah.dev/UnrealXR/unrealxr/ardriver/commons"
"github.com/charmbracelet/log"
"github.com/tebeka/atexit"
rl "git.terah.dev/UnrealXR/raylib-go/raylib"
)
type TextureModelPair struct {
Texture rl.Texture2D
Model rl.Model
}
func findMaxVerticalSize(fovyDeg float32, distance float32) float32 {
fovyRad := float64(fovyDeg * math.Pi / 180.0)
return 2 * distance * float32(math.Tan(fovyRad/2))
}
func findOptimalHorizontalRes(verticalDisplayRes float32, horizontalDisplayRes float32, verticalSize float32) float32 {
aspectRatio := horizontalDisplayRes / verticalDisplayRes
horizontalSize := verticalSize * aspectRatio
return horizontalSize
}
func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.DisplayMetadata, evdiCards []*EvdiDisplayMetadata) {
log.Info("Initializing AR driver")
headset, err := ardriver.GetDevice()
@ -19,21 +41,52 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa
log.Errorf("Failed to get device: %s", err.Error())
atexit.Exit(1)
}
log.Info("Initialized")
var pitch float32
var yaw float32
var roll float32
var (
currentPitch float32
previousPitch float32
currentYaw float32
previousYaw float32
currentRoll float32
previousRoll float32
hasGottenPitchCallbackBefore bool
hasGottenYawCallbackBefore bool
hasGottenRollCallbackBefore bool
)
arEventListner := &arcommons.AREventListener{
PitchCallback: func(newPitch float32) {
pitch = newPitch
if !hasGottenPitchCallbackBefore {
hasGottenPitchCallbackBefore = true
currentPitch = newPitch
previousPitch = newPitch
} else {
previousPitch = currentPitch
currentPitch = newPitch
}
},
YawCallback: func(newYaw float32) {
yaw = newYaw
if !hasGottenYawCallbackBefore {
hasGottenYawCallbackBefore = true
currentYaw = newYaw
previousYaw = newYaw
} else {
previousYaw = currentYaw
currentYaw = newYaw
}
},
RollCallback: func(newRoll float32) {
roll = newRoll
if !hasGottenRollCallbackBefore {
hasGottenRollCallbackBefore = true
currentRoll = newRoll
previousRoll = newRoll
} else {
previousRoll = currentRoll
currentRoll = newRoll
}
},
}
@ -44,8 +97,158 @@ func EnterRenderLoop(config *libconfig.Config, displayMetadata *edidtools.Displa
headset.RegisterEventListeners(arEventListner)
for {
log.Debugf("pitch: %f, yaw: %f, roll: %f", pitch, yaw, roll)
time.Sleep(10 * time.Millisecond)
fovY := float32(45.0)
verticalSize := findMaxVerticalSize(fovY, 5.0)
camera := rl.NewCamera3D(
rl.Vector3{
X: 0.0,
Y: verticalSize / 2,
Z: 5.0,
},
rl.Vector3{
X: 0.0,
Y: verticalSize / 2,
Z: 0.0,
},
rl.Vector3{
X: 0.0,
Y: 1.0,
Z: 0.0,
},
fovY,
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()
coreMesh := rl.GenMeshPlane(findOptimalHorizontalRes(float32(displayMetadata.MaxHeight), float32(displayMetadata.MaxWidth), verticalSize), verticalSize, 1, 1)
movementVector := rl.Vector3{
X: 0.0,
Y: 0.0,
Z: 0.0,
}
lookVector := rl.Vector3{
X: 0.0,
Y: 0.0,
Z: 0.0,
}
hasZVectorDisabledQuirk := false
hasSensorInitDelayQuirk := false
sensorInitStartTime := time.Now()
if displayMetadata.DeviceQuirks.ZVectorDisabled {
log.Warn("QUIRK: The Z vector has been disabled for your specific device")
hasZVectorDisabledQuirk = true
}
if displayMetadata.DeviceQuirks.SensorInitDelay != 0 {
log.Warnf("QUIRK: Waiting %d second(s) before reading sensors", displayMetadata.DeviceQuirks.SensorInitDelay)
log.Warn("|| MOVEMENT WILL NOT BE OPERATIONAL DURING THIS TIME. ||")
hasSensorInitDelayQuirk = true
}
rects := make([]*TextureModelPair, len(evdiCards))
for i, card := range evdiCards {
image := rl.NewImage(card.Buffer.Buffer, int32(displayMetadata.MaxWidth), int32(displayMetadata.MaxHeight), 1, rl.UncompressedR8g8b8a8)
texture := rl.LoadTextureFromImage(image)
model := rl.LoadModelFromMesh(coreMesh)
rl.SetMaterialTexture(model.Materials, rl.MapAlbedo, texture)
rects[i] = &TextureModelPair{
Texture: texture,
Model: model,
}
}
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
}
} 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.BeginDrawing()
rl.ClearBackground(rl.Black)
rl.BeginMode3D(camera)
for rectPos, rect := range rects {
card := evdiCards[rectPos]
ready, err := card.EvdiNode.WaitUntilEventsAreReadyToHandle(eventTimeoutDuration)
if err != nil {
log.Errorf("Failed to wait for display events: %s", err.Error())
break
}
if ready {
if err := card.EvdiNode.HandleEvents(card.EventContext); err != nil {
log.Errorf("Failed to handle display events: %s", err.Error())
break
}
card.EvdiNode.GrabPixels(card.Rect)
pixels := unsafe.Slice(
(*color.RGBA)(unsafe.Pointer(&card.Buffer.Buffer[0])),
len(card.Buffer.Buffer)/4,
)
rl.UpdateTexture(rect.Texture, pixels)
card.EvdiNode.RequestUpdate(card.Buffer)
}
rl.DrawModelEx(
rect.Model,
rl.Vector3{
X: 0,
Y: verticalSize / 2,
Z: 0,
},
// rotate around X to make it vertical
rl.Vector3{
X: 1,
Y: 0,
Z: 0,
},
90,
rl.Vector3{
X: 1,
Y: 1,
Z: 1,
},
rl.White,
)
break
}
rl.EndMode3D()
rl.EndDrawing()
}
log.Info("Goodbye!")
rl.CloseWindow()
}

View file

@ -1,9 +1,14 @@
package renderer
import "git.terah.dev/imterah/goevdi/libevdi"
import (
"git.terah.dev/imterah/goevdi/libevdi"
)
type EvdiDisplayMetadata struct {
EvdiNode *libevdi.EvdiNode
Rect *libevdi.EvdiDisplayRect
Buffer *libevdi.EvdiBuffer
EvdiNode *libevdi.EvdiNode
Rect *libevdi.EvdiDisplayRect
Buffer *libevdi.EvdiBuffer
EventContext *libevdi.EvdiEventContext
ShouldRequestUpdate bool
IsUpdateReady bool
}

21
go.mod
View file

@ -3,28 +3,31 @@ module git.terah.dev/UnrealXR/unrealxr
go 1.24.3
require (
git.terah.dev/UnrealXR/raylib-go/raylib v0.55.2-0.20250623002739-1468af2636e1 // indirect
git.terah.dev/imterah/goevdi v1.14.10 // indirect
github.com/anoopengineer/edidparser v0.0.0-20240602223913-86ca9ed3d2b0 // indirect
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
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 (
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
github.com/charmbracelet/log v0.4.2 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
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/goccy/go-yaml v1.18.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f // 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
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/tebeka/atexit v0.3.0 // indirect
github.com/urfave/cli/v3 v3.3.8 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/sys v0.33.0 // indirect

17
go.sum
View file

@ -1,9 +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 v0.0.0-20250211014657-96d98ad0735c h1:Ys33r6kjCyCs8N3ofSzCGxxL1gLIfetIiO1uL5UIsAM=
git.terah.dev/imterah/goevdi v0.0.0-20250211014657-96d98ad0735c/go.mod h1:RmxqlNaEJ+6qbHpyw6ivJhXlRYT1RcsZ9gzNOsFXfOA=
git.terah.dev/imterah/goevdi v1.14.10 h1:3zr3E/WRkSQ//+Gcrh0wfy0LVgm4Pcglc5K4MVcZ7n8=
git.terah.dev/imterah/goevdi v1.14.10/go.mod h1:z6GnBn2qHVV5N6fKZXPgUka8H9g5mZA5BRxCzUxLaNk=
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=
@ -21,6 +19,8 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
@ -39,26 +39,27 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tebeka/atexit v0.3.0 h1:jleL99H7Ywt80oJKR+VWmJNnezcCOG0CuzcN3CIpsdI=
github.com/tebeka/atexit v0.3.0/go.mod h1:WJmSUSmMT7WoR7etUOaGBVXk+f5/ZJ+67qwuedq7Fbs=
github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E=
github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=