Notes:

- We can actually use a single Activity.java file for oculus/pico now
- We can unconditionally compile os_android.c on Android
- No need for including extra jars in build system
- Headset rendering is guaranteed synchronous now, no need to ref L
- Add an "android flavor" build setting to differentiate between oculus
  and pico devices, since they both use OpenXR.
- Update the pico manifest to reflect their OpenXR sample
- Remove some OpenGL hacks that aren't necessary anymore
This commit is contained in:
bjorn 2022-03-22 15:47:18 -07:00
parent f1cc668298
commit d1dc2f3199
16 changed files with 49 additions and 948 deletions

3
.gitmodules vendored
View File

@ -16,9 +16,6 @@
[submodule "deps/openxr"]
path = deps/openxr
url = https://github.com/khronosgroup/openxr-sdk
[submodule "deps/pico"]
path = deps/pico
url = https://github.com/lovr-org/pico_native_sdk
[submodule "deps/oculus-openxr"]
path = deps/oculus-openxr
url = https://github.com/lovr-org/ovr_openxr_mobile_sdk

View File

@ -18,7 +18,6 @@ option(LOVR_ENABLE_TIMER "Enable the timer module" ON)
option(LOVR_USE_LUAJIT "Use LuaJIT instead of Lua" ON)
option(LOVR_USE_OPENXR "Enable the OpenXR backend for the headset module" ON)
option(LOVR_USE_WEBXR "Enable the WebXR backend for the headset module" OFF)
option(LOVR_USE_PICO "Enable the Pico backend for the headset module" OFF)
option(LOVR_USE_DESKTOP "Enable the keyboard/mouse backend for the headset module" ON)
option(LOVR_USE_STEAM_AUDIO "Enable the Steam Audio spatializer (be sure to also set LOVR_STEAM_AUDIO_PATH)" OFF)
option(LOVR_USE_OCULUS_AUDIO "Enable the Oculus Audio spatializer (be sure to also set LOVR_OCULUS_AUDIO_PATH)" OFF)
@ -55,6 +54,7 @@ if(EMSCRIPTEN)
set(CMAKE_EXECUTABLE_SUFFIX ".html")
set(LOVR_USE_WEBXR ON)
elseif(ANDROID)
set(ANDROID_FLAVOR "oculus" CACHE STRING "Which Android flavor to build (oculus or pico)")
find_package(Java REQUIRED)
set(LOVR_USE_DESKTOP OFF)
if(LOVR_BUILD_EXE)
@ -175,10 +175,17 @@ endif()
if(LOVR_ENABLE_HEADSET AND LOVR_USE_OPENXR)
include_directories(deps/openxr/include)
if(ANDROID)
set(LOVR_OPENXR_OCULUS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps/oculus-openxr" CACHE STRING "The path to the Oculus OpenXR loader")
add_library(openxr_loader SHARED IMPORTED)
set_target_properties(openxr_loader PROPERTIES IMPORTED_LOCATION "${LOVR_OPENXR_OCULUS_PATH}/Libs/Android/${ANDROID_ABI}/Release/libopenxr_loader.so")
set(LOVR_OPENXR openxr_loader)
if(ANDROID_FLAVOR STREQUAL "oculus")
set(LOVR_OPENXR_OCULUS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps/oculus-openxr" CACHE STRING "The path to the Oculus OpenXR loader")
set_target_properties(openxr_loader PROPERTIES IMPORTED_LOCATION "${LOVR_OPENXR_OCULUS_PATH}/Libs/Android/${ANDROID_ABI}/Release/libopenxr_loader.so")
elseif(ANDROID_FLAVOR STREQUAL "pico")
set(LOVR_OPENXR_PICO_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps/pico-openxr" CACHE STRING "The path to the Pico OpenXR loader")
set_target_properties(openxr_loader PROPERTIES IMPORTED_LOCATION "${LOVR_OPENXR_PICO_PATH}/Libs/Android/${ANDROID_ABI}/libopenxr_loader.so")
else()
message(FATAL_ERROR "Unsupported Android flavor (expected: oculus or pico)")
endif()
else()
if(LOVR_SYSTEM_OPENXR)
pkg_search_module(OPENXR openxr)
@ -194,14 +201,6 @@ if(LOVR_ENABLE_HEADSET AND LOVR_USE_OPENXR)
endif()
endif()
# Pico Native SDK (1.3.3)
if(LOVR_ENABLE_HEADSET AND LOVR_USE_PICO)
set(LOVR_PICO_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps/pico/aar" CACHE STRING "The path to the Pico SDK folder")
add_library(Pvr_NativeSDK SHARED IMPORTED)
set_target_properties(Pvr_NativeSDK PROPERTIES IMPORTED_LOCATION "${LOVR_PICO_PATH}/jni/${ANDROID_ABI}/libPvr_NativeSDK.so")
set(LOVR_PICO Pvr_NativeSDK)
endif()
# pthreads
if(LOVR_ENABLE_THREAD AND NOT (WIN32 OR EMSCRIPTEN))
set(THREADS_PREFER_PTHREAD_FLAG ON)
@ -302,7 +301,7 @@ set(LOVR_SRC
src/util.c
)
if(LOVR_BUILD_EXE AND NOT LOVR_USE_PICO)
if(LOVR_BUILD_EXE)
list(APPEND LOVR_SRC src/main.c)
endif()
@ -331,7 +330,6 @@ target_link_libraries(lovr
${LOVR_OPENGL}
${LOVR_OPENXR}
${LOVR_OCULUS_AUDIO}
${LOVR_PICO}
${LOVR_PTHREADS}
${LOVR_EMSCRIPTEN_FLAGS}
)
@ -449,10 +447,6 @@ if(LOVR_ENABLE_HEADSET)
target_compile_definitions(lovr PRIVATE LOVR_USE_OPENXR)
target_sources(lovr PRIVATE src/modules/headset/headset_openxr.c)
endif()
if(LOVR_USE_PICO)
target_compile_definitions(lovr PRIVATE LOVR_USE_PICO)
target_sources(lovr PRIVATE src/modules/headset/headset_pico.c)
endif()
if(LOVR_USE_WEBXR)
target_compile_definitions(lovr PRIVATE LOVR_USE_WEBXR)
target_sources(lovr PRIVATE src/modules/headset/headset_webxr.c)
@ -636,6 +630,7 @@ elseif(EMSCRIPTEN)
target_compile_definitions(lovr PRIVATE LOVR_WEBGL)
configure_file(src/resources/lovr.ico favicon.ico COPYONLY)
elseif(ANDROID)
target_sources(lovr PRIVATE src/core/os_android.c)
target_link_libraries(lovr log EGL GLESv3 android dl)
target_compile_definitions(lovr PRIVATE LOVR_GLES)
target_include_directories(lovr PRIVATE "${ANDROID_NDK}/sources/android/native_app_glue")
@ -651,35 +646,20 @@ elseif(ANDROID)
if(LOVR_BUILD_EXE)
set(ANDROID_JAR "${ANDROID_SDK}/platforms/${ANDROID_PLATFORM}/android.jar")
set(ANDROID_TOOLS "${ANDROID_SDK}/build-tools/${ANDROID_BUILD_TOOLS_VERSION}")
set(ANDROID_MANIFEST "${CMAKE_CURRENT_SOURCE_DIR}/src/resources/AndroidManifest_${ANDROID_FLAVOR}.xml" CACHE STRING "The AndroidManifest.xml file to use")
# If assets are included in the apk then add '-A assets' to aapt, otherwise don't add any flags
if(ANDROID_ASSETS)
set(ANDROID_ASSETS -A ${ANDROID_ASSETS})
endif()
# Flavor-specific config:
# - Imported targets need to have their libraries manually copied to raw/lib/<ABI>
# - Figure out which Java class (Activity) and AndroidManifest.xml to use
# - Oculus uses the regular android os layer, pico implements its own in the headset backend
# - Some of the Pico SDK is in a jar that has to be added to the classpath and d8 invocation
if(ANDROID_PACKAGE)
set(PACKAGE_RENAME "--rename-manifest-package" "${ANDROID_PACKAGE}")
endif()
if(LOVR_USE_OPENXR)
set(MANIFEST "oculus")
set(ACTIVITY "openxr")
target_sources(lovr PRIVATE src/core/os_android.c)
get_target_property(OPENXR_LIB ${LOVR_OPENXR} IMPORTED_LOCATION)
file(COPY ${OPENXR_LIB} DESTINATION raw/lib/${ANDROID_ABI})
set(ANDROID_CLASSPATH "${ANDROID_JAR}")
elseif(LOVR_USE_PICO)
set(MANIFEST "pico")
set(ACTIVITY "pico")
get_target_property(PICO_LIB ${LOVR_PICO} IMPORTED_LOCATION)
file(COPY ${PICO_LIB} DESTINATION raw/lib/${ANDROID_ABI})
set(EXTRA_JAR "${LOVR_PICO_PATH}/classes.jar")
if(WIN32)
set(ANDROID_CLASSPATH "${ANDROID_JAR};${EXTRA_JAR}")
else()
set(ANDROID_CLASSPATH "${ANDROID_JAR}:${EXTRA_JAR}")
endif()
endif()
if(LOVR_USE_OCULUS_AUDIO)
@ -692,20 +672,14 @@ elseif(ANDROID)
file(COPY ${PHONON_LIB} DESTINATION raw/lib/${ANDROID_ABI})
endif()
set(ANDROID_MANIFEST "${CMAKE_CURRENT_SOURCE_DIR}/src/resources/AndroidManifest_${MANIFEST}.xml" CACHE STRING "The AndroidManifest.xml file to use")
if(ANDROID_PACKAGE)
set(PACKAGE_RENAME "--rename-manifest-package" "${ANDROID_PACKAGE}")
endif()
# Make an apk
add_custom_target(
buildAPK ALL
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND ${CMAKE_COMMAND} -E copy "${ANDROID_MANIFEST}" AndroidManifest.xml
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/src/resources/Activity_${ACTIVITY}.java Activity.java
COMMAND ${Java_JAVAC_EXECUTABLE} -classpath "${ANDROID_CLASSPATH}" -d . Activity.java
COMMAND ${ANDROID_TOOLS}/d8 --min-api ${ANDROID_NATIVE_API_LEVEL} --output raw ${EXTRA_JAR} org/lovr/app/Activity.class
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/src/resources/Activity.java Activity.java
COMMAND ${Java_JAVAC_EXECUTABLE} -classpath "${ANDROID_JAR}" -d . Activity.java
COMMAND ${ANDROID_TOOLS}/d8 --min-api ${ANDROID_NATIVE_API_LEVEL} --output raw org/lovr/app/Activity.class
COMMAND
${ANDROID_TOOLS}/aapt
package -f

View File

@ -24,7 +24,6 @@ config = {
headsets = {
desktop = true,
openxr = false, -- if provided, should be path to folder containing OpenXR loader library
pico = false,
webxr = false
},
spatializers = {
@ -39,6 +38,7 @@ config = {
buildtools = '30.0.3',
keystore = '/path/to/keystore',
keystorepass = 'pass:password',
flavor = 'oculus', -- or pico
manifest = nil, -- path to custom AndroidManifest.xml
package = nil, -- package id, like org.lovr.app
project = nil -- path to LÖVR project to include in apk
@ -156,7 +156,7 @@ if target == 'wasm' then
end
if target == 'android' then
assert(config.headsets.pico or config.headsets.openxr, 'Please enable pico or openxr')
assert(config.headsets.openxr, 'You probably want to enable OpenXR')
hosts = { win32 = 'windows-x86_64', macos = 'darwin-x86_64', linux = 'linux-x86_64' }
host = hosts[tup.getconfig('TUP_PLATFORM')]
cc = ('%s/toolchains/llvm/prebuilt/%s/bin/clang'):format(config.android.ndk, host)
@ -320,12 +320,6 @@ if config.headsets.openxr then
end
end
if config.headsets.pico then
assert(target == 'android', 'Pico is not supported on this target')
lflags += '-lPvr_NativeSDK'
copy('deps/pico/jni/arm64-v8a/libPvr_NativeSDK.so', '$(bin)/%b')
end
if config.spatializers.oculus then
cflags_headset_oculus += '-Ideps/AudioSDK/Include'
ovraudio_libs = {
@ -445,43 +439,30 @@ if target == 'android' then
config.android[key] = #value > 0 and value or config.android[key]
end
activity =
config.headsets.pico and 'src/resources/Activity_pico.java' or
config.headsets.openxr and 'src/resources/Activity_openxr.java'
java = 'bin/Activity.java'
class = 'org/lovr/app/Activity.class'
binclass = 'bin/' .. class
jar = 'bin/lovr.jar'
dex = 'bin/apk/classes.dex'
androidversion = config.android.version
androidjar = ('%s/platforms/android-%d/android.jar'):format(config.android.sdk, androidversion)
extrajar = config.headsets.pico and 'deps/pico/classes.jar' or nil
classpathsep = tup.getconfig('TUP_PLATFORM') == 'win32' and ';' or ':'
classpath = table.concat({ androidjar, extrajar }, classpathsep)
package = #config.android.package > 0 and ('--rename-manifest-package ' .. config.android.package) or ''
project = #config.android.project > 0 and ('-A ' .. config.android.project) or ''
manifest = config.android.manifest or
config.headsets.pico and 'src/resources/AndroidManifest_pico.xml' or
config.headsets.openxr and 'src/resources/AndroidManifest_oculus.xml'
tools = config.android.sdk .. '/build-tools/' .. config.android.buildtools
ks = config.android.keystore
kspass = config.android.keystorepass
unaligned = 'bin/.lovr.apk.unaligned'
unsigned = 'bin/.lovr.apk.unsigned'
apk = 'bin/lovr.apk'
manifest = config.android.manifest or ('src/resources/AndroidManifest_%s.xml'):format(config.android.flavor)
package = #config.android.package > 0 and ('--rename-manifest-package ' .. config.android.package) or ''
project = #config.android.project > 0 and ('-A ' .. config.android.project) or ''
version = config.android.version
ks = config.android.keystore
kspass = config.android.keystorepass
androidjar = ('%s/platforms/android-%d/android.jar'):format(config.android.sdk, version)
tools = config.android.sdk .. '/build-tools/' .. config.android.buildtools
copy(manifest, 'bin/AndroidManifest.xml')
copy(activity, java)
tup.rule(java, '^ JAVAC %b^ javac -classpath $(classpath) -d bin %f', binclass)
copy('src/resources/Activity.java', java)
tup.rule(java, '^ JAVAC %b^ javac -classpath $(androidjar) -d bin %f', binclass)
tup.rule(binclass, '^ JAR %b^ jar -cf %o -C bin $(class)', jar)
tup.rule({ jar, extrajar }, '^ D8 %b^ $(tools)/d8 --min-api $(androidversion) --output bin/apk %f', dex)
tup.rule(jar, '^ D8 %b^ $(tools)/d8 --min-api $(version) --output bin/apk %f', dex)
tup.rule(
{ 'bin/AndroidManifest.xml', extra_inputs = { lib('*'), dex } },
'^ AAPT %b^ $(tools)/aapt package $(package) -F %o -M %f -0 so -I $(androidjar) $(project) bin/apk',

1
deps/pico vendored

@ -1 +0,0 @@
Subproject commit 418d31425e3873749bbd6a7cb97641635df6e398

View File

@ -12,7 +12,6 @@
StringEntry lovrHeadsetDriver[] = {
[DRIVER_DESKTOP] = ENTRY("desktop"),
[DRIVER_OPENXR] = ENTRY("openxr"),
[DRIVER_PICO] = ENTRY("pico"),
[DRIVER_WEBXR] = ENTRY("webxr"),
{ 0 }
};
@ -74,32 +73,11 @@ StringEntry lovrDeviceAxis[] = {
{ 0 }
};
typedef struct {
lua_State* L;
int ref;
} HeadsetRenderData;
static HeadsetRenderData headsetRenderData;
static void renderHelper(void* userdata) {
HeadsetRenderData* renderData = userdata;
lua_State* L = renderData->L;
#ifdef LOVR_USE_PICO
luax_geterror(L);
if (lua_isnil(L, -1) && renderData->ref != LUA_REFNIL) {
lua_pushcfunction(L, luax_getstack);
lua_rawgeti(L, LUA_REGISTRYINDEX, renderData->ref);
if (lua_pcall(L, 0, 0, -2)) {
luax_seterror(L);
}
lua_pop(L, 1); // pop luax_getstack
}
lua_pop(L, 1);
#else
lua_State* L = userdata;
if (lua_isfunction(L, -1)) {
lua_call(L, 0, 0);
}
#endif
}
static Device luax_optdevice(lua_State* L, int index) {
@ -596,20 +574,7 @@ static int l_lovrHeadsetAnimate(lua_State* L) {
static int l_lovrHeadsetRenderTo(lua_State* L) {
lua_settop(L, 1);
#ifdef LOVR_USE_PICO
if (headsetRenderData.ref != LUA_NOREF) {
luaL_unref(L, LUA_REGISTRYINDEX, headsetRenderData.ref);
}
headsetRenderData.ref = luaL_ref(L, LUA_REGISTRYINDEX);
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD);
headsetRenderData.L = lua_tothread(L, -1);
lua_pop(L, 1);
#else
headsetRenderData.L = L;
#endif
lovrHeadsetDisplayDriver->renderTo(renderHelper, &headsetRenderData);
lovrHeadsetDisplayDriver->renderTo(renderHelper, L);
lovrGraphicsSetViewMatrix(0, NULL);
lovrGraphicsSetViewMatrix(1, NULL);
lovrGraphicsSetProjection(0, NULL);
@ -769,7 +734,5 @@ int luaopen_lovr_headset(lua_State* L) {
luax_atexit(L, lovrHeadsetDestroy);
lovrHeadsetInit(drivers, driverCount, supersample, offset, msaa, overlay);
headsetRenderData.ref = LUA_NOREF;
return 1;
}

View File

@ -401,7 +401,7 @@ Color lovrGraphicsGetBackgroundColor() {
void lovrGraphicsSetBackgroundColor(Color color) {
state.backgroundColor = state.linearBackgroundColor = color;
#if !defined(LOVR_WEBGL) && !defined(LOVR_USE_PICO)
#if !defined(LOVR_WEBGL)
gammaCorrect(&state.linearBackgroundColor);
#endif
}
@ -840,7 +840,7 @@ void lovrGraphicsFlushMesh(Mesh* mesh) {
}
void lovrGraphicsClear(Color* color, float* depth, int* stencil) {
#if !defined(LOVR_WEBGL) && !defined(LOVR_USE_PICO)
#if !defined(LOVR_WEBGL)
if (color) gammaCorrect(color);
#endif
if (color || depth || stencil) lovrGraphicsFlush();

View File

@ -236,7 +236,6 @@ 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

@ -1539,35 +1539,6 @@ 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);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, state.vertexArray->ibo);
}
for (size_t i = 0; i < MAX_BUFFER_TYPES; i++) {
if (!state.vertexArray || i != BUFFER_INDEX) {
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) {
#ifdef LOVR_GL
lovrAssert(state.activeTimer == ~0u, "Attempt to start a new GPU timer while one is already active!");

View File

@ -21,9 +21,6 @@ bool lovrHeadsetInit(HeadsetDriver* drivers, size_t count, float supersample, fl
#ifdef LOVR_USE_OPENXR
case DRIVER_OPENXR: interface = &lovrHeadsetOpenXRDriver; break;
#endif
#ifdef LOVR_USE_PICO
case DRIVER_PICO: interface = &lovrHeadsetPicoDriver; break;
#endif
#ifdef LOVR_USE_WEBXR
case DRIVER_WEBXR: interface = &lovrHeadsetWebXRDriver; break;
#endif

View File

@ -13,7 +13,6 @@ struct Texture;
typedef enum {
DRIVER_DESKTOP,
DRIVER_OPENXR,
DRIVER_PICO,
DRIVER_WEBXR
} HeadsetDriver;
@ -145,7 +144,6 @@ typedef struct HeadsetInterface {
// Available drivers
extern HeadsetInterface lovrHeadsetOpenXRDriver;
extern HeadsetInterface lovrHeadsetPicoDriver;
extern HeadsetInterface lovrHeadsetWebXRDriver;
extern HeadsetInterface lovrHeadsetDesktopDriver;

View File

@ -1,611 +0,0 @@
#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);
}

View File

@ -1,170 +0,0 @@
package org.lovr.app;
import android.os.Bundle;
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, 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() {
//
}
public void onRenderPause() {
//
}
public void onRenderResume() {
//
}
public void onRendererShutdown() {
//
}
public void surfaceChangedCallBack(int width, int height) {
//
}
public void renderEventCallBack(int i) {
//
}
public void onTouchEvent() {
//
}
// 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

@ -1,12 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.lovr.app">
<uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application android:allowBackup="false" android:label="LÖVR" android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
<uses-feature android:name="android.hardware.vr.headtracking" android:required="true"/>
<uses-feature android:glEsVersion="0x00030002" android:required="true"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<application android:allowBackup="false" android:label="LÖVR">
<meta-data android:name="pvr.app.type" android:value="vr"/>
<activity android:name="Activity">
<activity android:name="Activity" android:launchMode="singleTask" android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
<meta-data android:name="android.app.lib_name" android:value="lovr"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

View File

@ -121,7 +121,7 @@ function lovr.boot()
debug = false
},
headset = {
drivers = { 'openxr', 'pico', 'webxr', 'desktop' },
drivers = { 'openxr', 'webxr', 'desktop' },
supersample = false,
offset = 1.7,
msaa = 4,

View File

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