Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,26 @@ else()
message(STATUS "NRD Denoiser Disabled")
endif()

# OpenXR SDK
option(MCVR_ENABLE_OPENXR "Enable OpenXR VR support" ON)

if (MCVR_ENABLE_OPENXR)
message(STATUS "OpenXR Support Enabled")
include(FetchContent)
FetchContent_Declare(
openxr_sdk
GIT_REPOSITORY https://github.com/KhronosGroup/OpenXR-SDK.git
GIT_TAG release-1.1.43
GIT_SHALLOW TRUE
EXCLUDE_FROM_ALL
)
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_API_LAYERS OFF CACHE BOOL "" FORCE)
set(BUILD_CONFORMANCE_TESTS OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(openxr_sdk)
set(OPENXR_INCLUDE_DIR ${openxr_sdk_SOURCE_DIR}/include)
else()
message(STATUS "OpenXR Support Disabled")
endif()

add_subdirectory(src)
9 changes: 9 additions & 0 deletions src/common/shared.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,15 @@ namespace Data {
T_UINT endPortalTextureID;
T_UINT pad4;
T_UINT pad5;

// VR stereo fields
T_MAT4 eyeViewOffsets[2]; // per-eye view offset (translate ±ipd/2)
T_MAT4 eyeProjOffsets[2]; // per-eye projection offset (identity for symmetric)
T_FLOAT ipd;
T_UINT stereoEnabled; // 0=mono, 1=stereo
T_FLOAT foveatedInnerRadius; // normalized radius of full-res inner circle (1.0 = full screen)
T_UINT foveatedOuterBlockSize; // block size for outer region (1=off, 2=2x2, 4=4x4)
T_VEC2 foveatedCenter; // normalised gaze centre (0.5,0.5 = screen centre)
};

struct SkyUBO {
Expand Down
5 changes: 5 additions & 0 deletions src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ if (MCVR_ENABLE_NRD)
target_compile_definitions(core PUBLIC NRD_STATIC_LIBRARY=1)
target_compile_definitions(core PUBLIC MCVR_ENABLE_NRD)
endif ()
if (MCVR_ENABLE_OPENXR)
target_link_libraries(core PUBLIC openxr_loader)
target_include_directories(core PUBLIC ${OPENXR_INCLUDE_DIR})
target_compile_definitions(core PUBLIC MCVR_ENABLE_OPENXR XR_USE_GRAPHICS_API_VULKAN)
endif ()
if (MSVC)
target_compile_definitions(core PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
target_compile_options(core PRIVATE /utf-8)
Expand Down
292 changes: 292 additions & 0 deletions src/core/middleware/com_radiance_client_proxy_vulkan_VRProxy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
#include "com_radiance_client_proxy_vulkan_VRProxy.h"

#include "core/render/renderer.hpp"
#include "core/render/render_framework.hpp"

JNIEXPORT void JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeSetEnabled(JNIEnv *,
jclass,
jboolean enabled) {
Renderer::options.vrEnabled = enabled;

#ifdef MCVR_ENABLE_OPENXR
if (Renderer::is_initialized()) {
auto framework = Renderer::instance().framework();
if (framework != nullptr && !enabled) {
framework->stopXRSession();
}
}
#endif
}

JNIEXPORT jboolean JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeStartXRSession(JNIEnv *,
jclass) {
#ifdef MCVR_ENABLE_OPENXR
if (!Renderer::is_initialized()) return JNI_FALSE;
auto framework = Renderer::instance().framework();
if (framework == nullptr) return JNI_FALSE;
return framework->startXRSession() ? JNI_TRUE : JNI_FALSE;
#else
return JNI_FALSE;
#endif
}

JNIEXPORT void JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeStopXRSession(JNIEnv *,
jclass) {
#ifdef MCVR_ENABLE_OPENXR
if (!Renderer::is_initialized()) return;
auto framework = Renderer::instance().framework();
if (framework == nullptr) return;
framework->stopXRSession();
#endif
}

JNIEXPORT void JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeSetRenderScale(JNIEnv *,
jclass,
jfloat renderScale) {
Renderer::options.vrRenderScale = renderScale;
}

JNIEXPORT void JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeSetIPD(JNIEnv *,
jclass,
jfloat ipd) {
Renderer::options.vrIPD = ipd;
}

JNIEXPORT void JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeSetWorldScale(JNIEnv *,
jclass,
jfloat worldScale) {
Renderer::options.vrWorldScale = worldScale;
}

JNIEXPORT void JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeSetWorldOrientation(
JNIEnv *, jclass, jfloat qx, jfloat qy, jfloat qz, jfloat qw) {
if (!Renderer::is_initialized()) return;
Renderer::instance().vrSystem().worldOrientation = glm::normalize(glm::quat(qw, qx, qy, qz));
}

JNIEXPORT jboolean JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeIsEnabled(JNIEnv *,
jclass) {
return Renderer::options.vrEnabled;
}

JNIEXPORT jint JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetEyeCount(JNIEnv *,
jclass) {
#ifdef MCVR_ENABLE_OPENXR
if (!Renderer::is_initialized()) return Renderer::options.vrEnabled ? 2 : 1;
auto framework = Renderer::instance().framework();
if (framework != nullptr && framework->isXRSessionRunning()) return 2;
return 1;
#else
return Renderer::options.vrEnabled ? 2 : 1;
#endif
}

JNIEXPORT jint JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetEyeRenderWidth(JNIEnv *,
jclass) {
if (!Renderer::is_initialized()) return 0;
return static_cast<jint>(Renderer::instance().vrSystem().eyeRenderWidth());
}

JNIEXPORT jint JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetEyeRenderHeight(JNIEnv *,
jclass) {
if (!Renderer::is_initialized()) return 0;
return static_cast<jint>(Renderer::instance().vrSystem().eyeRenderHeight());
}

JNIEXPORT jfloat JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetRefreshRate(JNIEnv *,
jclass) {
if (!Renderer::is_initialized()) return 0.0f;
return Renderer::instance().vrSystem().config.refreshRate;
}

JNIEXPORT jfloatArray JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetHeadPose(JNIEnv *env,
jclass) {
jfloatArray result = env->NewFloatArray(7);
if (!Renderer::is_initialized()) return result;
const auto &vr = Renderer::instance().vrSystem();
const auto &pose = vr.headPose;
if (!pose.valid) return result;

// Return raw tracking-space pose — worldOrientation is applied only
// in the C++ rendering path (buffers.cpp). Java-side VRData handles
// the worldRotation transform independently.
const glm::vec3 &pos = pose.position;
const glm::quat &ori = pose.orientation;

float data[7] = {pos.x, pos.y, pos.z, ori.x, ori.y, ori.z, ori.w};
env->SetFloatArrayRegion(result, 0, 7, data);
return result;
}

JNIEXPORT jfloatArray JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetEyeFov(JNIEnv *env,
jclass,
jint eye) {
jfloatArray result = env->NewFloatArray(4);
if (!Renderer::is_initialized() || eye < 0 || eye > 1) return result;
const auto &ep = Renderer::instance().vrSystem().eyes[eye];
float data[4] = {ep.tanLeft, ep.tanRight, ep.tanUp, ep.tanDown};
env->SetFloatArrayRegion(result, 0, 4, data);
return result;
}

JNIEXPORT jintArray JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetRecommendedResolution(JNIEnv *env,
jclass) {
jintArray result = env->NewIntArray(2);
if (!Renderer::is_initialized()) return result;
const auto &eye0 = Renderer::instance().vrSystem().eyes[0];
jint data[2] = {static_cast<jint>(eye0.recommendedWidth), static_cast<jint>(eye0.recommendedHeight)};
env->SetIntArrayRegion(result, 0, 2, data);
return result;
}

// ---- Controller input ----

JNIEXPORT jfloatArray JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetControllerPose(
JNIEnv *env, jclass, jint hand) {
// Returns [px, py, pz, qx, qy, qz, qw, vx, vy, vz, valid] = 11 floats
jfloatArray result = env->NewFloatArray(11);
if (!Renderer::is_initialized() || hand < 0 || hand > 1) return result;
const auto &vr = Renderer::instance().vrSystem();
const auto &ctrl = vr.controllers[hand];

// Return raw tracking-space pose — worldOrientation is applied only
// in the C++ rendering path (buffers.cpp). Java-side VRData handles
// the worldRotation transform independently.
const glm::vec3 &pos = ctrl.position;
const glm::quat &ori = ctrl.orientation;
const glm::vec3 &vel = ctrl.linearVelocity;

float data[11] = {
pos.x, pos.y, pos.z,
ori.x, ori.y, ori.z, ori.w,
vel.x, vel.y, vel.z,
ctrl.valid ? 1.0f : 0.0f
};
env->SetFloatArrayRegion(result, 0, 11, data);
return result;
}

JNIEXPORT jfloatArray JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetControllerButtons(
JNIEnv *env, jclass, jint hand) {
// Returns [triggerValue, gripValue, thumbstickX, thumbstickY,
// triggerPressed, gripPressed, primaryButton, secondaryButton,
// thumbstickClick, menuButton] = 10 floats
jfloatArray result = env->NewFloatArray(10);
if (!Renderer::is_initialized() || hand < 0 || hand > 1) return result;
const auto &ctrl = Renderer::instance().vrSystem().controllers[hand];
float data[10] = {
ctrl.triggerValue, ctrl.gripValue,
ctrl.thumbstick.x, ctrl.thumbstick.y,
ctrl.triggerPressed ? 1.0f : 0.0f,
ctrl.gripPressed ? 1.0f : 0.0f,
ctrl.primaryButton ? 1.0f : 0.0f,
ctrl.secondaryButton ? 1.0f : 0.0f,
ctrl.thumbstickClick ? 1.0f : 0.0f,
ctrl.menuButton ? 1.0f : 0.0f
};
env->SetFloatArrayRegion(result, 0, 10, data);
return result;
}

// ---- Haptics ----

JNIEXPORT void JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeVibrate(
JNIEnv *, jclass, jint hand, jfloat amplitude, jlong durationNs, jfloat frequency) {
#ifdef MCVR_ENABLE_OPENXR
if (!Renderer::is_initialized() || hand < 0 || hand > 1) return;
auto *xrCtx = Renderer::instance().framework()->xrContext();
if (!xrCtx || !xrCtx->isSessionRunning()) return;
xrCtx->input().vibrate(
xrCtx->session(), static_cast<uint32_t>(hand), amplitude, durationNs, frequency);
#endif
}

JNIEXPORT void JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeStopVibration(
JNIEnv *, jclass, jint hand) {
#ifdef MCVR_ENABLE_OPENXR
if (!Renderer::is_initialized() || hand < 0 || hand > 1) return;
auto *xrCtx = Renderer::instance().framework()->xrContext();
if (!xrCtx || !xrCtx->isSessionRunning()) return;
xrCtx->input().stopVibration(xrCtx->session(), static_cast<uint32_t>(hand));
#endif
}

// ---- Performance stats ----

JNIEXPORT jfloatArray JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetPerformanceStats(
JNIEnv *env, jclass) {
// Returns [gpuFrameTimeMs, cpuFrameTimeMs, cpuWorkMs, cpuWaitMs, compositorTargetMs, fps, droppedFrames, headroom] = 8 floats
jfloatArray result = env->NewFloatArray(8);
if (!Renderer::is_initialized()) return result;
const auto &stats = Renderer::instance().vrSystem().perfStats;
float data[8] = {
stats.gpuFrameTimeMs, stats.cpuFrameTimeMs,
stats.cpuWorkMs, stats.cpuWaitMs,
stats.compositorTargetMs, stats.fps,
static_cast<float>(stats.droppedFrames), stats.headroom
};
env->SetFloatArrayRegion(result, 0, 8, data);
return result;
}

// ---- World position offset ----

JNIEXPORT void JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeSetWorldPosition(
JNIEnv *, jclass, jfloat x, jfloat y, jfloat z) {
if (!Renderer::is_initialized()) return;
Renderer::instance().vrSystem().worldPosition = glm::vec3(x, y, z);
}

JNIEXPORT jfloatArray JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetWorldPosition(
JNIEnv *env, jclass) {
jfloatArray result = env->NewFloatArray(3);
if (!Renderer::is_initialized()) return result;
const auto &pos = Renderer::instance().vrSystem().worldPosition;
float data[3] = {pos.x, pos.y, pos.z};
env->SetFloatArrayRegion(result, 0, 3, data);
return result;
}

JNIEXPORT void JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeRecenter(
JNIEnv *, jclass) {
if (!Renderer::is_initialized()) return;
Renderer::instance().vrSystem().recenter();
}

// ---- Session state / device info ----

JNIEXPORT jfloat JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetFloorHeight(
JNIEnv *, jclass) {
#ifdef MCVR_ENABLE_OPENXR
if (!Renderer::is_initialized()) return 0.0f;
auto *xrCtx = Renderer::instance().framework()->xrContext();
if (!xrCtx) return 0.0f;
return xrCtx->floorHeight();
#else
return 0.0f;
#endif
}

JNIEXPORT jint JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetSessionState(
JNIEnv *, jclass) {
#ifdef MCVR_ENABLE_OPENXR
if (!Renderer::is_initialized()) return 0;
auto *xrCtx = Renderer::instance().framework()->xrContext();
if (!xrCtx) return 0;
return static_cast<jint>(xrCtx->sessionState());
#else
return 0;
#endif
}

JNIEXPORT jstring JNICALL Java_com_radiance_client_proxy_vulkan_VRProxy_nativeGetSystemName(
JNIEnv *env, jclass) {
#ifdef MCVR_ENABLE_OPENXR
if (!Renderer::is_initialized()) return env->NewStringUTF("unknown");
auto *xrCtx = Renderer::instance().framework()->xrContext();
if (!xrCtx) return env->NewStringUTF("unknown");
return env->NewStringUTF(xrCtx->systemName().c_str());
#else
return env->NewStringUTF("simulation");
#endif
}
Loading