Pico: Draw the rest of the owl;

Add entrypoints, headset backend code, fill in the Activity, and
add various special cases to account for the asynchronous render loop,
lack of sRGB support, and OpenGL state resets.
This commit is contained in:
bjorn 2020-07-27 14:56:21 -06:00
parent 58ab637465
commit ac58a1aeba
10 changed files with 431 additions and 25 deletions

View File

@ -353,6 +353,10 @@ set(LOVR_SRC
src/api/l_lovr.c
)
if(LOVR_USE_PICO)
list(REMOVE_ITEM LOVR_SRC src/main.c)
endif()
if(LOVR_BUILD_SHARED)
add_library(lovr SHARED ${LOVR_SRC})
elseif(LOVR_BUILD_EXE)

View File

@ -87,7 +87,7 @@ static HeadsetRenderData headsetRenderData;
static void renderHelper(void* userdata) {
HeadsetRenderData* renderData = userdata;
lua_State* L = renderData->L;
#ifdef EMSCRIPTEN
#if defined(EMSCRIPTEN) || defined(LOVR_USE_PICO)
luax_geterror(L);
if (lua_isnil(L, -1)) {
lua_pushcfunction(L, luax_getstack);
@ -579,7 +579,7 @@ static int l_lovrHeadsetRenderTo(lua_State* L) {
lua_settop(L, 1);
luaL_checktype(L, 1, LUA_TFUNCTION);
#ifdef EMSCRIPTEN
#if defined(EMSCRIPTEN) || defined(LOVR_USE_PICO)
if (headsetRenderData.ref != LUA_NOREF) {
luaL_unref(L, LUA_REGISTRYINDEX, headsetRenderData.ref);
}

View File

@ -310,7 +310,7 @@ void lovrGraphicsSetCamera(Camera* camera, bool clear) {
if (!state.camera.canvas) {
state.camera.canvas = state.defaultCanvas;
lovrCanvasSetStereo(state.camera.canvas, camera->stereo);
lovrCanvasSetStereo(state.camera.canvas, camera->stereo);
}
}
@ -364,7 +364,7 @@ Color lovrGraphicsGetBackgroundColor() {
void lovrGraphicsSetBackgroundColor(Color color) {
state.backgroundColor = state.linearBackgroundColor = color;
#ifndef LOVR_WEBGL
#if !defined(LOVR_WEBGL) && !defined(LOVR_USE_PICO)
gammaCorrect(&state.linearBackgroundColor);
#endif
}
@ -789,7 +789,7 @@ void lovrGraphicsFlushMesh(Mesh* mesh) {
}
void lovrGraphicsClear(Color* color, float* depth, int* stencil) {
#ifndef LOVR_WEBGL
#if !defined(LOVR_WEBGL) && !defined(LOVR_USE_PICO)
if (color) gammaCorrect(color);
#endif
if (color || depth || stencil) lovrGraphicsFlush();

View File

@ -228,6 +228,7 @@ void lovrGpuDraw(DrawCommand* draw);
void lovrGpuStencil(StencilAction action, int replaceValue, StencilCallback callback, void* userdata);
void lovrGpuPresent(void);
void lovrGpuDirtyTexture(void);
void lovrGpuResetState(void);
void lovrGpuTick(const char* label);
double lovrGpuTock(const char* label);
const GpuFeatures* lovrGpuGetFeatures(void);

View File

@ -1495,6 +1495,32 @@ void lovrGpuDirtyTexture() {
state.textures[state.activeTexture] = NULL;
}
// This doesn't actually reset all state, just state that is known to be changed externally
void lovrGpuResetState() {
if (state.vertexArray) {
glBindVertexArray(state.vertexArray->vao);
}
for (size_t i = 0; i < MAX_BUFFER_TYPES; i++) {
glBindBuffer(convertBufferType(i), state.buffers[i]);
}
glBindFramebuffer(GL_FRAMEBUFFER, state.framebuffer);
glUseProgram(state.program);
if (state.blendEnabled) {
glEnable(GL_BLEND);
} else {
glDisable(GL_BLEND);
}
if (state.depthEnabled) {
glEnable(GL_DEPTH_TEST);
} else {
glDisable(GL_DEPTH_TEST);
}
}
void lovrGpuTick(const char* label) {
#ifndef LOVR_WEBGL
lovrAssert(state.activeTimer == ~0u, "Attempt to start a new GPU timer while one is already active!");

View File

@ -33,6 +33,9 @@ bool lovrHeadsetInit(HeadsetDriver* drivers, size_t count, float offset, uint32_
#ifdef LOVR_USE_VRAPI
case DRIVER_VRAPI: interface = &lovrHeadsetVrApiDriver; break;
#endif
#ifdef LOVR_USE_PICO
case DRIVER_PICO: interface = &lovrHeadsetPicoDriver; break;
#endif
#ifdef LOVR_USE_WEBVR
case DRIVER_WEBVR: interface = &lovrHeadsetWebVRDriver; break;
#endif

View File

@ -1,5 +1,13 @@
#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/arr.h"
#include "core/maf.h"
#include "core/os.h"
#include "core/util.h"
#include <string.h>
#include <stdlib.h>
#include <time.h>
@ -8,6 +16,7 @@
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <android/log.h>
#include <jni.h>
// Platform
@ -177,34 +186,70 @@ bool lovrPlatformIsKeyDown(KeyCode key) {
// 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 offset, uint32_t msaa) {
state.offset = offset;
state.clipNear = .1f;
state.clipFar = 100.f;
return true;
}
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) {
return false;
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 0.;
return lovrPlatformGetTime();
}
static void pico_getDisplayDimensions(uint32_t* width, uint32_t* height) {
*width = 0;
*height = 0;
*width = state.displayWidth;
*height = state.displayHeight;
}
static const float* pico_getDisplayMask(uint32_t* count) {
@ -217,22 +262,28 @@ static uint32_t pico_getViewCount(void) {
}
static bool pico_getViewPose(uint32_t view, float* position, float* orientation) {
return false;
// TODO use HmdState pose info, offset view by half ipd
quat_init(orientation, state.headOrientation);
return view < 2;
}
static bool pico_getViewAngles(uint32_t view, float* left, float* right, float* up, float* down) {
return false;
*left = *right = *up = *down = state.fov;
return view < 2;
}
static void pico_getClipDistance(float* clipNear, float* clipFar) {
*clipNear = 0.f;
*clipFar = 0.f;
*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;
}
@ -243,15 +294,56 @@ static const float* pico_getBoundsGeometry(uint32_t* count) {
}
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);
return state.controllers[index].active;
}
return false;
}
static bool pico_getVelocity(Device device, float* velocity, float* angularVelocity) {
return false;
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) {
return false;
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) {
@ -259,11 +351,39 @@ static bool pico_isTouched(Device device, DeviceButton button, bool* touched) {
}
static bool pico_getAxis(Device device, DeviceAxis axis, float* value) {
return false;
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) {
return false;
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) {
@ -271,7 +391,8 @@ static struct ModelData* pico_newModelData(Device device) {
}
static void pico_renderTo(void (*callback)(void*), void* userdata) {
//
state.renderCallback = callback;
state.renderUserdata = userdata;
}
static void pico_update(float dt) {
@ -304,3 +425,155 @@ HeadsetInterface lovrHeadsetPicoDriver = {
.renderTo = pico_renderTo,
.update = pico_update
};
// Activity callbacks
static lua_State* L;
static lua_State* T;
static Variant cookie;
static void lovrPicoBoot(void) {
lovrAssert(lovrPlatformInit(), "Failed to initialize platform");
lovrPlatformSetTime(0.);
L = luaL_newstate();
luax_setmainthread(L);
luaL_openlibs(L);
lua_getglobal(L, "package");
lua_getfield(L, -1, "preload");
luaL_register(L, NULL, lovrModules);
lua_pop(L, 2);
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 (lua_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 }));
}
Camera camera;
camera.stereo = false;
camera.canvas = canvas;
for (uint32_t i = 0; i < 2; i++) {
float fov = tanf(state.fov);
mat4_fov(camera.projection[i], -fov, fov, fov, -fov, state.clipNear, state.clipFar);
mat4_identity(camera.viewMatrix[i]);
mat4_translate(camera.viewMatrix[i], state.headPosition[0], state.headPosition[1] + state.offset, state.headPosition[2]);
mat4_rotateQuat(camera.viewMatrix[i], state.headOrientation);
mat4_translate(camera.viewMatrix[i], state.ipd * (eye == 0 ? -.5f : .5f), 0.f, 0.f);
mat4_invert(camera.viewMatrix[i]);
}
lovrGpuResetState();
lovrGraphicsSetCamera(&camera, true);
state.renderCallback(state.renderUserdata);
lovrGraphicsSetCamera(NULL, false);
}

View File

@ -6,35 +6,93 @@ import com.picovr.vractivity.Eye;
import com.picovr.vractivity.HmdState;
import com.picovr.vractivity.RenderInterface;
import com.picovr.vractivity.VRActivity;
import com.picovr.cvclient.ButtonNum;
import com.picovr.cvclient.CVController;
import com.picovr.cvclient.CVControllerListener;
import com.picovr.cvclient.CVControllerManager;
import com.picovr.picovrlib.cvcontrollerclient.ControllerClient;
import com.psmart.vrlib.PicovrSDK;
public class Activity extends VRActivity implements RenderInterface {
public class Activity extends VRActivity implements RenderInterface, CVControllerListener {
CVControllerManager controllerManager;
boolean controllersActive;
// Activity
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
if (ControllerClient.isControllerServiceExisted(this)) {
controllerManager = new CVControllerManager(this);
controllerManager.setListener(this);
}
lovrPicoOnCreate(getPackageCodePath());
}
public void onPause() {
super.onPause();
if (controllerManager != null) {
controllerManager.unbindService();
}
}
public void onResume() {
super.onResume();
PicovrSDK.SetEyeBufferSize(1920, 1920);
if (controllerManager != null) {
controllerManager.bindService();
}
}
// RenderInterface
public void initGL(int width, int height) {
//
lovrPicoSetDisplayDimensions(width, height);
}
public void onFrameBegin(HmdState state) {
//
if (controllersActive) {
for (int i = 0; i < 2; i++) {
CVController controller = (i == 0) ?
controllerManager.getMainController() :
controllerManager.getSubController();
if (controller == null || controller.getConnectState() == 0) {
lovrPicoUpdateControllerPose(i, false, 0, 0, 0, 0, 0, 0, 0);
continue;
}
float p[] = controller.getPosition();
float q[] = controller.getOrientation();
lovrPicoUpdateControllerPose(i, true, p[0], p[1], p[2], q[0], q[1], q[2], q[3]);
int thumbstick[] = controller.getTouchPad();
float trigger = (float) controller.getTriggerNum() / 255.f;
float thumbstickX = ((float) thumbstick[1] - 128.f) / (thumbstick[1] > 128 ? 127.f : 128.f);
float thumbstickY = ((float) thumbstick[0] - 128.f) / (thumbstick[0] > 128 ? 127.f : 128.f);
int buttons = 0;
ButtonNum gripButton = (i == 0) ? ButtonNum.buttonRG : ButtonNum.buttonLG; // Yes I know
buttons |= trigger >= .9f ? (1 << 0) : 0;
buttons |= controller.getButtonState(ButtonNum.click) ? (1 << 1) : 0;
buttons |= controller.getButtonState(gripButton) ? (1 << 2) : 0;
buttons |= controller.getButtonState(ButtonNum.app) ? (1 << 3) : 0;
buttons |= controller.getButtonState(ButtonNum.buttonAX) ? (1 << 4) : 0;
buttons |= controller.getButtonState(ButtonNum.buttonBY) ? (1 << 5) : 0;
lovrPicoUpdateControllerInput(i, buttons, trigger, thumbstickX, thumbstickY);
}
}
float p[] = state.getPos();
float q[] = state.getOrientation();
float fov = state.getFov();
float ipd = state.getIpd();
lovrPicoOnFrame(p[0], p[1], p[2], q[0], q[1], q[2], q[3], fov, ipd);
}
public void onDrawEye(Eye eye) {
//
lovrPicoDrawEye(eye.getType());
}
public void onFrameEnd() {
@ -65,6 +123,47 @@ public class Activity extends VRActivity implements RenderInterface {
//
}
// CVControllerListener
public void onBindSuccess() {
//
}
public void onBindFail() {
controllersActive = false;
}
public void onThreadStart() {
controllersActive = true;
}
public void onConnectStateChanged(int serial, int state) {
//
}
public void onMainControllerChanged(int serial) {
//
}
public void onChannelChanged(int device, int channel) {
//
}
// Native
protected native void lovrPicoOnCreate(String apkPath);
protected native void lovrPicoSetDisplayDimensions(int width, int height);
protected native void lovrPicoUpdateControllerPose(int hand, boolean active, float x, float y, float z, float qx, float qy, float qz, float qw);
protected native void lovrPicoUpdateControllerInput(int hand, int buttons, float trigger, float thumbstickX, float thumbstickY);
protected native void lovrPicoOnFrame(float x, float y, float z, float qx, float qy, float qz, float qw, float fov, float ipd);
protected native void lovrPicoDrawEye(int eye);
public void vibrate(int hand, float strength, float duration) {
if (controllerManager != null) {
int ms = (int) (duration * 1000.f);
ControllerClient.vibrateCV2ControllerStrength(strength, ms, hand);
}
}
static {
System.loadLibrary("lovr");
}

View File

@ -96,7 +96,7 @@ function lovr.boot()
timer = true
},
headset = {
drivers = { 'leap', 'openxr', 'oculus', 'vrapi', 'openvr', 'webxr', 'webvr', 'desktop' },
drivers = { 'leap', 'openxr', 'oculus', 'vrapi', 'pico', 'openvr', 'webxr', 'webvr', 'desktop' },
offset = 1.7,
msaa = 4
},

View File

@ -126,7 +126,7 @@ const char* lovrShaderFragmentSuffix = ""
" discard; \n"
" } \n"
"#endif \n"
#ifdef LOVR_WEBGL
#if defined(LOVR_WEBGL) || defined(LOVR_USE_PICO)
" lovrCanvas[0].rgb = pow(lovrCanvas[0].rgb, vec3(.4545)); \n"
#endif
"#endif \n"