lovr/src/modules/headset/headset_pico.c

612 lines
16 KiB
C

#include "headset/headset.h"
#include "event/event.h"
#include "graphics/graphics.h"
#include "graphics/canvas.h"
#include "resources/boot.lua.h"
#include "api/api.h"
#include "core/maf.h"
#include "core/os.h"
#include "util.h"
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <android/log.h>
#include <jni.h>
// Platform
static struct {
fn_permission* onPermissionEvent;
} os;
bool os_init() {
os_open_console();
return true;
}
void os_destroy() {
//
}
const char* os_get_name() {
return "Android";
}
uint32_t os_get_core_count() {
return sysconf(_SC_NPROCESSORS_ONLN);
}
// To make regular printing work, a thread makes a pipe and redirects stdout and stderr to the write
// end of the pipe. The read end of the pipe is forwarded to __android_log_write.
static struct {
int handles[2];
pthread_t thread;
} logState;
static void* log_main(void* data) {
int* fd = data;
pipe(fd);
dup2(fd[1], STDOUT_FILENO);
dup2(fd[1], STDERR_FILENO);
setvbuf(stdout, 0, _IOLBF, 0);
setvbuf(stderr, 0, _IONBF, 0);
ssize_t length;
char buffer[1024];
while ((length = read(fd[0], buffer, sizeof(buffer) - 1)) > 0) {
buffer[length] = '\0';
__android_log_write(ANDROID_LOG_DEBUG, "LOVR", buffer);
}
return 0;
}
void os_open_console() {
pthread_create(&logState.thread, NULL, log_main, logState.handles);
pthread_detach(logState.thread);
}
#define NS_PER_SEC 1000000000ULL
double os_get_time() {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (double) t.tv_sec + (t.tv_nsec / (double) NS_PER_SEC);
}
void os_sleep(double seconds) {
seconds += .5e-9;
struct timespec t;
t.tv_sec = seconds;
t.tv_nsec = (seconds - t.tv_sec) * NS_PER_SEC;
while (nanosleep(&t, &t));
}
JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPermissionEvent(JNIEnv* jni, jobject activity, jint permission, jboolean granted) {
if (os.onPermissionEvent) {
os.onPermissionEvent(permission, granted);
}
}
void os_request_permission(os_permission permission) {
// TODO
}
void os_poll_events() {
//
}
void os_on_quit(fn_quit* callback) {
//
}
void os_on_focus(fn_focus* callback) {
//
}
void os_on_resize(fn_resize* callback) {
//
}
void os_on_key(fn_key* callback) {
//
}
void os_on_text(fn_text* callback) {
// TODO
}
void os_on_permission(fn_permission* callback) {
os.onPermissionEvent = callback;
}
bool os_window_open(const os_window_config* config) {
return true;
}
bool os_window_is_open() {
return false;
}
void os_window_get_size(int* width, int* height) {
if (width) *width = 0;
if (height) *height = 0;
}
void os_window_get_fbsize(int* width, int* height) {
*width = 0;
*height = 0;
}
void os_window_set_vsync(int interval) {
//
}
void os_window_swap() {
//
}
fn_gl_proc* os_get_gl_proc_address(const char* function) {
return eglGetProcAddress(function);
}
size_t os_get_home_directory(char* buffer, size_t size) {
return 0;
}
size_t os_get_data_directory(char* buffer, size_t size) {
buffer[0] = '\0';
return 0;
}
size_t os_get_working_directory(char* buffer, size_t size) {
return getcwd(buffer, size) ? strlen(buffer) : 0;
}
size_t os_get_executable_path(char* buffer, size_t size) {
ssize_t length = readlink("/proc/self/exe", buffer, size - 1);
if (length >= 0) {
buffer[length] = '\0';
return length;
} else {
return 0;
}
}
static char apkPath[1024];
size_t os_get_bundle_path(char* buffer, size_t size, const char** root) {
size_t length = strlen(apkPath);
if (length >= size) return 0;
memcpy(buffer, apkPath, length);
buffer[length] = '\0';
*root = "/assets";
return length;
}
void os_get_mouse_position(double* x, double* y) {
*x = *y = 0.;
}
void os_set_mouse_mode(os_mouse_mode mode) {
//
}
bool os_is_mouse_down(os_mouse_button button) {
return false;
}
bool os_is_key_down(os_key key) {
return false;
}
// Headset backend
typedef struct {
GLint id;
Canvas* instance;
} NativeCanvas;
static struct {
float offset;
float clipNear;
float clipFar;
uint32_t displayWidth;
uint32_t displayHeight;
float headPosition[4];
float headOrientation[4];
float fov;
float ipd;
struct {
bool active;
uint16_t buttons;
uint16_t changed;
float trigger;
float thumbstick[2];
float position[4];
float orientation[4];
float hapticStrength;
float hapticDuration;
} controllers[2];
arr_t(NativeCanvas) canvases;
void (*renderCallback)(void*);
void* renderUserdata;
} state;
static bool pico_init(float supersample, float offset, uint32_t msaa, bool overlay) {
state.offset = offset;
state.clipNear = .1f;
state.clipFar = 100.f;
return true;
}
static void pico_start(void) {
//
}
static void pico_destroy(void) {
arr_free(&state.canvases);
memset(&state, 0, sizeof(state));
}
// TODO use presence of controllers to determine G2 vs Neo (isControllerServiceExisted)
static bool pico_getName(char* name, size_t length) {
strncpy(name, "Pico", length - 1);
name[length - 1] = '\0';
return true;
}
// The Unity/Unreal SDKs expose true origin types (Pvr_SetTrackingOrigin) but there does not appear
// to be a way to access this from the Native SDK. Pose information appears to be relative to the
// initial head pose.
static HeadsetOrigin pico_getOriginType(void) {
return ORIGIN_HEAD;
}
static double pico_getDisplayTime(void) {
return os_get_time();
}
static void pico_getDisplayDimensions(uint32_t* width, uint32_t* height) {
*width = state.displayWidth;
*height = state.displayHeight;
}
static const float* pico_getDisplayMask(uint32_t* count) {
*count = 0;
return NULL;
}
static uint32_t pico_getViewCount(void) {
return 2;
}
static bool pico_getViewPose(uint32_t view, float* position, float* orientation) {
vec3_init(position, state.headPosition);
quat_init(orientation, state.headOrientation);
position[1] += state.offset;
return view < 2;
}
static bool pico_getViewAngles(uint32_t view, float* left, float* right, float* up, float* down) {
*left = *right = *up = *down = state.fov;
return view < 2;
}
static void pico_getClipDistance(float* clipNear, float* clipFar) {
*clipNear = state.clipNear;
*clipFar = state.clipFar;
}
static void pico_setClipDistance(float clipNear, float clipFar) {
state.clipNear = clipNear;
state.clipFar = clipFar;
}
// The Unity/Unreal SDKs expose something called "SeeThrough" that is very similar to the Oculus
// Guardian API, but this does not appear to be in the Native SDK
static void pico_getBoundsDimensions(float* width, float* depth) {
*width = *depth = 0.f;
}
static const float* pico_getBoundsGeometry(uint32_t* count) {
*count = 0;
return NULL;
}
static bool pico_getPose(Device device, float* position, float* orientation) {
if (device == DEVICE_HEAD) {
vec3_init(position, state.headPosition);
quat_init(orientation, state.headOrientation);
position[1] += state.offset;
return true;
}
if (device == DEVICE_HAND_LEFT || device == DEVICE_HAND_RIGHT) {
uint32_t index = device - DEVICE_HAND_LEFT;
vec3_init(position, state.controllers[index].position);
quat_init(orientation, state.controllers[index].orientation);
position[1] += state.offset;
return state.controllers[index].active;
}
return false;
}
static bool pico_getVelocity(Device device, float* velocity, float* angularVelocity) {
return false; // Controllers only expose acceleration and angular velocity, so we skip it
}
static bool pico_isDown(Device device, DeviceButton button, bool* down, bool* changed) {
if (device != DEVICE_HAND_LEFT && device != DEVICE_HAND_RIGHT) {
return false;
}
uint32_t index = device - DEVICE_HAND_LEFT;
if (!state.controllers[index].active) {
return false;
}
bool active = true;
uint16_t mask = 0;
switch (button) {
case BUTTON_TRIGGER: mask = 1 << 0; break;
case BUTTON_THUMBSTICK: mask = 1 << 1; break;
case BUTTON_GRIP: mask = 1 << 2; break;
case BUTTON_MENU: mask = 1 << 3; break;
case BUTTON_A: mask = 1 << 4; active = index == 1; break;
case BUTTON_X: mask = 1 << 4; active = index == 0; break;
case BUTTON_B: mask = 1 << 5; active = index == 1; break;
case BUTTON_Y: mask = 1 << 5; active = index == 0; break;
default: return false;
}
*down = state.controllers[index].buttons & mask;
*changed = state.controllers[index].changed & mask;
return active;
}
static bool pico_isTouched(Device device, DeviceButton button, bool* touched) {
return false;
}
static bool pico_getAxis(Device device, DeviceAxis axis, float* value) {
if (device != DEVICE_HAND_LEFT && device != DEVICE_HAND_RIGHT) {
return false;
}
uint32_t index = device - DEVICE_HAND_LEFT;
if (!state.controllers[index].active) {
return false;
}
switch (axis) {
case AXIS_TRIGGER:
*value = state.controllers[index].trigger;
return true;
case AXIS_THUMBSTICK:
value[0] = state.controllers[index].thumbstick[0];
value[1] = state.controllers[index].thumbstick[1];
return true;
default:
return false;
}
}
static bool pico_vibrate(Device device, float strength, float duration, float frequency) {
if (device != DEVICE_HAND_LEFT && device != DEVICE_HAND_RIGHT) {
return false;
}
uint32_t index = device - DEVICE_HAND_LEFT;
state.controllers[index].hapticStrength = strength;
state.controllers[index].hapticDuration = duration;
return true;
}
static struct ModelData* pico_newModelData(Device device, bool animated) {
return NULL;
}
static bool pico_animate(Device device, struct Model* model) {
return false;
}
static void pico_renderTo(void (*callback)(void*), void* userdata) {
state.renderCallback = callback;
state.renderUserdata = userdata;
}
static void pico_update(float dt) {
//
}
HeadsetInterface lovrHeadsetPicoDriver = {
.driverType = DRIVER_PICO,
.init = pico_init,
.start = pico_start,
.destroy = pico_destroy,
.getName = pico_getName,
.getOriginType = pico_getOriginType,
.getDisplayTime = pico_getDisplayTime,
.getDisplayDimensions = pico_getDisplayDimensions,
.getDisplayMask = pico_getDisplayMask,
.getViewCount = pico_getViewCount,
.getViewPose = pico_getViewPose,
.getViewAngles = pico_getViewAngles,
.getClipDistance = pico_getClipDistance,
.setClipDistance = pico_setClipDistance,
.getBoundsDimensions = pico_getBoundsDimensions,
.getBoundsGeometry = pico_getBoundsGeometry,
.getPose = pico_getPose,
.getVelocity = pico_getVelocity,
.isDown = pico_isDown,
.isTouched = pico_isTouched,
.getAxis = pico_getAxis,
.vibrate = pico_vibrate,
.newModelData = pico_newModelData,
.animate = pico_animate,
.renderTo = pico_renderTo,
.update = pico_update
};
// Activity callbacks
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
static struct lua_State* L;
static struct lua_State* T;
static Variant cookie;
static void lovrPicoBoot(void) {
lovrAssert(os_init(), "Failed to initialize platform");
L = luaL_newstate();
luax_setmainthread(L);
luaL_openlibs(L);
luax_preload(L);
lua_pushcfunction(L, luax_getstack);
if (luaL_loadbuffer(L, (const char*) src_resources_boot_lua, src_resources_boot_lua_len, "@boot.lua") || lua_pcall(L, 0, 1, -2)) {
fprintf(stderr, "%s\n", lua_tostring(L, -1));
return;
}
T = lua_newthread(L);
lua_pushvalue(L, -2);
lua_xmove(L, T, 1);
lovrSetErrorCallback(luax_vthrow, T);
lovrSetLogCallback(luax_vlog, T);
}
JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPicoOnCreate(JNIEnv* jni, jobject activity, jstring apk) {
const char* path = (*jni)->GetStringUTFChars(jni, apk, NULL);
size_t length = strlen(path);
if (length < sizeof(apkPath)) {
memcpy(apkPath, path, length);
}
lovrPicoBoot();
}
JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPicoSetDisplayDimensions(JNIEnv* jni, jobject activity, int width, int height) {
state.displayWidth = width;
state.displayHeight = height;
}
JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPicoUpdateControllerPose(JNIEnv* jni, jobject activity, int hand, bool active, float x, float y, float z, float qx, float qy, float qz, float qw) {
state.controllers[hand].active = active;
vec3_set(state.controllers[hand].position, x, y, z);
quat_set(state.controllers[hand].orientation, -qx, -qy, qz, qw);
}
JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPicoUpdateControllerInput(JNIEnv* jni, jobject activity, int hand, int buttons, float trigger, float thumbstickX, float thumbstickY) {
state.controllers[hand].changed = state.controllers[hand].buttons ^ buttons;
state.controllers[hand].buttons = (uint16_t) buttons;
state.controllers[hand].trigger = trigger;
state.controllers[hand].thumbstick[0] = thumbstickX;
state.controllers[hand].thumbstick[1] = thumbstickY;
}
JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPicoOnFrame(JNIEnv* jni, jobject activity, float x, float y, float z, float qx, float qy, float qz, float qw, float fov, float ipd) {
vec3_set(state.headPosition, x, y, z);
quat_set(state.headOrientation, qx, qy, qz, qw);
state.fov = fov * M_PI / 180.f;
state.ipd = ipd;
// Haptics
for (uint32_t i = 0; i < 2; i++) {
if (state.controllers[i].hapticStrength > 0.f) {
float strength = state.controllers[i].hapticStrength;
float duration = state.controllers[i].hapticDuration;
jclass class = (*jni)->GetObjectClass(jni, activity);
jmethodID vibrate = (*jni)->GetMethodID(jni, class, "vibrate", "(IFF)V");
(*jni)->CallObjectMethod(jni, activity, vibrate, i, strength, duration);
state.controllers[i].hapticStrength = 0.f;
}
}
// Resume the lovr.run coroutine, and if it returns (doesn't yield) then either reboot or exit
if (L && T) {
luax_geterror(T);
luax_clearerror(T);
if (luax_resume(T, 1) != LUA_YIELD) {
bool restart = lua_type(T, 1) == LUA_TSTRING && !strcmp(lua_tostring(T, 1), "restart");
if (restart) {
luax_checkvariant(T, 2, &cookie);
if (cookie.type == TYPE_OBJECT) {
cookie.type = TYPE_NIL;
memset(&cookie.value, 0, sizeof(cookie.value));
}
lua_close(L);
lovrPicoBoot();
} else {
lua_close(L);
L = NULL;
T = NULL;
// Call 'finish' method on the Activity
jclass class = (*jni)->GetObjectClass(jni, activity);
jmethodID finish = (*jni)->GetMethodID(jni, class, "finish", "()V");
(*jni)->CallObjectMethod(jni, activity, finish);
}
}
}
}
JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPicoDrawEye(JNIEnv* jni, jobject object, int eye) {
if (!state.renderCallback) return;
// Pico modifies a lot of global OpenGL state, including the framebuffer binding, VAO binding,
// buffer bindings, blending, and depth test settings. Since there is no swapchain or texture
// submission API, we have to render into the currently active OpenGL framebuffer, so a cache of
// native Canvas objects is used for that. For the rest of the states, there is a new "nuke all
// OpenGL state" function added to clear any changes made by Pico (lovrGpuResetState) :(
GLint framebuffer;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &framebuffer);
Canvas* canvas = NULL;
for (uint32_t i = 0; i < state.canvases.length; i++) {
if (state.canvases.data[i].id == framebuffer) {
canvas = state.canvases.data[i].instance;
break;
}
}
if (!canvas) {
CanvasFlags flags = { .depth.enabled = true };
canvas = lovrCanvasCreateFromHandle(state.displayWidth, state.displayHeight, flags, framebuffer, 0, 0, 1, true);
arr_push(&state.canvases, ((NativeCanvas) { .id = framebuffer, .instance = canvas }));
}
// start each eye from origin
lovrGraphicsOrigin();
for (uint32_t i = 0; i < 2; i++) {
float view[16];
mat4_identity(view);
mat4_translate(view, state.headPosition[0], state.headPosition[1] + state.offset, state.headPosition[2]);
mat4_rotateQuat(view, state.headOrientation);
mat4_translate(view, state.ipd * (eye == 0 ? -.5f : .5f), 0.f, 0.f);
mat4_invert(view);
lovrGraphicsSetViewMatrix(i, view);
float projection[16];
mat4_fov(projection, state.fov, state.fov, state.fov, state.fov, state.clipNear, state.clipFar);
lovrGraphicsSetProjection(i, projection);
}
lovrGpuResetState();
lovrGraphicsSetBackbuffer(canvas, false, true);
state.renderCallback(state.renderUserdata);
lovrGraphicsSetBackbuffer(NULL, false, false);
}