1
0
Fork 0
mirror of https://github.com/bjornbytes/lovr.git synced 2024-07-11 08:23:34 +00:00
lovr/src/headset/oculus_mobile.c

468 lines
15 KiB
C

#include "headset/headset.h"
#include "oculus_mobile_bridge.h"
#include "math.h"
#include "graphics/graphics.h"
#include "lib/glad/glad.h"
#include <assert.h>
#include "platform.h"
// Data passed from bridge code to headset code
typedef struct {
BridgeLovrDimensions displayDimensions;
BridgeLovrDevice deviceType;
BridgeLovrUpdateData updateData;
} BridgeLovrMobileData;
BridgeLovrMobileData bridgeLovrMobileData;
// Headset
static void (*renderCallback)(void*);
static void* renderUserdata;
static float offset;
// Headset driver object
static bool oculusMobileInit(float _offset, int msaa) {
// Make sure HeadsetDriver and BridgeLovrDevice have not gone out of sync
assert(BRIDGE_LOVR_DEVICE_UNKNOWN == HEADSET_UNKNOWN);
assert(BRIDGE_LOVR_DEVICE_GEAR == HEADSET_GEAR);
assert(BRIDGE_LOVR_DEVICE_GO == HEADSET_GO);
offset = _offset;
return true;
}
static void oculusMobileDestroy() {
}
static HeadsetType oculusMobileGetType() {
return (HeadsetType)(int)bridgeLovrMobileData.deviceType;
}
static HeadsetOrigin oculusMobileGetOriginType() {
return ORIGIN_HEAD;
}
static bool oculusMobileIsMounted() {
return true; // ???
}
static void oculusMobileGetDisplayDimensions(uint32_t* width, uint32_t* height) {
*width = bridgeLovrMobileData.displayDimensions.width;
*height = bridgeLovrMobileData.displayDimensions.height;
}
static void oculusMobileGetClipDistance(float* clipNear, float* clipFar) {
// TODO
}
static void oculusMobileSetClipDistance(float clipNear, float clipFar) {
// TODO
}
static void oculusMobileGetBoundsDimensions(float* width, float* depth) {
*width = 0;
*depth = 0;
}
static const float* oculusMobileGetBoundsGeometry(int* count) {
*count = 0;
return NULL;
}
static void oculusMobileGetPose(float* x, float* y, float* z, float* angle, float* ax, float* ay, float* az) {
*x = bridgeLovrMobileData.updateData.lastHeadPose.x;
*y = bridgeLovrMobileData.updateData.lastHeadPose.y + offset; // Correct for head height
*z = bridgeLovrMobileData.updateData.lastHeadPose.z;
// Notice: Ax and Az are both swapped and inverted. Experimentally, if you do the Oculus Go controller position
// lines up with Lovr visually, and if you don't it doesn't. This is probably needed because of different axis standards.
quat_getAngleAxis(bridgeLovrMobileData.updateData.lastHeadPose.q, angle, az, ay, ax);
*ax = -*ax;
*az = -*az;
}
// TODO: This has never been tested
static void oculusMobileGetVelocity(float* vx, float* vy, float* vz) {
*vx = bridgeLovrMobileData.updateData.lastHeadVelocity.x;
*vy = bridgeLovrMobileData.updateData.lastHeadVelocity.y;
*vz = bridgeLovrMobileData.updateData.lastHeadVelocity.z;
}
// TODO: This has never been tested
static void oculusMobileGetAngularVelocity(float* vx, float* vy, float* vz) {
*vx = bridgeLovrMobileData.updateData.lastHeadVelocity.ax;
*vy = bridgeLovrMobileData.updateData.lastHeadVelocity.ay;
*vz = bridgeLovrMobileData.updateData.lastHeadVelocity.az;
}
static Controller *controller;
static Controller** oculusMobileGetControllers(uint8_t* count) {
if (!controller)
controller = lovrAlloc(Controller);
*count = bridgeLovrMobileData.updateData.goPresent; // TODO: Figure out what multi controller Oculus Mobile looks like and support it
return &controller;
}
static bool oculusMobileControllerIsConnected(Controller* controller) {
return bridgeLovrMobileData.updateData.goPresent;
}
static ControllerHand oculusMobileControllerGetHand(Controller* controller) {
return HAND_UNKNOWN;
}
static void oculusMobileControllerGetPose(Controller* controller, float* x, float* y, float* z, float* angle, float* ax, float* ay, float* az) {
*x = bridgeLovrMobileData.updateData.goPose.x;
*y = bridgeLovrMobileData.updateData.goPose.y + offset; // Correct for head height
*z = bridgeLovrMobileData.updateData.goPose.z;
// Notice: Ax and Az are both swapped and inverted. Experimentally, if you do the Oculus Go controller position
// lines up with Lovr visually, and if you don't it doesn't. This is probably needed because of different axis standards.
quat_getAngleAxis(bridgeLovrMobileData.updateData.goPose.q, angle, az, ay, ax);
*ax = -*ax;
*az = -*az;
}
static void oculusMobileControllerGetVelocity(Controller* controller, float* vx, float* vy, float* vz) {
*vx = bridgeLovrMobileData.updateData.goVelocity.x;
*vy = bridgeLovrMobileData.updateData.goVelocity.y;
*vz = bridgeLovrMobileData.updateData.goVelocity.z;
}
static void oculusMobileControllerGetAngularVelocity(Controller* controller, float* vx, float* vy, float* vz) {
*vx = bridgeLovrMobileData.updateData.goVelocity.ax;
*vy = bridgeLovrMobileData.updateData.goVelocity.ay;
*vz = bridgeLovrMobileData.updateData.goVelocity.az;
}
static float oculusMobileControllerGetAxis(Controller* controller, ControllerAxis axis) {
switch (axis) {
case CONTROLLER_AXIS_TOUCHPAD_X:
return (bridgeLovrMobileData.updateData.goTrackpad.x-160)/160.0;
case CONTROLLER_AXIS_TOUCHPAD_Y:
return (bridgeLovrMobileData.updateData.goTrackpad.y-160)/160.0;
case CONTROLLER_AXIS_TRIGGER:
return bridgeLovrMobileData.updateData.goButtonDown ? 1.0 : 0.0;
default:
return 0;
}
}
static bool buttonCheck(BridgeLovrButton field, ControllerButton button) {
switch (button) {
case CONTROLLER_BUTTON_MENU:
return field & BRIDGE_LOVR_BUTTON_MENU;
case CONTROLLER_BUTTON_TRIGGER:
return field & BRIDGE_LOVR_BUTTON_SHOULDER;
case CONTROLLER_BUTTON_TOUCHPAD:
return field & BRIDGE_LOVR_BUTTON_TOUCHPAD;
default:
return false;
}
}
static bool oculusMobileControllerIsDown(Controller* controller, ControllerButton button) {
return buttonCheck(bridgeLovrMobileData.updateData.goButtonDown, button);
}
static bool oculusMobileControllerIsTouched(Controller* controller, ControllerButton button) {
return buttonCheck(bridgeLovrMobileData.updateData.goButtonTouch, button);
}
static void oculusMobileControllerVibrate(Controller* controller, float duration, float power) {
}
static ModelData* oculusMobileControllerNewModelData(Controller* controller) {
return NULL;
}
// TODO: need to set up swap chain textures for the eyes and finish view transforms
static void oculusMobileRenderTo(void (*callback)(void*), void* userdata) {
renderCallback = callback;
renderUserdata = userdata;
}
static void oculusMobileUpdate(float dt) {
}
HeadsetInterface lovrHeadsetOculusMobileDriver = {
DRIVER_OCULUS_MOBILE,
oculusMobileInit,
oculusMobileDestroy,
oculusMobileGetType,
oculusMobileGetOriginType,
oculusMobileIsMounted,
oculusMobileGetDisplayDimensions,
oculusMobileGetClipDistance,
oculusMobileSetClipDistance,
oculusMobileGetBoundsDimensions,
oculusMobileGetBoundsGeometry,
oculusMobileGetPose,
oculusMobileGetVelocity,
oculusMobileGetAngularVelocity,
oculusMobileGetControllers,
oculusMobileControllerIsConnected,
oculusMobileControllerGetHand,
oculusMobileControllerGetPose,
oculusMobileControllerGetVelocity,
oculusMobileControllerGetAngularVelocity,
oculusMobileControllerGetAxis,
oculusMobileControllerIsDown,
oculusMobileControllerIsTouched,
oculusMobileControllerVibrate,
oculusMobileControllerNewModelData,
oculusMobileRenderTo,
NULL, // No mirror texture
oculusMobileUpdate
};
// Oculus-specific platform functions
static double timeOffset;
void lovrPlatformSetTime(double time) {
timeOffset = bridgeLovrMobileData.updateData.displayTime - time;
}
double lovrPlatformGetTime(void) {
return bridgeLovrMobileData.updateData.displayTime - timeOffset;
}
void lovrPlatformGetFramebufferSize(int* width, int* height) {
*width = bridgeLovrMobileData.displayDimensions.width;
*height = bridgeLovrMobileData.displayDimensions.height;
}
bool lovrPlatformHasWindow() {
return false;
}
// "Bridge" (see oculus_mobile_bridge.h)
#include <stdio.h>
#include <android/log.h>
#include "physfs.h"
#include <sys/stat.h>
#include <assert.h>
#include "oculus_mobile_bridge.h"
#include "luax.h"
#include "lib/sds/sds.h"
#include "api.h"
#include "lib/lua-cjson/lua_cjson.h"
#include "lib/lua-enet/enet.h"
#include "headset/oculus_mobile.h"
// Implicit from boot.lua.h
extern unsigned char boot_lua[];
extern unsigned int boot_lua_len;
static lua_State *L, *Lcoroutine;
static int coroutineRef = LUA_NOREF;
static int coroutineStartFunctionRef = LUA_NOREF;
static char *apkPath;
// Expose to filesystem.h
char *lovrOculusMobileWritablePath;
// Used for resume (pausing the app and returning to the menu) logic. This is needed for two reasons
// 1. The GLFW time should rewind after a pause so that the app cannot perceive time passed
// 2. There is a bug in the Mobile SDK https://developer.oculus.com/bugs/bug/189155031962759/
// On the first frame after a resume, the time will be total nonsense
static double lastPauseAt, lastPauseAtRaw; // platform time and oculus time at last pause
enum {
PAUSESTATE_NONE, // Normal state
PAUSESTATE_PAUSED, // A pause has been issued -- waiting for resume
PAUSESTATE_BUG, // We have resumed, but the next frame will be the bad frame
PAUSESTATE_RESUME // We have resumed, and the next frame will need to adjust the clock
} pauseState;
int lovr_luaB_print_override (lua_State *L);
#define SDS(...) sdscatfmt(sdsempty(), __VA_ARGS__)
static void android_vthrow(lua_State* L, const char* format, va_list args) {
#define MAX_ERROR_LENGTH 1024
char lovrErrorMessage[MAX_ERROR_LENGTH];
vsnprintf(lovrErrorMessage, MAX_ERROR_LENGTH, format, args);
lovrWarn("Error: %s\n", lovrErrorMessage);
assert(0);
}
static int luax_custom_atpanic(lua_State *L) {
// This doesn't appear to get a sensible stack. Maybe Luajit would work better?
luax_traceback(L, L, lua_tostring(L, -1), 0); // Pushes the traceback onto the stack
lovrThrow("Lua panic: %s", lua_tostring(L, -1));
return 0;
}
static void bridgeLovrInitState() {
// Ready to actually go now.
// Copypaste the init sequence from lovrRun:
// Load libraries
L = luaL_newstate(); // FIXME: Can this be handed off to main.c?
luax_setmainstate(L);
lua_atpanic(L, luax_custom_atpanic);
luaL_openlibs(L);
lovrLog("\n OPENED LIB\n");
lovrSetErrorCallback((lovrErrorHandler) android_vthrow, L);
// Install custom print
lua_pushcfunction(L, luax_print);
lua_setglobal(L, "print");
lovrPlatformSetTime(0);
// Set "arg" global (see main.c)
{
lua_newtable(L);
lua_pushliteral(L, "lovr");
lua_pushvalue(L, -1); // Double at named key
lua_setfield(L, -3, "exe");
lua_rawseti(L, -2, -3);
// Mimic the arguments "--root /assets" as parsed by lovrInit
lua_pushliteral(L, "--root");
lua_rawseti(L, -2, -2);
lua_pushliteral(L, "/assets");
lua_pushvalue(L, -1); // Double at named key
lua_setfield(L, -3, "root");
lua_rawseti(L, -2, -1);
lua_pushstring(L, apkPath);
lua_rawseti(L, -2, 0);
lua_setglobal(L, "arg");
}
// Populate package.preload with built-in modules
lua_getglobal(L, "package");
lua_getfield(L, -1, "preload");
luaL_register(L, NULL, lovrModules);
lua_pop(L, 2);
// Run init
lua_pushcfunction(L, luax_getstack);
if (luaL_loadbuffer(L, (const char*) boot_lua, boot_lua_len, "boot.lua") || lua_pcall(L, 0, 1, -2)) {
lovrWarn("\n LUA STARTUP FAILED: %s\n", lua_tostring(L, -1));
lua_close(L);
assert(0);
}
coroutineStartFunctionRef = luaL_ref(L, LUA_REGISTRYINDEX); // Value returned by boot.lua
Lcoroutine = lua_newthread(L); // Leave L clear to be used by the draw function
lua_atpanic(Lcoroutine, luax_custom_atpanic);
coroutineRef = luaL_ref(L, LUA_REGISTRYINDEX); // Hold on to the Lua-side coroutine object so it isn't GC'd
lovrLog("\n STATE INIT COMPLETE\n");
}
void bridgeLovrInit(BridgeLovrInitData *initData) {
lovrLog("\n INSIDE LOVR\n");
// Save writable data directory for LovrFilesystemInit later
{
lovrOculusMobileWritablePath = sdsRemoveFreeSpace(SDS("%s/data", initData->writablePath));
mkdir(lovrOculusMobileWritablePath, 0777);
}
// Unpack init data
bridgeLovrMobileData.displayDimensions = initData->suggestedEyeTexture;
bridgeLovrMobileData.updateData.displayTime = initData->zeroDisplayTime;
bridgeLovrMobileData.deviceType = initData->deviceType;
free(apkPath);
apkPath = strdup(initData->apkPath);
bridgeLovrInitState();
lovrLog("\n BRIDGE INIT COMPLETE\n");
}
void bridgeLovrUpdate(BridgeLovrUpdateData *updateData) {
// Unpack update data
bridgeLovrMobileData.updateData = *updateData;
if (pauseState == PAUSESTATE_BUG) { // Bad frame-- replace bad time with last known good oculus time
bridgeLovrMobileData.updateData.displayTime = lastPauseAtRaw;
pauseState = PAUSESTATE_RESUME;
} else if (pauseState == PAUSESTATE_RESUME) { // Resume frame-- adjust platform time to be equal to last good platform time
lovrPlatformSetTime(lastPauseAt);
pauseState = PAUSESTATE_NONE;
}
// Go
if (coroutineStartFunctionRef != LUA_NOREF) {
lua_rawgeti(Lcoroutine, LUA_REGISTRYINDEX, coroutineStartFunctionRef);
luaL_unref (Lcoroutine, LUA_REGISTRYINDEX, coroutineStartFunctionRef);
coroutineStartFunctionRef = LUA_NOREF; // No longer needed
}
int coroutineArgs = luax_pushLovrHeadsetRenderError(Lcoroutine);
if (lua_resume(Lcoroutine, coroutineArgs) != LUA_YIELD) {
if (lua_type(Lcoroutine, -1) == LUA_TSTRING && !strcmp(lua_tostring(Lcoroutine, -1), "restart")) {
lua_close(L);
luax_setmainstate(NULL);
bridgeLovrInitState();
} else {
lovrLog("\n LUA REQUESTED A QUIT\n");
assert(0);
}
}
}
static void lovrOculusMobileDraw(int framebuffer, int width, int height, float *eyeViewMatrix, float *projectionMatrix) {
lovrGpuDirtyTexture();
CanvasFlags flags = {0};
Canvas *canvas = lovrCanvasCreateFromHandle(width, height, flags, framebuffer, 0, 0, 1, true);
Camera camera = { .canvas = canvas, .stereo = false };
memcpy(camera.viewMatrix[0], eyeViewMatrix, sizeof(camera.viewMatrix[0]));
mat4_translate(camera.viewMatrix[0], 0, -offset, 0);
memcpy(camera.projection[0], projectionMatrix, sizeof(camera.projection[0]));
lovrGraphicsSetCamera(&camera, true);
if (renderCallback)
renderCallback(renderUserdata);
lovrGraphicsSetCamera(NULL, false);
lovrRelease(canvas);
}
void bridgeLovrDraw(BridgeLovrDrawData *drawData) {
int eye = drawData->eye;
lovrOculusMobileDraw(drawData->framebuffer, bridgeLovrMobileData.displayDimensions.width, bridgeLovrMobileData.displayDimensions.height,
bridgeLovrMobileData.updateData.eyeViewMatrix[eye], bridgeLovrMobileData.updateData.projectionMatrix[eye]); // Is this indexing safe?
}
// Android activity has been stopped or resumed
// In order to prevent weird dt jumps, we need to freeze and reset the clock
static bool armedUnpause;
void bridgeLovrPaused(bool paused) {
if (paused) { // Save last platform and oculus times and wait for resume
lastPauseAt = lovrPlatformGetTime();
lastPauseAtRaw = bridgeLovrMobileData.updateData.displayTime;
pauseState = PAUSESTATE_PAUSED;
} else {
if (pauseState != PAUSESTATE_NONE) { // Got a resume-- set flag to start the state machine in bridgeLovrUpdate
pauseState = PAUSESTATE_BUG;
}
}
}
// Android activity has been "destroyed" (but process will probably not quit)
void bridgeLovrClose() {
pauseState = PAUSESTATE_NONE;
lua_close(L);
}