What Is the XRServer and How Does WebXR/OpenXR Integration Work in Godot?

The XRServer is a global singleton in Godot Engine that manages all XR (AR/VR) functionality, acting as a central registry for XRInterface implementations like OpenXR and WebXR, while handling tracker data, world scaling, and frame lifecycle coordination.

The XRServer serves as the backbone of Godot’s XR architecture within the godotengine/godot repository, abstracting platform-specific runtimes into a unified API. Whether targeting native headsets via OpenXR or browser-based VR through WebXR, the XRServer orchestrates interfaces, trackers, and rendering hooks to deliver consistent spatial computing capabilities across platforms.

XRServer Architecture and Core Responsibilities

Singleton Pattern and Global Access

The XRServer operates as a thread-safe singleton accessible via XRServer::get_singleton(). This global object exposes the entire XR subsystem to the engine, eliminating the need for manual instantiation while ensuring centralized state management.

Interface Registry and Primary Selection

Concrete XR implementations derive from the abstract XRInterface class defined in servers/xr/xr_interface.h. The server maintains these interfaces in Vector<Ref<XRInterface>> interfaces and designates one as the primary interface via set_primary_interface(). Only the primary interface supplies view-projection matrices and eye textures to the renderer.

Tracker Management System

Spatial data flows through XRPositionalTracker objects representing headsets, controllers, hands, and body tracking. The server registers trackers via add_tracker() and stores them in a Dictionary trackers keyed by name. Each frame, XRServer::_process() updates these trackers with fresh pose data from the active interface.

World Scale and Origin Control

Godot’s XR coordinate system supports dynamic scaling through world_scale and spatial reorientation via world_origin. The server propagates these values to the rendering thread using _set_render_world_scale() and related helpers, ensuring consistent spatial mapping between physics and visual representations.

Frame Lifecycle Coordination

The XRServer drives the per-frame pipeline through three critical hooks:

  • _process() – Updates tracker poses and interface state
  • pre_render() – Allows the interface to submit frame data
  • end_frame() – Finalizes the frame and signals completion

WebXR Integration via WebXRInterfaceJS

The WebXR backend, implemented in modules/webxr/webxr_interface_js.h and modules/webxr/webxr_interface_js.cpp, bridges Godot to browser-based VR/AR capabilities.

Browser Session Initialization

WebXRInterfaceJS::initialize() initiates an XR session through Emscripten-generated JavaScript callbacks. The method registers input sources and allocates color and depth textures that the WebXR compositor populates each frame. Session modes—immersive-vr, immersive-ar, or inline—map to Godot’s configuration via set_session_mode().

Input Source Handling and Textures

The interface maintains an array of InputSource structs (input_sources[16]) storing controller state. JavaScript events trigger _update_input_source(), which synchronizes button presses, axes, and pose data with Godot’s tracker system. Rendering utilizes get_color_texture() and get_depth_texture() to retrieve WebGL textures supplied by the browser’s XR compositor.

Per-Frame Processing Pipeline

Each frame, WebXRInterfaceJS::process() retrieves the latest head pose from the browser, updates the head tracker via XRPositionalTracker, and synchronizes all active input source trackers. This data flows through XRServer to the rendering system, which draws the stereo views into the WebXR-provided textures.

OpenXR Integration Architecture

The OpenXR backend in modules/openxr/ provides native support for SteamVR, Oculus, Windows Mixed Reality, and other compliant runtimes.

Session and Action Set Management

OpenXRInterface::initialize() loads an OpenXRActionMap resource through _load_action_map(), creating OpenXR action sets, actions, and interaction profiles. These map Godot’s input system to hardware-specific controls. The method then calls openxr_api->initialize_session() to establish the runtime connection.

Tracker Creation and Hand Tracking

The interface creates runtime trackers via openxr_api->tracker_create() and wraps them in XRControllerTracker objects registered with XRServer. Hand tracking support, implemented in modules/openxr/extensions/openxr_hand_tracking_extension.h, exposes finger joint data as additional trackers when the runtime supports the XR_EXT_hand_tracking extension.

Rendering and Swap-Chain Handling

