diff --git a/app/main.go b/app/main.go index cd762c0..471b548 100644 --- a/app/main.go +++ b/app/main.go @@ -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) diff --git a/app/renderer/renderer.go b/app/renderer/renderer.go index e3cb39a..d1ebbf9 100644 --- a/app/renderer/renderer.go +++ b/app/renderer/renderer.go @@ -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() } diff --git a/app/renderer/struct.go b/app/renderer/struct.go index a1dd2d0..0acbde4 100644 --- a/app/renderer/struct.go +++ b/app/renderer/struct.go @@ -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 } diff --git a/go.mod b/go.mod index aa4a6e5..aedc610 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 72d89f4..aad411c 100644 --- a/go.sum +++ b/go.sum @@ -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=