feat(radiance-xr): 接入运行时 XR 会话控制并公开 VRProxy 功能接口#195
feat(radiance-xr): 接入运行时 XR 会话控制并公开 VRProxy 功能接口#195BeyondtheApex wants to merge 2 commits intoMinecraft-Radiance:mainfrom
Conversation
## 功能更新记录 ### 1) 运行时 XR 会话控制联动 - 新增按世界状态驱动的会话开关策略:仅在进世界后开启 XR 会话,离开世界后停止会话。 - 保持用户开关与运行态解耦:允许开启 VR 配置但不强制在大厅占用 XR 会话。 - 会话切换后通过底层重建机制自动进入 mono 或 stereo 渲染路径。 ### 2) 客户端 Tick 集成 - 在客户端 Tick 流程挂接 XR 会话控制器。 - 在 Tick 中同步键鼠 VR 状态(世界朝向与玩家朝向),未启用 VR 时自动 no-op。 ### 3) 键鼠 VR 追踪空间同步 - 支持通过 mouseYaw 驱动世界空间朝向(world orientation)。 - 使用头显姿态推导玩家朝向(yaw/pitch),实现键鼠与 HMD 的协同朝向。 ### 4) Java 侧 XR 能力封装 - 对 JNI 接口提供稳定的 Java 包装层,统一参数边界处理和调用入口。 - 提供会话控制、姿态查询、控制器输入、触觉、性能统计、设备信息等能力。 ## 公开 XR 功能接口(Radiance 对外 API) 下列接口由 Java 层 VRProxy 对外公开,供上层玩法或 UI 系统调用。 ### A. 会话与开关 - setEnabled(boolean enabled) - isEnabled() - startXRSession() - stopXRSession() - getSessionState() - getSystemName() ### B. 渲染配置 - setRenderScale(float renderScale) - setIPD(float ipd) - setWorldScale(float worldScale) - getEyeCount() - getEyeRenderWidth() - getEyeRenderHeight() - getRefreshRate() - getRecommendedResolution() ### C. 头显与眼参数 - getHeadPose() - getEyeFov(int eye) ### D. 控制器输入 - getControllerPose(int hand) - getControllerButtons(int hand) ### E. 触觉反馈 - vibrate(int hand, float amplitude, long durationNs, float frequency) - vibrate(int hand, float amplitude, int durationMs) - stopVibration(int hand) ### F. 世界空间与重定位 - setWorldOrientation(org.joml.Quaternionfc q) - setWorldPosition(float x, float y, float z) - getWorldPosition() - recenter() - getFloorHeight() ### G. 性能指标 - getPerformanceStats() ## 主要对接文件(Radiance) - src/main/java/com/radiance/client/proxy/vulkan/VRProxy.java - src/main/java/com/radiance/client/vr/XRSessionController.java - src/main/java/com/radiance/client/vr/VRMouseState.java - src/main/java/com/radiance/client/RadianceClient.java ## 与底层 MCVR 的契约说明 - Radiance 仅暴露并组织 XR API,不直接持有底层渲染资源。 - 底层会话与渲染资源生命周期由 MCVR 管理,Radiance 通过 VRProxy 发起控制与读取状态。
There was a problem hiding this comment.
Pull request overview
该 PR 为 Radiance XR/VR 集成补齐了 Java 侧运行时会话控制与对外 VRProxy API,并在客户端 Tick 中同步键鼠 VR 的追踪空间/玩家朝向,同时为渲染管线与世界 UBO 增加了 VR/stereo 相关参数入口。
Changes:
- 新增
VRProxyJNI Java 封装层,公开 XR 会话控制、姿态/控制器/性能等查询与设置接口。 - 新增运行时
XRSessionController+VRMouseState,并通过 mixin 拦截鼠标 look 以实现键鼠 VR yaw 累积与 HMD 驱动 pitch。 - 渲染侧增加 stereo 相关数据:Pipeline 构建参数传入
eyeCount,World UBO size 扩容,ray_tracing 模块新增 foveated 配置项。
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/resources/radiance.mixins.json | 注册新的 VR 鼠标同步 mixin |
| src/main/resources/modules/ray_tracing.yaml | 增加 foveated 相关 attribute 配置项 |
| src/main/java/com/radiance/mixins/vr_input/VRMouseSyncMixin.java | 拦截鼠标 look,将 yaw 累积到 VR tracking-space |
| src/main/java/com/radiance/mixin_related/MixinPlugin.java | 简化 shouldApplyMixin 返回逻辑 |
| src/main/java/com/radiance/client/vr/XRSessionController.java | 基于世界状态启停 XR session |
| src/main/java/com/radiance/client/vr/VRMouseState.java | Tick 内同步 worldOrientation 与玩家朝向 |
| src/main/java/com/radiance/client/proxy/vulkan/VRProxy.java | 新增 VR JNI 代理与对外 API 包装层 |
| src/main/java/com/radiance/client/proxy/vulkan/BufferProxy.java | World UBO size 扩容以容纳 VR stereo 字段 |
| src/main/java/com/radiance/client/pipeline/Pipeline.java | 构建时同步 eyeCount 并传入 native build 参数 |
| src/main/java/com/radiance/client/option/Options.java | 新增 VR 配置项读写,并在 setter 中调用 VRProxy |
| src/main/java/com/radiance/client/RadianceClient.java | 注册 VRMouseState / XRSessionController 的 tick 回调 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Sync eyeCount from VRSystem before building | ||
| eyeCount = VRProxy.isEnabled() ? VRProxy.getEyeCount() : 1; |
There was a problem hiding this comment.
eyeCount is derived from VRProxy.isEnabled() ? VRProxy.getEyeCount() : 1 and then passed to native as the view count. Two issues: (1) isEnabled() may be true even when an XR session is not running (per PR description, VR can be enabled without starting a session in lobby), which can cause the pipeline to be built in stereo unnecessarily; (2) getEyeCount() is not sanitized, so a transient 0/invalid value would propagate into native. Consider basing this on an explicit “session active” signal and clamping the result to a supported range (typically 1..2) before using it.
| // Sync eyeCount from VRSystem before building | |
| eyeCount = VRProxy.isEnabled() ? VRProxy.getEyeCount() : 1; | |
| // Sync eyeCount from VRSystem before building and clamp to supported range [1, 2] | |
| int rawEyeCount = VRProxy.isEnabled() ? VRProxy.getEyeCount() : 1; | |
| eyeCount = Math.min(Math.max(rawEyeCount, 1), 2); |
| private void radiance$interceptMouseForVR(double cursorDeltaX, double cursorDeltaY, CallbackInfo ci) { | ||
| if (!VRProxy.isEnabled() || !VRMouseState.mouseTrackingEnabled) return; | ||
| if (!((Object) this instanceof ClientPlayerEntity)) return; | ||
|
|
||
| // Accumulate yaw only (MC convention: cursorDeltaX * 0.15 = degrees). | ||
| // Negate because worldOrientation's inverse is applied in C++ rendering: | ||
| // positive mouseYaw should rotate view rightward (negative yaw in MC). | ||
| VRMouseState.mouseYaw -= (float) (cursorDeltaX * 0.15); | ||
| ci.cancel(); |
There was a problem hiding this comment.
changeLookDirection is cancelled whenever VRProxy.isEnabled() is true, but isEnabled() appears to reflect the user/runtime-enabled flag (set via Options.setVREnabled) rather than “XR session is actively running with valid head pose”. In cases like VR enabled in lobby / before the XR session starts (or if head pose is invalid), this will cancel vanilla mouse look without providing an HMD-driven fallback, effectively freezing camera rotation. Consider gating the interception on an explicit “session running / pose valid” condition (e.g., session state, or a dedicated VRProxy.isSessionActive()), and only cancel when that condition is true.
| if (headPose == null || headPose.length < 7 || headPose[6] == 0) return; | ||
|
|
There was a problem hiding this comment.
The head-pose validity check treats headPose[6] == 0 (qw==0) as invalid. A quaternion with w==0 can be a valid 180° rotation, so this will incorrectly skip legitimate poses and stop syncing player yaw/pitch in those cases. Since VRProxy.nativeGetHeadPose() is documented as “zeroes if not valid”, use a stronger validity check (e.g., all components near zero, or quaternion norm > epsilon), or include an explicit validity flag in the returned array.
| if (headPose == null || headPose.length < 7 || headPose[6] == 0) return; | |
| if (headPose == null || headPose.length < 7) return; | |
| // VRProxy.nativeGetHeadPose() is documented as "zeroes if not valid". | |
| // Treat a pose as invalid only if all components are (nearly) zero, | |
| // so that valid 180° rotations with qw == 0 are still accepted. | |
| boolean allZero = true; | |
| for (int i = 0; i < 7; i++) { | |
| if (Math.abs(headPose[i]) > 1e-6f) { | |
| allZero = false; | |
| break; | |
| } | |
| } | |
| if (allZero) return; |
| public static void setVRRenderScale(float renderScale, boolean write) { | ||
| Options.vrRenderScale = renderScale; | ||
| VRProxy.setRenderScale(renderScale); | ||
| if (write) { | ||
| overwriteConfig(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Options.setVRRenderScale stores the raw renderScale into Options.vrRenderScale, but VRProxy.setRenderScale clamps the value before sending it to native. This can leave the persisted config/UI state out of sync with what the runtime actually uses (e.g., negative values get saved but are clamped in native). Clamp (or otherwise sanitize) the value before assigning to Options.vrRenderScale / writing to properties; same applies to vrIPD and vrWorldScale vs their clamping in VRProxy.
| boolean inWorld = client.world != null && client.isFinishedLoading(); | ||
| boolean targetActive = Options.vrEnabled && inWorld; | ||
|
|
||
| if (targetActive == lastTargetActive) { | ||
| return; | ||
| } | ||
|
|
||
| // Keep runtime prepared according to user setting, but only run XR session in-world. | ||
| VRProxy.setEnabled(Options.vrEnabled); | ||
|
|
||
| if (targetActive) { | ||
| lastTargetActive = VRProxy.startXRSession(); | ||
| } else { | ||
| VRProxy.stopXRSession(); | ||
| lastTargetActive = false; | ||
| } |
There was a problem hiding this comment.
XRSessionController.tick will attempt to start the XR session every tick as long as targetActive is true and VRProxy.startXRSession() returns false (because lastTargetActive remains false). If session startup can fail transiently (runtime not ready, permission dialogs, etc.), this can spam native start attempts and logs and may have performance/instability implications. Consider tracking “desired active” separately from “session active”, and/or adding a retry/backoff strategy or only retrying when session state changes.
| int size = 560; | ||
| int size = 832; // must match sizeof(WorldUBO) in shared.hpp (includes VR stereo fields) | ||
| ByteBuffer bb = stack.malloc(size); | ||
| long addr = memAddress(bb); |
There was a problem hiding this comment.
stack.malloc(size) returns uninitialized memory. After increasing the UBO size to 832 (to include VR stereo fields), any new/trailing fields not explicitly written by Java will contain garbage and can lead to undefined shader behavior if the native side/shaders read them. Either zero-initialize the buffer (e.g., stack allocation that clears, or explicit memset) and/or explicitly write default values for the newly added VR-related fields when not in stereo.
| long addr = memAddress(bb); | |
| long addr = memAddress(bb); | |
| memSet(addr, (byte) 0, size); // zero-initialize UBO to avoid garbage in unused/trailing fields |
功能更新记录
1) 运行时 XR 会话控制联动
2) 客户端 Tick 集成
3) 键鼠 VR 追踪空间同步
4) Java 侧 XR 能力封装
公开 XR 功能接口(Radiance 对外 API)
下列接口由 Java 层 VRProxy 对外公开,供上层玩法或 UI 系统调用。
A. 会话与开关
B. 渲染配置
C. 头显与眼参数
D. 控制器输入
E. 触觉反馈
F. 世界空间与重定位
G. 性能指标
主要对接文件(Radiance)
与底层 MCVR 的契约说明
测试情况
9700x 9070xt上运行正常
仅测试了nrd和fsr3路径
设备不足,无法测试dlss路径