OpenXRInterface manages swap-chain images through OpenXRAPI. Methods get_color_texture() and get_depth_texture() return texture RIDs corresponding to the runtime’s swap-chain. pre_draw_viewport() and post_draw_viewport() handle synchronization between Godot’s rendering and the OpenXR compositor, supporting Vulkan, OpenGL, and Metal backends.

Thread Safety and Render State

Because OpenXR requires thread-aware operation, OpenXRInterface copies pose data into a per-frame RenderState structure. It pushes these updates to the rendering thread via RenderingServer::call_on_render_thread(), preventing race conditions between the game loop and GPU submission.

Practical Implementation in GDScript

The following example demonstrates initializing XR for both OpenXR and WebXR, accessing tracker data, and configuring environment blend modes:


# Enable XR (works for both OpenXR and WebXR)

var xr_server = XRServer.get_singleton()
var xr_interface = xr_server.find_interface("OpenXR")   # Use "WebXR" for web builds

if xr_interface and not xr_interface.is_initialized():
    xr_interface.initialize()               # starts the XR session

    xr_server.set_primary_interface(xr_interface)  # make it the active renderer

# Get the headset transform each frame (e.g. in _process())

func _process(delta):
    var head_tracker = xr_server.get_tracker("head")
    if head_tracker:
        var hmd_transform = head_tracker.get_pose("default")   # Returns Transform3D

        # Use hmd_transform to drive a camera, AR/VR avatar, etc.

        $XRCamera3D.global_transform = hmd_transform

# Query supported play-area mode and switch to room-scale if possible

if xr_interface.get_supported_environment_blend_modes().has(XRInterface.XR_ENV_BLEND_MODE_ALPHA_BLEND):
    xr_interface.set_environment_blend_mode(XRInterface.XR_ENV_BLEND_MODE_ALPHA_BLEND)

# Example for WebXR: start an immersive-VR session from a UI button

func _on_StartVR_pressed():
    var webxr = xr_server.find_interface("WebXR")
    if webxr:
        webxr.set_session_mode("immersive-vr")
        webxr.initialize()

Summary

  • XRServer is the global singleton in servers/xr/xr_server.h that centralizes all XR operations, managing interfaces, trackers, and world-space transformations.
  • XRInterface implementations like WebXRInterfaceJS and OpenXRInterface abstract platform-specific runtimes into a common API registered with the XRServer.
  • WebXR integration uses Emscripten-generated JavaScript callbacks to bridge browser-based VR/AR sessions, storing controller state in InputSource arrays and utilizing WebGL textures for rendering.
  • OpenXR integration provides native support through OpenXRAPI, handling action sets, swap-chain management, and hand-tracking extensions while maintaining thread safety via RenderState structures.
  • Tracker objects (XRPositionalTracker, XRControllerTracker) registered via add_tracker() provide unified access to head, controller, and hand data regardless of the underlying runtime.

Frequently Asked Questions

What is the difference between XRServer and XRInterface?

XRServer is the global singleton that manages all XR functionality across the engine, while XRInterface is an abstract base class that specific implementations (OpenXR, WebXR) inherit from. The server maintains a list of interfaces and designates one as primary to handle rendering, but the interface itself contains the platform-specific logic for session management and hardware communication.

How does Godot handle controller tracking for both OpenXR and WebXR?

Both implementations create XRControllerTracker objects registered with XRServer via add_tracker(). WebXR maps browser input sources to an internal InputSource array, updating tracker poses through _update_input_source() when JavaScript events fire. OpenXR creates trackers via openxr_api->tracker_create() and updates them each frame through handle_tracker(), reading pose data directly from the OpenXR runtime's action system.

Can I use multiple XR interfaces simultaneously?

Godot's architecture supports multiple registered interfaces, but only one primary interface can be active for rendering at any given time, set via set_primary_interface(). You can query available interfaces with find_interface() and switch between them (for example, falling back from OpenXR to WebXR), but simultaneous rendering to multiple XR runtimes is not supported by the current architecture.

Where are the XRServer source files located in the Godot repository?

The core XRServer implementation resides in servers/xr/xr_server.h and servers/xr/xr_server.cpp. The abstract XRInterface base class is defined in servers/xr/xr_interface.h. Platform-specific implementations are located in modules/webxr/ for browser-based XR and modules/openxr/ for native OpenXR support, with the latter containing openxr_interface.h/cpp and the low-level openxr_api.h/cpp wrapper.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →