Phone VR with MobileVRInterface
Executed lifecycle helper, lens parameters, units and phone-stereo limitations.
Phone VR with MobileVRInterface
MobileVRInterface is a built-in stereo-output interface for a phone display in a holder. Its XRServer name is Native mobile. This path is separate from OpenXR and ARCore. It models head rotation from available sensor input, while position uses a fixed EyeHeight: it does not track walking around a room.
For a device using OpenXR, follow OpenXR startup. This section covers the simple phone-stereo path and lifecycle diagnosis.
What successful startup means
Initialize creates a head tracker, makes the interface primary and returns true. Current implementation does this without a physical phone too. Reporting “headset connected” after Initialize alone is therefore incorrect. Two views, a head pose and fixed height do not establish sensor availability or tracking accuracy either.
Output needs a Viewport with UseXR=true and an XR rig. Initializing the interface does not enable this flag automatically. Do not run two components managing primary interface and the head tracker concurrently.
A single-owner example
The helper uses the registered interface, refuses to overwrite an existing head/primary, retains UseXR and releases only its own initialization:
using System;
using Reckfell;
public sealed class MobileVrPreview
{
private MobileVRInterface? current;
private XRTracker? head;
private Viewport? viewport;
private bool previousUseXr;
// One owner, main thread. Call TryStop before leaving the preview.
public bool TryStart(Viewport target)
{
ArgumentNullException.ThrowIfNull(target);
if (current != null) return false;
var candidate = XRServer.FindInterface("Native mobile") as MobileVRInterface;
if (candidate == null || candidate.IsInitialized()
|| XRServer.PrimaryInterface != null || XRServer.GetTracker("head") != null)
return false;
bool oldUseXr = target.UseXR;
if (!candidate.Initialize()) return false;
var tracker = XRServer.GetTracker("head");
if (tracker == null)
{
candidate.Uninitialize();
return false;
}
current = candidate;
head = tracker;
viewport = target;
previousUseXr = oldUseXr;
target.UseXR = true;
return true;
}
public bool TryStop()
{
var active = current;
if (active == null) return true;
// Do not unregister another provider's head tracker after a conflict.
if (XRServer.PrimaryInterface != active || XRServer.GetTracker("head") != head)
return false;
if (viewport != null && ReckfellObject.IsInstanceValid(viewport))
viewport.UseXR = previousUseXr;
active.Uninitialize();
current = null;
head = null;
viewport = null;
return true;
}
}Retain one MobileVrPreview instance in the controlling Entity. In _Ready, pass its live GetViewport() to TryStart and check the bool. Call TryStop when leaving preview, before deleting the owner. Use ReckfellApi.Print for messages, not the old GD.Print.
TryStop=false signals an ownership conflict: another component changed primary or head. The helper does not unregister another provider's tracker in that case. Resolve the conflict in controller logic; do not start a second XR provider over the first one. Repeating TryStop after a successful stop is safe.
The interface and tracker are borrowed from XRServer. Do not Dispose/Free them. The helper does not free the Viewport either. It changes no lens parameters, global WorldScale or reference frame: configure those separately before startup.
Parameters and units
| C# parameter | Default | Meaning |
|---|---|---|
| EyeHeight | 1.85 | Assumed height in meters before WorldScale, not measured phone position. |
| Iod | 6.0 | Interocular distance in centimeters. At WorldScale=1, eye offsets are −0.03 and +0.03. |
| DisplayWidth | 14.5 | Physical display width in centimeters, not pixel resolution. |
| DisplayToLens | 4.0 | Display-to-lens distance in centimeters. |
| Oversample | 1.5 | Render-target size multiplier that also affects frustum calculation; not MSAA. |
| K1, K2 | 0.215, 0.215 | Dimensionless lens-distortion coefficients, not universal holder calibration. |
| OffsetRect | (0, 0, 1, 1) | Output region relative to the full screen rectangle. |
| VrsMinRadius | 20 | Full-density central region as a percentage of size when using VRS. Setter range: 1–100. |
| VrsStrength | 1 | VRS-map strength, range 0.1–10. Guarantees no GPU saving. |
Measure actual display and holder parameters. Do not pass meters to DisplayToLens/Iod or centimeters to EyeHeight. The optical model needs meaningful positive dimensions. Many setters store numbers without checking physical correctness.
This path implements no automatic holder QR-profile loading: Initialize does not discover distortion coefficients or dimensions. Higher Oversample does not fix incorrect lens geometry. Code computes the per-eye target as half the window width × Oversample and window height × Oversample; larger values increase rendering volume.
Scale and eye caches
Set WorldScale before startup. Current GetCameraOffsets caches eye offsets. Execution found that changing WorldScale from 1 to 2 left previously computed ±0.03 unchanged. A controlled TryStop/TryStart rebuilt them as ±0.06. This is a reproduced current-engine limitation, not a fixed defect.
Changing Iod to another value clears offsets and projections; DisplayWidth, DisplayToLens and Oversample clear projections. Assigning the same value again is not a universal cache reset. Array getters can return a shared cache: do not edit its elements to change configuration.
Sensors and output
For Android, source documents input_devices/sensors/enable_accelerometer, enable_gravity, enable_gyroscope and enable_magnetometer. These enable input sources, not evidence that hardware exists. Check actual values and behavior on the target device. GetTrackingStatus and sensor-fusion quality were not accepted by this run.
The main output path splits the screen into left/right halves with lens distortion. A SubViewport without a screen rectangle produces no on-screen output through this interface and emits a warning: a separate render target does not automatically become a holder image. VRS needs an appropriate RD renderer and Viewport.VrsMode=Xr; VRS properties alone do not establish actual map use.
The C# example was tested as lifecycle code on Windows/SDK 1.0.0. This does not establish C# export to a phone. Confirm platform, language-runtime and module support in the selected build and on-device testing.
Diagnosis
| Symptom | Check |
|---|---|
| Native mobile is missing | The build's mobile_vr module; disable_xr disables it. |
| TryStart returns false | An initialized interface, occupied primary/head, repeated start or missing module. |
| Initialize succeeds but the head is static | Sensors and their settings; initialization success checks no phone. |
| Wrong height | EyeHeight in meters and WorldScale; this is a fixed-height model. |
| Stereo does not align | Iod/DisplayWidth/DisplayToLens in centimeters, holder parameters and distortion. |
| Eye offsets ignore scale changes | Stop your own preview and start after configuring scale; do not overwrite another provider's head. |
| TryStop returns false | A competing primary/head owner; the helper deliberately retains others' data. |
Verification scope
Installed Reckfell 1.0.0, Windows headless with XR off: 22 observations cover parameters, helper startup/shutdown, ownership conflicts, UseXR restoration, fixed height and two-view calculations. Sensors and imagery received no hardware acceptance. Original live-WorldScale cache failure remains a separate unsuccessful acceptance result in the report.
Phone hardware/C# export, GetTrackingStatus initialization, sensor accuracy, real lenses, stereo pixels, VRS output, comfort and FPS were not tested. Engine source, installed Runtime and SDK were unchanged.