WebXR: browser startup and limitations
Asynchronous startup, reference spaces, input and actual Web-export limitations.
WebXR: browser startup and limitations
WebXR connects a web application to a browser-supported XR device. It is a separate export target, not a way to run a Windows build inside a browser. A WebXRInterface class in the SDK does not establish a working browser session.
Current C# limitation: the installed SDK is 1.0.0, but its C# export plugin does not implement Web as a target. Do not expect a C# desktop project to become a WebXR application by selecting Web export. A native web template and a project using a scripting/runtime configuration supported by that template are separate requirements. This article does not claim a successful Reckfell C# browser export.
Requirements before connecting a headset
- A web build containing the WebXR module and a compatible project. The module requires OpenGL support and XR enabled;
proxy_to_pthreaddisables it on Web. - The Compatibility renderer. Web platform flags disable RenderingDevice; desktop Vulkan/Forward+ settings are not a browser recipe.
- A browser and device that support the requested session mode.
inline,immersive-vrandimmersive-arare distinct requests; support for one does not establish the others. - A secure page context and browser permission. Start an immersive session from the user's button action. For embedded pages, check the embedding site's XR permissions policy too. See the WebXR specification.
The ordinary page must remain usable when XR is unavailable or refused. Start with a small scene and a working 2D entry/exit interface before adding expensive rendering.
Correct session sequence
One component should own startup, signal subscriptions and viewport restoration. Do not run the OpenXR startup routine and WebXR startup routine against the same origin simultaneously.
- Find the registered interface named
WebXRthrough XRServer. A missing interface means this build/target cannot provide it; do not manually create the abstract WebXRInterface. - Subscribe to SessionSupported, SessionStarted, SessionEnded and SessionFailed before querying or starting. Retain the original Viewport.UseXR value for restoration.
- Call IsSessionSupported with the intended mode, for example
immersive-vr. It returns no support boolean: SessionSupported reports the mode and result later. Keep the enter button disabled while waiting and match the returned mode to your request. - Configure SessionMode, RequiredFeatures, OptionalFeatures and RequestedReferenceSpaceTypes before startup. Required/optional feature values are comma-separated strings in the engine API, not JSON arrays.
- In the enter button's user-action handler, call Initialize directly. Avoid awaiting unrelated work first. Disable repeated startup while the request is pending.
- Enable Viewport.UseXR after SessionStarted, then inspect ReferenceSpaceType and EnabledFeatures. Initialize returning true or IsInitialized becoming true is not proof that the browser accepted the session: native code sets initialized before the asynchronous request completes.
- On SessionFailed or SessionEnded, restore the previous viewport flag, show the ordinary UI and allow a deliberate retry. Retain the failure message. Unsubscribe when your owner component exits; do not Dispose the borrowed registered interface.
Early validation can emit SessionFailed while Initialize is still on the stack. Have subscriptions and fallback state ready first. Do not overwrite a failure handler's state merely because Initialize returned.
The current JS support-query bridge handles a resolved support result but has no rejection handler for isSessionSupported(). A rejected browser promise may leave your SessionSupported callback absent. Give pending UI a timeout/error path; a timeout means query unresolved, not proof that the mode is unsupported. This source limitation was not fixed in the engine.
Reference space and features
The interface defaults are SessionMode=inline and RequestedReferenceSpaceTypes=local; configure an immersive mode explicitly.
| Setting | Practical meaning |
|---|---|
RequiredFeatures=local-floor | Startup must obtain the floor-space feature. Use only if your game cannot function without it. |
OptionalFeatures=local-floor | Request floor support without making it mandatory. |
RequestedReferenceSpaceTypes=local-floor,local | Try reference spaces in order; the second is a fallback, not an additional simultaneous origin. |
| ReferenceSpaceType | Actual selected space after SessionStarted; may differ from the first request. |
| EnabledFeatures | Reported accepted feature list. An optional request alone does not enable gameplay that requires it. |
If the selected space is local, do not treat Y=0 as a measured physical floor. Keep standing and seated setups distinct. A browser may estimate floor height; application content still needs an appropriate origin. Requesting a browser feature also does not prove that Reckfell exposes every related API.
Rendering path
Current source can fall back from XRWebGLBinding/projection layers to XRWebGLLayer: this happens when layers are disabled, the binding is absent/fails, or createProjectionLayer is missing. Native startup also disables the layers path when GLES multiview is unavailable. The old blanket claim that every stereo session requires multiview does not describe this fallback.
Fallback code existing is not a device-compatibility guarantee. Check actual stereo output on the target browser/device. DisableWebXRLayers selects the fallback path before startup; it is not a quality preset. This article verifies neither pixels, antialiasing nor headset performance.
Controller input
Standard-mapped controllers use these names; devices with a nonstandard mapping use generic button_0/axis_0 names instead. Not every input source provides every button or grip pose.
| Names | Value |
|---|---|
| trigger_click, grip_click | Boolean derived in current source from button pressure > 0; not a separate browser pressed flag. |
| trigger, grip | Button pressure reported by the browser. |
| touchpad_click, thumbstick_click, ax_button, by_button | Standard button booleans when the device provides them. |
| touchpad_x/y, thumbstick_x/y | Scalar axes; standard-mapped Y is inverted by the engine. |
| touchpad, thumbstick | Vector2 values, created when the corresponding axis pair exists. |
| default, aim | Aim transform for an active input source. |
| grip | Grip transform only when supplied by the source. |
Use input-source activity and current pose tracking data before interacting. A gaze or screen input is not automatically a physical hand controller. Do not assume an OpenXR action map is loaded or identical simply because some input names overlap.
Diagnosis
| Symptom | Check |
|---|---|
| No WebXR interface in the desktop editor | Expected platform distinction: the concrete interface is registered under WEB_ENABLED. |
| C# Web export fails | Current C# exporter target support, not XR scene geometry. |
| Enter button stays pending | Query mode, browser console, permission/policy and the support-query rejection limitation. |
| Initialize succeeds, then session fails | SessionFailed message: request rejection, WebGL compatibility or exhausted reference-space choices. |
| Wrong player height | Actual ReferenceSpaceType and origin; local is not a floor guarantee. |
| Missing grip or buttons | Input-source activity, mapping and device-provided data; avoid inventing unavailable controls. |
Verification scope
The actual engine JS bridge ran 16 checks in Node with explicit browser, session, GL and timer mocks. Only Emscripten's library-registration binding was adapted; function bodies were unchanged. Checks covered support-query resolution, immediate request dispatch, feature parsing, ordered reference-space fallback, layer-path selection, startup failures and JS cleanup.
This is not a browser export or headset test. Native callback integration, real permissions/user activation, WASM build, C# Web execution, pixels, controllers and performance remain unverified. C++ lifecycle, module/platform gates, C# export restrictions and input mapping were reviewed in current source. Engine Source, Runtime and SDK were unchanged.