mirror of https://github.com/bjornbytes/lovr.git
612 lines
16 KiB
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);
|
|
}
|