Created
May 24, 2026 03:06
-
-
Save DigitalRedPanda/9f9316700d4d5696054e12c6469c73f0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include "alvr_client_core.h" | |
| #include "arcore_c_api.h" | |
| #include "cardboard.h" | |
| #include <GLES3/gl3.h> | |
| #include <EGL/egl.h> | |
| #include <algorithm> | |
| #include <android/log.h> | |
| #include <deque> | |
| #include <jni.h> | |
| #include <map> | |
| #include <thread> | |
| #include <unistd.h> | |
| #include <vector> | |
| #include <atomic> | |
| #include "nlohmann/json.hpp" | |
| #include "utils.h" | |
| using namespace nlohmann; | |
| using namespace std; | |
| uint64_t HEAD_ID = alvr_path_string_to_id("/user/head"); | |
| // TODO: Make this configurable. | |
| // Using ARCore orientation is more accurate, but causes a ~0.5 second delay, | |
| // which is probably nauseating for most folks. TODO. | |
| bool useARCoreOrientation = false; | |
| // Note: the Cardboard SDK cannot estimate display time and an heuristic is used instead. | |
| const uint64_t VSYNC_QUEUE_INTERVAL_NS = 50e6; | |
| const float FLOOR_HEIGHT = 1.5; | |
| const int MAXIMUM_TRACKING_FRAMES = 360; | |
| struct NativeContext { | |
| JavaVM *javaVm = nullptr; | |
| jobject javaContext = nullptr; | |
| CardboardHeadTracker *headTracker = nullptr; | |
| CardboardLensDistortion *lensDistortion = nullptr; | |
| CardboardDistortionRenderer *distortionRenderer = nullptr; | |
| bool arcoreEnabled = false; | |
| ArSession *arSession = nullptr; | |
| ArFrame *arFrame = nullptr; | |
| ArAnchor *arFloorAnchor = nullptr; | |
| GLuint arTexture = 0; | |
| AlvrQuat lastOrientation = {0.f, 0.f, 0.f, 0.f}; | |
| float lastPosition[3] = {0.f, 0.f, 0.f}; | |
| int screenWidth = 0; | |
| int screenHeight = 0; | |
| int screenRotation = 0; | |
| bool renderingParamsChanged = true; | |
| bool glContextRecreated = false; | |
| bool running = false; | |
| atomic<bool> streaming; | |
| float displayRefreshRate = 60.0f; | |
| std::thread inputThread; | |
| // Use one texture per eye, no need for swapchains. | |
| GLuint lobbyTextures[2] = {0, 0}; | |
| GLuint streamTextures[2] = {0, 0}; | |
| float eyeOffsets[2] = {0.0, 0.0}; | |
| AlvrFov fovArr[2] = {}; | |
| AlvrViewParams viewParams[2] = {}; | |
| AlvrDeviceMotion deviceMotion = {}; | |
| // Store last decoded frame for reprojection when no new frame arrives | |
| void *lastStreamBuffer = nullptr; | |
| int64_t lastStreamTimestampNs = -1; | |
| NativeContext() { | |
| streaming.store(false); | |
| memset(&fovArr, 0, (sizeof(fovArr))); | |
| memset(&viewParams, 0, (sizeof(viewParams))); | |
| memset(&deviceMotion, 0, (sizeof(deviceMotion))); | |
| } | |
| ~NativeContext() { | |
| streaming.store(false); | |
| if (inputThread.joinable()) { | |
| inputThread.join(); | |
| } | |
| } | |
| }; | |
| NativeContext CTX = {}; | |
| int64_t GetBootTimeNano() { | |
| struct timespec res = {}; | |
| clock_gettime(CLOCK_BOOTTIME, &res); | |
| return (int64_t)res.tv_sec * 1000000000LL + res.tv_nsec; | |
| } | |
| // Inverse unit quaternion | |
| AlvrQuat inverseQuat(AlvrQuat q) { return {-q.x, -q.y, -q.z, q.w}; } | |
| void cross(float a[3], float b[3], float out[3]) { | |
| out[0] = a[1] * b[2] - a[2] * b[1]; | |
| out[1] = a[2] * b[0] - a[0] * b[2]; | |
| out[2] = a[0] * b[1] - a[1] * b[0]; | |
| } | |
| void quatVecMultiply(AlvrQuat q, float v[3], float out[3]) { | |
| float rv[3], rrv[3]; | |
| float r[3] = {q.x, q.y, q.z}; | |
| cross(r, v, rv); | |
| cross(r, rv, rrv); | |
| for (int i = 0; i < 3; i++) { | |
| out[i] = v[i] + 2 * (q.w * rv[i] + rrv[i]); | |
| } | |
| } | |
| void offsetPosWithQuat(AlvrQuat q, float offset[3], float outPos[3]) { | |
| float rotatedOffset[3]; | |
| quatVecMultiply(q, offset, rotatedOffset); | |
| outPos[0] -= rotatedOffset[0]; | |
| outPos[1] -= rotatedOffset[1] - FLOOR_HEIGHT; | |
| outPos[2] -= rotatedOffset[2]; | |
| } | |
| AlvrFov getFov(CardboardEye eye) { | |
| float f[4]; | |
| CardboardLensDistortion_getFieldOfView(CTX.lensDistortion, eye, f); | |
| AlvrFov fov = {}; | |
| fov.left = -f[0]; | |
| fov.right = f[1]; | |
| fov.up = f[3]; | |
| fov.down = -f[2]; | |
| return fov; | |
| } | |
| AlvrPose getPose(uint64_t timestampNs) { | |
| AlvrPose pose = {}; | |
| bool returnLastPosition = false; | |
| if (!CTX.arcoreEnabled || (CTX.arcoreEnabled && !useARCoreOrientation)) { | |
| float pos[3]; | |
| float q[4]; | |
| CardboardHeadTracker_getPose(CTX.headTracker, (int64_t) timestampNs, kLandscapeLeft, pos, q); | |
| auto inverseOrientation = AlvrQuat{q[0], q[1], q[2], q[3]}; | |
| pose.orientation = inverseQuat(inverseOrientation); | |
| CTX.lastOrientation = pose.orientation; | |
| } | |
| if (CTX.arcoreEnabled && CTX.arSession != nullptr) { | |
| if (eglGetCurrentContext() == EGL_NO_CONTEXT) { | |
| throw std::runtime_error("Failed to get EGL context in getPose."); | |
| returnLastPosition = false; | |
| goto out; | |
| } | |
| int ret = ArSession_update(CTX.arSession, CTX.arFrame); | |
| if (ret != AR_SUCCESS) { | |
| error("getPose: ArSession_update failed (%d), using last position", ret); | |
| returnLastPosition = true; | |
| goto out; | |
| } | |
| ArCamera *arCamera = nullptr; | |
| ArFrame_acquireCamera(CTX.arSession, CTX.arFrame, &arCamera); | |
| ArTrackingState arTrackingState; | |
| ArCamera_getTrackingState(CTX.arSession, arCamera, &arTrackingState); | |
| if (arTrackingState != AR_TRACKING_STATE_TRACKING) { | |
| error("getPose: Camera is not tracking, using last position"); | |
| if (arTrackingState == AR_TRACKING_STATE_PAUSED) { | |
| error("- AR tracking state is PAUSED"); | |
| ArTrackingFailureReason failureReason; | |
| ArCamera_getTrackingFailureReason(CTX.arSession, arCamera, &failureReason); | |
| error("- Failure reason: %d", failureReason); | |
| } else if (arTrackingState == AR_TRACKING_STATE_STOPPED) { | |
| error("- AR tracking state is STOPPED"); | |
| } | |
| returnLastPosition = true; | |
| ArCamera_release(arCamera); | |
| goto out; | |
| } | |
| ArPose *arPose = nullptr; | |
| ArPose_create(CTX.arSession, nullptr, &arPose); | |
| ArCamera_getDisplayOrientedPose(CTX.arSession, arCamera, arPose); | |
| // ArPose_getPoseRaw() returns a pose in {qx, qy, qz, qw, tx, ty, tz} format. | |
| float arRawPose[7] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f}; | |
| ArPose_getPoseRaw(CTX.arSession, arPose, arRawPose); | |
| /* We determine floor position by finding the lowest detected plane. We do this by | |
| * placing an anchor in the center position of the plane, and if it's lower than the | |
| * currently placed anchor (CTX.floorAnchor), we replace it. | |
| * | |
| * (By default, ARCore's "world coordinates" space begins wherever the device is, but this | |
| * can desync over time. Anchor position adapts to world space movement. */ | |
| ArTrackableList *trackables = nullptr; | |
| ArTrackableList_create(CTX.arSession, &trackables); | |
| ArFrame_getUpdatedTrackables(CTX.arSession, CTX.arFrame, AR_TRACKABLE_PLANE, trackables); | |
| int32_t detectedPlaneCount; | |
| ArTrackableList_getSize(CTX.arSession, trackables, &detectedPlaneCount); | |
| for (int i = 0; i < detectedPlaneCount; i++) { | |
| ArTrackable* arTrackable = nullptr; | |
| ArTrackableList_acquireItem(CTX.arSession, trackables, i, | |
| &arTrackable); | |
| const ArPlane *plane = ArAsPlane(arTrackable); | |
| ArTrackingState planeTrackingState; | |
| ArTrackable_getTrackingState(CTX.arSession, arTrackable, &planeTrackingState); | |
| ArPlaneType planeType; | |
| ArPlane_getType(CTX.arSession, plane, &planeType); | |
| if (planeTrackingState == AR_TRACKING_STATE_TRACKING && | |
| planeType == AR_PLANE_HORIZONTAL_UPWARD_FACING) { | |
| ArPose *planePose = nullptr; | |
| ArPose_create(CTX.arSession, nullptr, &planePose); | |
| ArPlane_getCenterPose(CTX.arSession, plane, planePose); | |
| bool reanchor = false; | |
| if (CTX.arFloorAnchor == nullptr) { | |
| reanchor = true; | |
| } else { | |
| ArPose *currentFloorPose = nullptr; | |
| ArPose_create(CTX.arSession, nullptr, ¤tFloorPose); | |
| ArAnchor_getPose(CTX.arSession, CTX.arFloorAnchor, currentFloorPose); | |
| float currentFloorPoseRaw[7] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f}; | |
| ArPose_getPoseRaw(CTX.arSession, currentFloorPose, currentFloorPoseRaw); | |
| float planePoseRaw[7] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f}; | |
| ArPose_getPoseRaw(CTX.arSession, planePose, planePoseRaw); | |
| if (planePoseRaw[5] < currentFloorPoseRaw[5]) { | |
| info("Found new plane lower than pose (current %f vs new %f), reanchoring", | |
| currentFloorPoseRaw[5], planePoseRaw[5]); | |
| reanchor = true; | |
| } | |
| } | |
| if (reanchor) { | |
| if (CTX.arFloorAnchor != nullptr) { | |
| ArAnchor_detach(CTX.arSession, CTX.arFloorAnchor); | |
| ArAnchor_release(CTX.arFloorAnchor); | |
| } | |
| ArPose *planePoseNoRotation = ArPose_extractTranslation(CTX.arSession, | |
| planePose); | |
| ArTrackable_acquireNewAnchor(CTX.arSession, arTrackable, planePoseNoRotation, | |
| &CTX.arFloorAnchor); | |
| ArPose_destroy(planePoseNoRotation); | |
| } | |
| ArPose_destroy(planePose); | |
| } | |
| } | |
| ArTrackableList_destroy(trackables); | |
| float anchorRawPose[7] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f}; | |
| if (CTX.arFloorAnchor != nullptr) { | |
| ArPose *anchorPose = nullptr; | |
| ArPose_create(CTX.arSession, nullptr, &anchorPose); | |
| ArAnchor_getPose(CTX.arSession, CTX.arFloorAnchor, anchorPose); | |
| ArPose_getPoseRaw(CTX.arSession, anchorPose, anchorRawPose); | |
| ArPose_destroy(anchorPose); | |
| } else { | |
| // Before anchor is created, use camera's current position as reference | |
| // to prevent position jumps when anchor is first established | |
| anchorRawPose[4] = arRawPose[4]; // tx | |
| anchorRawPose[5] = arRawPose[5]; // ty (device height becomes "floor") | |
| anchorRawPose[6] = arRawPose[6]; // tz | |
| } | |
| // Position calculation remains the same | |
| pose.position[0] = arRawPose[4] - anchorRawPose[4]; // ✅ Now relative to anchor OR initial position | |
| pose.position[1] = arRawPose[5] - anchorRawPose[5]; // ✅ Floor-relative with smooth transition | |
| pose.position[2] = arRawPose[6] - anchorRawPose[6]; // ✅ Consistent handling | |
| CTX.lastPosition[0] = pose.position[0]; | |
| CTX.lastPosition[1] = pose.position[1]; | |
| CTX.lastPosition[2] = pose.position[2]; | |
| // for (int i = 0; i < 3; i++) { | |
| // CTX.lastPosition[i] = arRawPose[i + 4]; | |
| // } | |
| if (useARCoreOrientation) { | |
| auto orientation = AlvrQuat{arRawPose[0], arRawPose[1], arRawPose[2], | |
| arRawPose[3]}; | |
| pose.orientation = orientation; | |
| CTX.lastOrientation = pose.orientation; | |
| } | |
| ArPose_destroy(arPose); | |
| ArCamera_release(arCamera); | |
| } | |
| out: | |
| if (returnLastPosition) { | |
| pose.orientation = CTX.lastOrientation; | |
| for (int i = 0; i < 3; i++) { | |
| pose.position[i] = CTX.lastPosition[i]; | |
| } | |
| } | |
| return pose; | |
| } | |
| void updateViewConfigs(uint64_t targetTimestampNs = 0) { | |
| if (!targetTimestampNs) | |
| targetTimestampNs = GetBootTimeNano() + alvr_get_head_prediction_offset_ns(); | |
| AlvrPose headPose = getPose(targetTimestampNs); | |
| CTX.deviceMotion.device_id = HEAD_ID; | |
| CTX.deviceMotion.pose = headPose; | |
| float headToEye[3] = {CTX.eyeOffsets[kLeft], 0.0, 0.0}; | |
| CTX.viewParams[kLeft].pose = headPose; | |
| offsetPosWithQuat(headPose.orientation, headToEye, CTX.viewParams[kLeft].pose.position); | |
| CTX.viewParams[kLeft].fov = CTX.fovArr[kLeft]; | |
| headToEye[0] = CTX.eyeOffsets[kRight]; | |
| CTX.viewParams[kRight].pose = headPose; | |
| offsetPosWithQuat(headPose.orientation, headToEye, CTX.viewParams[kRight].pose.position); | |
| CTX.viewParams[kRight].fov = CTX.fovArr[kRight]; | |
| } | |
| void inputThread() { | |
| auto deadline = std::chrono::steady_clock::now(); | |
| if (CTX.arcoreEnabled) { | |
| /* ARCore requires an EGL context to work. Since we're calling it from a secondary | |
| * thread that is not the main GL thread, we need to provide our own context. */ | |
| info("inputThread: creating ARCore EGL context for input thread"); | |
| // 1. Initialize EGL | |
| EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); | |
| if (display == EGL_NO_DISPLAY) { | |
| throw std::runtime_error("Failed to get EGL display."); | |
| } | |
| if (!eglInitialize(display, nullptr, nullptr)) { | |
| throw std::runtime_error("Failed to initialize EGL."); | |
| } | |
| // 2. Choose EGL configuration | |
| EGLint numConfigs; | |
| EGLConfig config; | |
| EGLint configAttribs[] = { | |
| EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, | |
| EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, | |
| EGL_BLUE_SIZE, 8, | |
| EGL_GREEN_SIZE, 8, | |
| EGL_RED_SIZE, 8, | |
| EGL_ALPHA_SIZE, 8, | |
| EGL_NONE | |
| }; | |
| if (!eglChooseConfig(display, configAttribs, &config, 1, &numConfigs)) { | |
| throw std::runtime_error("Failed to choose EGL config."); | |
| } | |
| if (numConfigs == 0) { | |
| throw std::runtime_error("No suitable EGL configurations found."); | |
| } | |
| // 3. Create an offscreen (pbuffer) surface | |
| EGLint pbufferAttribs[] = { | |
| EGL_WIDTH, 1920, | |
| EGL_HEIGHT, 1920, | |
| EGL_NONE | |
| }; | |
| EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs); | |
| if (surface == EGL_NO_SURFACE) { | |
| throw std::runtime_error("Failed to create EGL pbuffer surface."); | |
| } | |
| // 4. Create an EGL context | |
| EGLint contextAttribs[] = { | |
| EGL_CONTEXT_CLIENT_VERSION, 3, // OpenGL ES 3.0 context | |
| EGL_NONE | |
| }; | |
| EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs); | |
| if (context == EGL_NO_CONTEXT) { | |
| throw std::runtime_error("Failed to create EGL context."); | |
| } | |
| // 5. Bind the context to the current thread | |
| if (!eglMakeCurrent(display, surface, surface, context)) { | |
| throw std::runtime_error("Failed to make EGL context current."); | |
| } | |
| } | |
| info("inputThread: thread staring..."); | |
| while (CTX.streaming.load()) { | |
| auto targetTimestampNs = GetBootTimeNano() + alvr_get_head_prediction_offset_ns(); | |
| updateViewConfigs(targetTimestampNs); | |
| alvr_send_tracking( | |
| targetTimestampNs, CTX.viewParams, &CTX.deviceMotion, 1, nullptr, nullptr); | |
| deadline += std::chrono::nanoseconds((uint64_t) (1e9 / CTX.displayRefreshRate / 3)); | |
| std::this_thread::sleep_until(deadline); | |
| } | |
| } | |
| extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) { | |
| CTX.javaVm = vm; | |
| return JNI_VERSION_1_6; | |
| } | |
| extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_initializeNative( | |
| JNIEnv *env, jobject obj, jint screenWidth, jint screenHeight, jfloat refreshRate, jboolean enableARCore) { | |
| CTX.javaContext = env->NewGlobalRef(obj); | |
| uint32_t viewWidth = std::max(screenWidth, screenHeight) / 2; | |
| uint32_t viewHeight = std::min(screenWidth, screenHeight); | |
| alvr_initialize_android_context((void *) CTX.javaVm, (void *) CTX.javaContext); | |
| float refreshRatesBuffer[1] = {refreshRate}; | |
| AlvrClientCapabilities caps = {}; | |
| caps.default_view_height = viewHeight; | |
| caps.default_view_width = viewWidth; | |
| caps.external_decoder = false; | |
| caps.refresh_rates = refreshRatesBuffer; | |
| caps.refresh_rates_count = 1; | |
| caps.foveated_encoding = | |
| true; // By default disable FFE (can be force-enabled by Server Settings | |
| caps.encoder_high_profile = true; | |
| caps.encoder_10_bits = true; | |
| caps.encoder_av1 = true; | |
| alvr_initialize(caps); | |
| CTX.displayRefreshRate = refreshRate; | |
| Cardboard_initializeAndroid(CTX.javaVm, CTX.javaContext); | |
| CTX.headTracker = CardboardHeadTracker_create(); | |
| CTX.arcoreEnabled = (bool) enableARCore; | |
| if (CTX.arcoreEnabled) { | |
| if (ArSession_create(env, CTX.javaContext, &CTX.arSession) != AR_SUCCESS) { | |
| error("initializeNative: Could not create ARCore session"); | |
| CTX.arcoreEnabled = false; | |
| return; | |
| } | |
| ArConfig* arConfig = nullptr; | |
| ArConfig_create(CTX.arSession, &arConfig); | |
| // Explicitly disable all unnecessary features to preserve CPU power. | |
| ArConfig_setDepthMode(CTX.arSession, arConfig, AR_DEPTH_MODE_DISABLED); | |
| ArConfig_setLightEstimationMode(CTX.arSession, arConfig, AR_LIGHT_ESTIMATION_MODE_DISABLED); | |
| ArConfig_setPlaneFindingMode(CTX.arSession, arConfig, AR_PLANE_FINDING_MODE_HORIZONTAL_AND_VERTICAL); | |
| ArConfig_setCloudAnchorMode(CTX.arSession, arConfig, AR_CLOUD_ANCHOR_MODE_DISABLED); | |
| // Set "latest camera image" update mode (ArSession_update returns immediately without blocking) | |
| ArConfig_setUpdateMode(CTX.arSession, arConfig, AR_UPDATE_MODE_LATEST_CAMERA_IMAGE); | |
| // TODO: Add camera config filter: | |
| // https://developers.google.com/ar/develop/c/camera-configs | |
| if (ArSession_configure(CTX.arSession, arConfig) != AR_SUCCESS) { | |
| error("initializeNative: Could not configure ARCore session"); | |
| return; | |
| } | |
| ArFrame_create(CTX.arSession, &CTX.arFrame); | |
| } | |
| } | |
| extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_destroyNative(JNIEnv *, | |
| jobject) { | |
| CTX.streaming.store(false); | |
| if (CTX.inputThread.joinable()) { | |
| CTX.inputThread.join(); | |
| } | |
| alvr_destroy_opengl(); | |
| alvr_destroy(); | |
| CardboardHeadTracker_destroy(CTX.headTracker); | |
| CTX.headTracker = nullptr; | |
| CardboardLensDistortion_destroy(CTX.lensDistortion); | |
| CTX.lensDistortion = nullptr; | |
| CardboardDistortionRenderer_destroy(CTX.distortionRenderer); | |
| CTX.distortionRenderer = nullptr; | |
| if (CTX.arcoreEnabled && CTX.arSession != nullptr) { | |
| ArSession_destroy(CTX.arSession); | |
| CTX.arSession = nullptr; | |
| ArFrame_destroy(CTX.arFrame); | |
| CTX.arFrame = nullptr; | |
| } | |
| } | |
| extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_resumeNative(JNIEnv *, | |
| jobject) { | |
| CardboardHeadTracker_resume(CTX.headTracker); | |
| if (CTX.arcoreEnabled && CTX.arSession != nullptr) { | |
| ArStatus arSessionStatus = ArSession_resume(CTX.arSession); | |
| if (arSessionStatus != AR_SUCCESS) { | |
| error("Failed to resume tracking: %d", arSessionStatus); | |
| } | |
| } | |
| CTX.renderingParamsChanged = true; | |
| uint8_t *buffer; | |
| int size; | |
| CardboardQrCode_getSavedDeviceParams(&buffer, &size); | |
| if (size == 0) { | |
| CardboardQrCode_scanQrCodeAndSaveDeviceParams(); | |
| } | |
| CardboardQrCode_destroy(buffer); | |
| CTX.running = true; | |
| alvr_resume(); | |
| } | |
| extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_pauseNative(JNIEnv *, | |
| jobject) { | |
| alvr_pause(); | |
| if (CTX.running) { | |
| CTX.running = false; | |
| } | |
| CardboardHeadTracker_pause(CTX.headTracker); | |
| } | |
| extern "C" JNIEXPORT void JNICALL | |
| Java_viritualisres_phonevr_ALVRActivity_surfaceCreatedNative(JNIEnv *, jobject) { | |
| alvr_initialize_opengl(); | |
| CTX.glContextRecreated = true; | |
| } | |
| extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_setScreenResolutionNative( | |
| JNIEnv *, jobject, jint width, jint height) { | |
| CTX.screenWidth = width; | |
| CTX.screenHeight = height; | |
| CTX.renderingParamsChanged = true; | |
| } | |
| extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_setScreenRotationNative( | |
| JNIEnv *, jobject, jint rotation) { | |
| CTX.screenRotation = rotation; | |
| CTX.renderingParamsChanged = true; | |
| } | |
| extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_sendBatteryLevel( | |
| JNIEnv *, jobject, jfloat level, jboolean plugged) { | |
| alvr_send_battery(HEAD_ID, level, plugged); | |
| } | |
| extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_renderNative(JNIEnv *, | |
| jobject) { | |
| try { | |
| if (CTX.renderingParamsChanged) { | |
| info("renderingParamsChanged, processing new params"); | |
| uint8_t *buffer; | |
| int size; | |
| CardboardQrCode_getSavedDeviceParams(&buffer, &size); | |
| if (size == 0) { | |
| CardboardQrCode_destroy(buffer); // must free even when empty | |
| return; | |
| } | |
| info("renderingParamsChanged, sending new params to alvr"); | |
| if (CTX.lensDistortion) { | |
| CardboardLensDistortion_destroy(CTX.lensDistortion); | |
| CTX.lensDistortion = nullptr; | |
| } | |
| info("renderingParamsChanged, destroyed distortion"); | |
| CTX.lensDistortion = | |
| CardboardLensDistortion_create(buffer, size, CTX.screenWidth, CTX.screenHeight); | |
| CardboardQrCode_destroy(buffer); | |
| if (CTX.distortionRenderer) { | |
| CardboardDistortionRenderer_destroy(CTX.distortionRenderer); | |
| CTX.distortionRenderer = nullptr; | |
| } | |
| const CardboardOpenGlEsDistortionRendererConfig config{kGlTexture2D}; | |
| CTX.distortionRenderer = CardboardOpenGlEs2DistortionRenderer_create(&config); | |
| for (int eye = 0; eye < 2; eye++) { | |
| CardboardMesh mesh; | |
| CardboardLensDistortion_getDistortionMesh( | |
| CTX.lensDistortion, (CardboardEye) eye, &mesh); | |
| CardboardDistortionRenderer_setMesh( | |
| CTX.distortionRenderer, &mesh, (CardboardEye) eye); | |
| float matrix[16] = {}; | |
| CardboardLensDistortion_getEyeFromHeadMatrix( | |
| CTX.lensDistortion, (CardboardEye) eye, matrix); | |
| CTX.eyeOffsets[eye] = matrix[12]; | |
| } | |
| CTX.fovArr[kLeft] = getFov(kLeft); | |
| CTX.fovArr[kRight] = getFov(kRight); | |
| if (CTX.arcoreEnabled && CTX.arSession != nullptr) { | |
| ArSession_setDisplayGeometry( | |
| CTX.arSession, CTX.screenRotation, CTX.screenWidth, CTX.screenHeight); | |
| } | |
| info("renderingParamsChanged, updating new view configs (FOV) to alvr"); | |
| // alvr_send_views_config(fovArr, CTX.eyeOffsets[0] - CTX.eyeOffsets[1]); | |
| } | |
| // Note: if GL context is recreated, old resources are already freed. | |
| if (CTX.renderingParamsChanged && !CTX.glContextRecreated) { | |
| info("Pausing ALVR since glContext is not recreated, deleting textures"); | |
| alvr_pause_opengl(); | |
| GL(glDeleteTextures(2, CTX.lobbyTextures)); | |
| } | |
| if (CTX.renderingParamsChanged || CTX.glContextRecreated) { | |
| info("Rebuilding, binding textures, Resuming ALVR since glContextRecreated %b, " | |
| "renderingParamsChanged %b", | |
| CTX.renderingParamsChanged, | |
| CTX.glContextRecreated); | |
| GL(glGenTextures(2, CTX.lobbyTextures)); | |
| for (auto &lobbyTexture : CTX.lobbyTextures) { | |
| GL(glBindTexture(GL_TEXTURE_2D, lobbyTexture)); | |
| GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); | |
| GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); | |
| GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); | |
| GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); | |
| GL(glTexImage2D(GL_TEXTURE_2D, | |
| 0, | |
| GL_RGB, | |
| CTX.screenWidth / 2, | |
| CTX.screenHeight, | |
| 0, | |
| GL_RGB, | |
| GL_UNSIGNED_BYTE, | |
| nullptr)); | |
| } | |
| const uint32_t *targetViews[2] = {(uint32_t *) &CTX.lobbyTextures[0], | |
| (uint32_t *) &CTX.lobbyTextures[1]}; | |
| alvr_resume_opengl(CTX.screenWidth / 2, CTX.screenHeight, targetViews, 1, true); | |
| if (CTX.arcoreEnabled && CTX.arSession != nullptr) { | |
| GLuint arTextureIdArray[1]; | |
| glGenTextures(1, arTextureIdArray); | |
| CTX.arTexture = arTextureIdArray[0]; | |
| GL(glBindTexture(GL_TEXTURE_2D, CTX.arTexture)); | |
| GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); | |
| GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); | |
| GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); | |
| GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); | |
| ArSession_setCameraTextureName(CTX.arSession, CTX.arTexture); | |
| } | |
| CTX.renderingParamsChanged = false; | |
| CTX.glContextRecreated = false; | |
| } | |
| AlvrEvent event; | |
| while (alvr_poll_event(&event)) { | |
| if (event.tag == ALVR_EVENT_HUD_MESSAGE_UPDATED) { | |
| auto message_length = alvr_hud_message(nullptr); | |
| auto message_buffer = std::vector<char>(message_length); | |
| alvr_hud_message(&message_buffer[0]); | |
| info("ALVR Poll Event: HUD Message Update - %s", &message_buffer[0]); | |
| if (message_length > 0) | |
| alvr_update_hud_message_opengl(&message_buffer[0]); | |
| } | |
| if (event.tag == ALVR_EVENT_STREAMING_STARTED) { | |
| info("ALVR Poll Event: ALVR_EVENT_STREAMING_STARTED, generating and binding " | |
| "textures..."); | |
| auto config = event.STREAMING_STARTED; | |
| auto settings_len = alvr_get_settings_json(nullptr); | |
| auto settings_buffer = std::vector<char>(settings_len); | |
| alvr_get_settings_json(&settings_buffer[0]); | |
| info("Got settings from ALVR Server - %s", &settings_buffer[0]); | |
| if (settings_len > 900) // to workthough logcat buffer limit | |
| info("Got settings from ALVR Server - %s", &settings_buffer[900]); | |
| json settings_json = json::parse(&settings_buffer[0]); | |
| GL(glGenTextures(2, CTX.streamTextures)); | |
| for (auto &streamTexture : CTX.streamTextures) { | |
| GL(glBindTexture(GL_TEXTURE_2D, streamTexture)); | |
| GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); | |
| GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); | |
| GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); | |
| GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); | |
| GL(glTexImage2D(GL_TEXTURE_2D, | |
| 0, | |
| GL_RGB, | |
| config.view_width, | |
| config.view_height, | |
| 0, | |
| GL_RGB, | |
| GL_UNSIGNED_BYTE, | |
| nullptr)); | |
| } | |
| CTX.fovArr[0] = getFov((CardboardEye) 0); | |
| CTX.fovArr[1] = getFov((CardboardEye) 1); | |
| info("ALVR Poll Event: ALVR_EVENT_STREAMING_STARTED, View configs updated..."); | |
| auto leftIntHandle = (uint32_t) CTX.streamTextures[0]; | |
| auto rightIntHandle = (uint32_t) CTX.streamTextures[1]; | |
| const uint32_t *textureHandles[2] = {&leftIntHandle, &rightIntHandle}; | |
| auto render_config = AlvrStreamConfig{}; | |
| render_config.view_resolution_width = config.view_width; | |
| render_config.view_resolution_height = config.view_height; | |
| render_config.swapchain_textures = textureHandles; | |
| render_config.swapchain_length = 1; | |
| render_config.enable_foveation = false; | |
| if (!settings_json["video"].is_null()) { | |
| if (!settings_json["video"]["foveated_encoding"].is_null()) { | |
| info("settings_json.video.foveated_encoding is %s", | |
| settings_json["video"]["foveated_encoding"].dump().c_str()); | |
| // Foveated encoding would be a "Enabled": {Array} or "Disabled" String | |
| if (!settings_json["video"]["foveated_encoding"].is_string()) { | |
| render_config.enable_foveation = true; | |
| render_config.foveation_center_size_x = | |
| settings_json["video"]["foveated_encoding"]["Enabled"] | |
| ["center_size_x"]; | |
| render_config.foveation_center_size_y = | |
| settings_json["video"]["foveated_encoding"]["Enabled"] | |
| ["center_size_y"]; | |
| render_config.foveation_center_shift_x = | |
| settings_json["video"]["foveated_encoding"]["Enabled"] | |
| ["center_shift_x"]; | |
| render_config.foveation_center_shift_y = | |
| settings_json["video"]["foveated_encoding"]["Enabled"] | |
| ["center_shift_y"]; | |
| render_config.foveation_edge_ratio_x = | |
| settings_json["video"]["foveated_encoding"]["Enabled"] | |
| ["edge_ratio_x"]; | |
| render_config.foveation_edge_ratio_y = | |
| settings_json["video"]["foveated_encoding"]["Enabled"] | |
| ["edge_ratio_y"]; | |
| } else | |
| info("foveated_encoding is Disabled"); | |
| } else | |
| error("settings_json doesn't have a video.foveated_encoding key"); | |
| } else | |
| error("settings_json doesn't have a video key"); | |
| info("Settings for foveation:"); | |
| info("render_config.enable_foveation: %b", render_config.enable_foveation); | |
| info("render_config.foveation_center_size_x: %f", | |
| render_config.foveation_center_size_x); | |
| info("render_config.foveation_center_size_y: %f", | |
| render_config.foveation_center_size_y); | |
| info("render_config.foveation_center_shift_x: %f", | |
| render_config.foveation_center_shift_x); | |
| info("render_config.foveation_center_shift_y: %f", | |
| render_config.foveation_center_shift_y); | |
| info("render_config.foveation_edge_ratio_x: %f", | |
| render_config.foveation_edge_ratio_x); | |
| info("render_config.foveation_edge_ratio_y: %f", | |
| render_config.foveation_edge_ratio_y); | |
| alvr_start_stream_opengl(render_config); | |
| info("ALVR Poll Event: ALVR_EVENT_STREAMING_STARTED, opengl stream started and " | |
| "input " | |
| "Thread started..."); | |
| CTX.streaming.store(true); | |
| CTX.inputThread = std::thread(inputThread); | |
| } else if (event.tag == ALVR_EVENT_STREAMING_STOPPED) { | |
| info("ALVR Poll Event: ALVR_EVENT_STREAMING_STOPPED, Waiting for inputThread to " | |
| "join..."); | |
| CTX.lastStreamBuffer = nullptr; // clear reprojection buffer on disconnect | |
| CTX.lastStreamTimestampNs = -1; | |
| CTX.streaming.store(false); | |
| CTX.inputThread.join(); | |
| GL(glDeleteTextures(2, CTX.streamTextures)); | |
| CTX.streamTextures[0] = 0; // zero out so stale handles can't be used | |
| CTX.streamTextures[1] = 0; | |
| info("ALVR Poll Event: ALVR_EVENT_STREAMING_STOPPED, Stream stopped deleted " | |
| "textures."); | |
| } | |
| } | |
| CardboardEyeTextureDescription viewsDescs[2] = {}; | |
| for (auto &viewsDesc : viewsDescs) { | |
| viewsDesc.left_u = 0.0; | |
| viewsDesc.right_u = 1.0; | |
| viewsDesc.top_v = 1.0; | |
| viewsDesc.bottom_v = 0.0; | |
| } | |
| if (CTX.streaming.load()) { | |
| void *streamHardwareBuffer = nullptr; | |
| AlvrViewParams dummyViewParams; | |
| auto timestampNs = alvr_get_frame(&dummyViewParams, &streamHardwareBuffer); | |
| if (timestampNs == -1) { | |
| // No new decoded frame this cycle. | |
| // Instead of returning (which causes a blank/ghost frame), reuse the | |
| // last valid buffer — this is basic async reprojection: same image, | |
| // but composited with the current head orientation by Cardboard. | |
| if (CTX.lastStreamBuffer == nullptr) { | |
| // We truly have no frame yet at all, nothing to show. | |
| return; | |
| } | |
| streamHardwareBuffer = CTX.lastStreamBuffer; | |
| timestampNs = CTX.lastStreamTimestampNs; | |
| } else { | |
| // New frame arrived — save it for future reprojection cycles. | |
| CTX.lastStreamBuffer = streamHardwareBuffer; | |
| CTX.lastStreamTimestampNs = timestampNs; | |
| } | |
| uint32_t swapchainIndices[2] = {0, 0}; | |
| alvr_render_stream_opengl(streamHardwareBuffer, swapchainIndices); | |
| alvr_report_submit(timestampNs, 0); | |
| viewsDescs[0].texture = CTX.streamTextures[0]; | |
| viewsDescs[1].texture = CTX.streamTextures[1]; | |
| } else { | |
| AlvrPose pose = getPose(GetBootTimeNano() + VSYNC_QUEUE_INTERVAL_NS); | |
| AlvrViewInput viewInputs[2] = {}; | |
| for (int eye = 0; eye < 2; eye++) { | |
| float headToEye[3] = {CTX.eyeOffsets[eye], 0.0, 0.0}; | |
| // offset head pos to Eye Position | |
| offsetPosWithQuat(pose.orientation, headToEye, viewInputs[eye].pose.position); | |
| viewInputs[eye].pose = pose; | |
| viewInputs[eye].fov = getFov((CardboardEye) eye); | |
| viewInputs[eye].swapchain_index = 0; | |
| } | |
| alvr_render_lobby_opengl(viewInputs); | |
| viewsDescs[0].texture = CTX.lobbyTextures[0]; | |
| viewsDescs[1].texture = CTX.lobbyTextures[1]; | |
| } | |
| // Note: the Cardboard SDK does not support reprojection! | |
| // todo: manually implement it? | |
| // info("nativeRendered: Rendering to Display..."); | |
| CardboardDistortionRenderer_renderEyeToDisplay(CTX.distortionRenderer, | |
| 0, | |
| 0, | |
| 0, | |
| CTX.screenWidth, | |
| CTX.screenHeight, | |
| &viewsDescs[0], | |
| &viewsDescs[1]); | |
| } catch (const json::exception &e) { | |
| error(std::string(std::string(__FUNCTION__) + std::string(__FILE_NAME__) + | |
| std::string(e.what())) | |
| .c_str()); | |
| } | |
| } | |
| extern "C" JNIEXPORT void JNICALL | |
| Java_viritualisres_phonevr_ALVRActivity_switchViewerNative(JNIEnv *, jobject) { | |
| CardboardQrCode_scanQrCodeAndSaveDeviceParams(); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #ifndef PHONEVR_UTILS_H | |
| #define PHONEVR_UTILS_H | |
| #include "alvr_client_core.h" | |
| #include "arcore_c_api.h" | |
| #include <GLES3/gl3.h> | |
| #include <android/log.h> | |
| #include <string> | |
| #define error(...) log(ALVR_LOG_LEVEL_ERROR, __FILE_NAME__, __LINE__, __func__, __VA_ARGS__) | |
| #define info(...) log(ALVR_LOG_LEVEL_INFO, __FILE_NAME__, __LINE__, __func__, __VA_ARGS__) | |
| #define debug(...) log(ALVR_LOG_LEVEL_DEBUG, __FILE_NAME__, __LINE__, __func__, __VA_ARGS__) | |
| const char LOG_TAG[] = "ALVR_PVR_NATIVE"; | |
| static void log(AlvrLogLevel level, | |
| const char *FILE_NAME, | |
| unsigned int LINE_NO, | |
| const char *FUNC, | |
| const char *format, | |
| ...) { | |
| va_list args; | |
| va_start(args, format); | |
| char buf[1024]; | |
| std::string format_str; | |
| format_str = std::string(FILE_NAME) + ":" + std::to_string(LINE_NO) + ": " + std::string(FUNC) + | |
| "():" + format; | |
| int count = vsnprintf(buf, sizeof(buf), format_str.c_str(), args); | |
| if (count > (int) sizeof(buf)) | |
| count = (int) sizeof(buf); | |
| if (count > 0 && buf[count - 1] == '\n') | |
| buf[count - 1] = '\0'; | |
| alvr_log(level, buf); | |
| switch (level) { | |
| case ALVR_LOG_LEVEL_DEBUG: | |
| __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s", buf); | |
| break; | |
| case ALVR_LOG_LEVEL_INFO: | |
| __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "%s", buf); | |
| break; | |
| case ALVR_LOG_LEVEL_ERROR: | |
| __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "%s", buf); | |
| break; | |
| case ALVR_LOG_LEVEL_WARN: | |
| __android_log_print(ANDROID_LOG_WARN, LOG_TAG, "%s", buf); | |
| break; | |
| } | |
| va_end(args); | |
| } | |
| static const char *GlErrorString(GLenum error) { | |
| switch (error) { | |
| case GL_NO_ERROR: | |
| return "GL_NO_ERROR"; | |
| case GL_INVALID_ENUM: | |
| return "GL_INVALID_ENUM"; | |
| case GL_INVALID_VALUE: | |
| return "GL_INVALID_VALUE"; | |
| case GL_INVALID_OPERATION: | |
| return "GL_INVALID_OPERATION"; | |
| case GL_INVALID_FRAMEBUFFER_OPERATION: | |
| return "GL_INVALID_FRAMEBUFFER_OPERATION"; | |
| case GL_OUT_OF_MEMORY: | |
| return "GL_OUT_OF_MEMORY"; | |
| default: | |
| return "unknown"; | |
| } | |
| } | |
| [[maybe_unused]] static void GLCheckErrors(const char *file, int line) { | |
| GLenum error = glGetError(); | |
| if (error == GL_NO_ERROR) { | |
| return; | |
| } | |
| while (error != GL_NO_ERROR) { | |
| error("GL error on %s:%d : %s", file, line, GlErrorString(error)); | |
| error = glGetError(); | |
| } | |
| abort(); | |
| } | |
| #define GL(func) \ | |
| func; \ | |
| GLCheckErrors(__FILE__, __LINE__) | |
| /* Returns a pose having the translation of this pose but no rotation. | |
| * The caller is responsible for freeing the original pose and the output pose. | |
| * (C++ port of Pose.extractTranslation.) */ | |
| static ArPose *ArPose_extractTranslation(ArSession *session, ArPose *in_pose) { | |
| float raw_pose[7] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f}; | |
| ArPose_getPoseRaw(session, in_pose, raw_pose); | |
| raw_pose[0] = 0.f; | |
| raw_pose[1] = 0.f; | |
| raw_pose[2] = 0.f; | |
| raw_pose[3] = 0.f; | |
| ArPose *out_pose = nullptr; | |
| ArPose_create(session, raw_pose, &out_pose); | |
| return out_pose; | |
| } | |
| #endif // PHONEVR_UTILS_H |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment