Skip to content

Migration Guide for PR 3220

halx99 edited this page Jul 6, 2026 · 2 revisions

Migration Guide: OpenXR VR Scene Composition

This PR replaces the old generic VR renderer hook with a scene-compositor based architecture and adds an optional OpenXR runtime path. Existing non-VR projects should keep working without changes. Projects using the previous VR renderer or custom scene renderer APIs need the updates below.

1. Enable OpenXR only when needed

OpenXR is opt-in:

-DAX_ENABLE_VR=ON
-DAX_ENABLE_OPENXR=ON

AX_ENABLE_OPENXR depends on AX_ENABLE_VR and is not available for iOS or WebAssembly. When enabled, the build fetches and links the OpenXR loader from the Khronos OpenXR-SDK dependency.

If you use Vulkan for OpenXR, call registerVulkanInterop() in applicationWillLaunch() before any RHI driver or render view is created:

void AppDelegate::applicationWillLaunch()
{
#ifdef AX_ENABLE_OPENXR
    registerVulkanInterop();
#endif
}

registerVulkanInterop() injects an OpenXRVulkanInterop into GraphicsCore through the VulkanInterop abstract interface. The Vulkan backend queries the OpenXR runtime for required instance/device extensions and selects the runtime-compatible physical device through this interface, without any OpenXR headers in the core RHI.

2. Replace VRGenericRenderer

The old VRGenericRenderer class has been removed. Use one of the new compositors:

  • VRPreviewSceneCompositor: stereo preview/debug rendering without an XR runtime.
  • VRSceneCompositor: product VR rendering through OpenXR.

Before:

#include "axmol/vr/VRGenericRenderer.h"

auto vrRenderer = std::make_unique<ax::VRGenericRenderer>();
Director::getInstance()->setSceneRenderer(std::move(vrRenderer));

After:

#include "axmol/vr/VRPreviewSceneCompositor.h"
#ifdef AX_ENABLE_OPENXR
#include "axmol/vr/VRSceneCompositor.h"
#endif

auto director = ax::Director::getInstance();
auto renderView = director->getRenderView();

#ifdef AX_ENABLE_OPENXR
renderView->setSceneCompositor(std::make_unique<ax::VRSceneCompositor>());
#else
renderView->setSceneCompositor(std::make_unique<ax::VRPreviewSceneCompositor>());
#endif

Pass nullptr to setSceneCompositor() to restore the default compositor:

renderView->setSceneCompositor(nullptr);

3. Move custom scene renderers to SceneCompositor

SceneRenderer has been renamed and reframed as SceneCompositor.

If you had a custom subclass:

  • Replace #include "axmol/scene/SceneRenderer.h" with #include "axmol/scene/SceneCompositor.h".
  • Derive from ax::SceneCompositor.
  • Install it on the RenderViewCore, not on Director.
  • Override pollEvents() if your compositor must poll runtime events before scheduler update.
  • Keep overriding renderScene(), onRenderViewChanged(), setScissorRect(), and getScissorRect() as needed.

Before:

class MyRenderer : public ax::SceneRenderer
{
    void renderScene(ax::Renderer* renderer, ax::Scene* scene) override;
};

Director::getInstance()->setSceneRenderer(std::make_unique<MyRenderer>());

After:

class MyCompositor : public ax::SceneCompositor
{
    void renderScene(ax::Renderer* renderer, ax::Scene* scene) override;
};

Director::getInstance()->getRenderView()->setSceneCompositor(std::make_unique<MyCompositor>());

Code that queried Director::getSceneRenderer() should query the render view instead:

auto compositor = Director::getInstance()->getRenderView()->getSceneCompositor();

4. Update pointer and 3D hit-testing code

Pointer events can now carry a controller ray and a hit result:

if (event->hasRay())
{
    const auto& ray = event->getRay().value();
}

if (event->hasHitResult())
{
    Vec3 worldPoint = event->getHitResult().worldPoint;
}

For custom nodes, override Node::onPointerHitTest() and write world-space hit points to outHitPoint:

bool MyNode::onPointerHitTest(ax::PointerEvent* event,
                              const ax::Camera* camera,
                              ax::Vec3* outHitPoint)
{
    if (event->hasRay())
    {
        // Ray-based 3D/controller picking.
    }
    else
    {
        // Traditional 2D pointer picking.
    }
    return hit;
}

Terrain and UI widgets already support this path. If your old gameplay code manually rebuilt a camera ray in pointer callbacks, prefer the dispatcher-provided hit result when listening on the target node.

Before:

auto ray = camera->screenToRay(event->getScreenLocation());
Vec3 hit;
if (terrain->getIntersectionPoint(ray, hit))
{
    movePlayerTo(hit);
}

After:

if (event->hasHitResult())
{
    movePlayerTo(event->getHitResult().worldPoint);
}

5. Listen for XR controller input

Use XRInputEventListener for controller buttons, axes, and poses:

auto listener = ax::XRInputEventListener::create();

listener->onButton = [](ax::XRInputEvent* event) {
    if (event->getInput() == ax::XRInputEvent::Input::Trigger &&
        event->getPhase() == ax::XRInputEvent::Phase::Pressed)
    {
        // Trigger pressed.
    }
};

listener->onAxis = [](ax::XRInputEvent* event) {
    auto axis = event->getAxis();
};

listener->onPose = [](ax::XRInputEvent* event) {
    if (event->hasAimRay())
    {
        const auto& ray = event->getAimRay().value();
    }
};

Director::getInstance()->getEventDispatcher()->addEventListenerWithFixedPriority(listener, 1);

Existing pointer listeners can still receive controller-ray interactions when the OpenXR compositor maps aim poses and trigger/thumbstick input into PointerEvent.

6. Backend and platform notes

7. Rename DriverContext to GraphicsCore

DriverContext has been renamed to GraphicsCore. The class now manages the VulkanInterop* interface instead of vendor-specific OpenXR compatibility flags. If your code references DriverContext directly, replace it with GraphicsCore.

  • OpenXR runtime rendering currently targets D3D11, D3D12, Vulkan, and Android OpenGL ES bindings where supported by the runtime.
  • Win32 only: This PR is validated on Windows desktop only. Android is not tested and not guaranteed to work.
  • Desktop OpenGL and Windows ANGLE GLES are not valid OpenXR graphics backends in this implementation.
  • If the HMD/runtime is unavailable, the OpenXR path logs a warning and falls back to normal scene rendering.
  • OpenXR APIs live in ax::experimental.
  • Custom RHI backends must implement createTextureFromNativeHandle() and submitCurrentFrameCommands() to support runtime-owned swapchain images.

Clone this wiki locally