Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
48a0456
ci: Exercise Quick Start workflow on GPU runner
arussell-nvidia Jul 14, 2026
b256827
ci: avoid Quick Start E2E port collisions
arussell-nvidia Jul 14, 2026
7eb0b3f
fix: keep WSS proxy backend on CloudXR runtime port
arussell-nvidia Jul 14, 2026
85be58c
fix: stream quick start CloudXR readiness logs
arussell-nvidia Jul 14, 2026
fd0afdf
fix: reuse sourced CloudXR runtime
arussell-nvidia Jul 18, 2026
6678c06
fix: restore USB backend default port
arussell-nvidia Jul 14, 2026
ff8db36
test: add Quick Start client validation probe
arussell-nvidia Jul 18, 2026
c62ba43
test: use hosted desktop client for Quick Start probe
arussell-nvidia Jul 18, 2026
934bdb2
Route OOB client config into hosted validation
arussell-nvidia Jul 16, 2026
e6cb4b2
Enable software WebGL for Quick Start client probe
arussell-nvidia Jul 16, 2026
ad9957f
Start Quick Start client with CDP user gesture
arussell-nvidia Jul 16, 2026
7a9291a
Autostart hosted Quick Start client validation
arussell-nvidia Jul 16, 2026
160f79b
Retry hosted Quick Start client autostart
arussell-nvidia Jul 16, 2026
ad1d5cb
Use minimal XR features for hosted client validation
arussell-nvidia Jul 16, 2026
f815e17
Use local reference space for hosted client validation
arussell-nvidia Jul 16, 2026
8cd72a5
Force IWER for hosted client validation
arussell-nvidia Jul 16, 2026
88fe376
Recreate XR store after IWER loads
arussell-nvidia Jul 16, 2026
ffbc322
Preload IWER before client app bootstrap
arussell-nvidia Jul 16, 2026
8bb3752
Harden hosted IWER session diagnostics
arussell-nvidia Jul 16, 2026
ef34e22
ci: add Quick Start Isaac Lab validation hook
arussell-nvidia Jul 18, 2026
12706a0
style: format Quick Start launcher tests
arussell-nvidia Jul 18, 2026
fecbfb2
test: align launcher WSS error wording
arussell-nvidia Jul 18, 2026
c912e25
ci: limit Ubuntu workflow token permissions
arussell-nvidia Jul 18, 2026
e191a53
Merge remote-tracking branch 'origin/main' into codex/cxr-3577-quicks…
arussell-nvidia Jul 21, 2026
74169b9
Merge current main into Quick Start validation
arussell-nvidia Jul 22, 2026
23eef1d
Fix Quick Start review findings
arussell-nvidia Jul 22, 2026
069bb07
Format Quick Start launcher tests
arussell-nvidia Jul 22, 2026
b20959f
Isolate CloudXR deps from Isaac Lab
arussell-nvidia Jul 28, 2026
d322006
Split Isaac Lab validation into follow-up
arussell-nvidia Jul 28, 2026
019d832
Merge current main into Quick Start validation
arussell-nvidia Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .github/workflows/build-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

env:
# Shared apt build dependencies. Each job installs these plus its own extras
# (ccache, clang-format, the OAK camera autotools chain).
Expand Down Expand Up @@ -626,6 +629,105 @@ jobs:
# Run tests with CloudXR runtime
./scripts/run_tests_with_cloudxr.sh --python-version ${{ matrix.python_version }}

test-quick-start-e2e:
# Runs the documented Quick Start workflow against the candidate wheel on a
# GPU runner: install package, launch CloudXR, source the generated env, run
# the documented gripper retargeting example, and upload the logs/summary.
runs-on: [self-hosted, linux, gpu, x64]
needs: build-ubuntu
timeout-minutes: 30

steps:
- name: Checkout code
uses: actions/checkout@v6
with:
submodules: recursive

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'

- name: Download isaacteleop wheel (x64, py3.11)
uses: actions/download-artifact@v7
with:
name: isaacteleop-wheels-x64-py3.11
path: quick-start-wheels

- name: Run Quick Start E2E workflow
env:
CI: true
QUICK_START_E2E_REQUIRE_WHEEL: "1"
QUICK_START_E2E_WHEEL_DIR: quick-start-wheels
QUICK_START_E2E_ARTIFACT_DIR: ${{ runner.temp }}/isaacteleop-quick-start-e2e
run: ./scripts/run_quick_start_e2e.sh

- name: Upload Quick Start E2E artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v6
with:
name: quick-start-e2e-x64-py3.11
path: |
${{ runner.temp }}/isaacteleop-quick-start-e2e/*.log
${{ runner.temp }}/isaacteleop-quick-start-e2e/*.json
${{ runner.temp }}/isaacteleop-quick-start-e2e/cloudxr/run/cloudxr.env
${{ runner.temp }}/isaacteleop-quick-start-e2e/cloudxr/logs/**/*.log
if-no-files-found: warn
retention-days: 7

test-quick-start-client-e2e:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
# Follow-up Quick Start validation: launch the locally hosted desktop/IWER
# client in a Chromium browser, click CONNECT, and require server-side OOB
# state to report a connected streaming client.
runs-on: [self-hosted, linux, gpu, x64]
needs: build-ubuntu
timeout-minutes: 35

steps:
- name: Checkout code
uses: actions/checkout@v6
with:
submodules: recursive

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'

- name: Download isaacteleop wheel (x64, py3.11)
uses: actions/download-artifact@v7
with:
name: isaacteleop-wheels-x64-py3.11
path: quick-start-wheels

- name: Build hosted desktop web client
uses: ./.github/actions/build-cloudxr-web-client
with:
ngc-api-key: ${{ secrets.NGC_TELEOP_CORE_GITHUB_SERVICE_KEY }}

- name: Run Quick Start client E2E workflow
env:
CI: true
QUICK_START_E2E_REQUIRE_WHEEL: "1"
QUICK_START_E2E_VALIDATE_CLIENT: "1"
QUICK_START_E2E_WHEEL_DIR: quick-start-wheels
QUICK_START_E2E_ARTIFACT_DIR: ${{ runner.temp }}/isaacteleop-quick-start-client-e2e
TELEOP_WEB_CLIENT_STATIC_DIR: ${{ github.workspace }}/deps/cloudxr/webxr_client/build
run: ./scripts/run_quick_start_e2e.sh

- name: Upload Quick Start client E2E artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v6
with:
name: quick-start-client-e2e-x64-py3.11
path: |
${{ runner.temp }}/isaacteleop-quick-start-client-e2e/*.log
${{ runner.temp }}/isaacteleop-quick-start-client-e2e/*.json
${{ runner.temp }}/isaacteleop-quick-start-client-e2e/cloudxr/run/cloudxr.env
${{ runner.temp }}/isaacteleop-quick-start-client-e2e/cloudxr/logs/**/*.log
if-no-files-found: warn
retention-days: 7

test-teleop-ros2:
runs-on: [self-hosted, linux, gpu, "${{ matrix.arch }}"]
needs: build-ubuntu
Expand Down Expand Up @@ -935,6 +1037,8 @@ jobs:
- test-viz-gpu
- test-viz-sanitizers
- test-cloudxr
- test-quick-start-e2e
- test-quick-start-client-e2e
- test-teleop-ros2
if: ${{ always() }}

Expand Down
202 changes: 128 additions & 74 deletions deps/cloudxr/webxr_client/helpers/LoadIWER.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,95 +23,149 @@ declare global {
}
}

const IWER_version = '2.2.1';
const IWER_DEVUI_version = '2.2.0';

export interface IWERLoadResult {
supportsImmersive: boolean;
iwerLoaded: boolean;
}

export async function loadIWERIfNeeded(): Promise<IWERLoadResult> {
async function installBundledIWER(): Promise<boolean> {
try {
const IWERModule = await import('iwer');
const IWERGlobal = (IWERModule as any).default ?? IWERModule;
if (window.xrDevice) {
ensureRuntimeInstalled(window.xrDevice, IWERGlobal);
installNavigatorXRShim(window.xrDevice, IWERGlobal);
installMakeXRCompatibleShim();
return true;
}

const XRDeviceCtor = IWERGlobal.XRDevice;
const deviceProfile = IWERGlobal.metaQuest3;
if (!XRDeviceCtor || !deviceProfile) {
console.warn('Bundled IWER module is missing XRDevice or metaQuest3.');
return false;
}

const device: XRDevice = new XRDeviceCtor(deviceProfile);
ensureRuntimeInstalled(device, IWERGlobal);
installNavigatorXRShim(device, IWERGlobal);
installMakeXRCompatibleShim();
window.xrDevice = device;
const supportsVr = await (navigator.xr as XRSystem | undefined)
?.isSessionSupported?.('immersive-vr')
.catch(() => false);
console.info('Bundled IWER runtime installed.', {
navigatorXR: navigator.xr?.constructor?.name ?? '',
requestSession: String((navigator.xr as XRSystem | undefined)?.requestSession).slice(
0,
80
),
supportsImmersiveVr: Boolean(supportsVr),
supportedSessionModes: readSupportedSessionModes(device, IWERGlobal),
});
return true;
} catch (e) {
console.warn('IWER runtime install failed:', e);
return false;
}
}

function ensureRuntimeInstalled(device: XRDevice, IWERGlobal: any): void {
const privateDevice = readPrivateDevice(device, IWERGlobal);
if (privateDevice?.xrSystem && navigator.xr === privateDevice.xrSystem) {
return;
}

(device as any).installRuntime?.({ forceInstall: true });
}

function readPrivateDevice(device: XRDevice, IWERGlobal: any): any {
try {
return IWERGlobal.P_DEVICE ? (device as any)[IWERGlobal.P_DEVICE] : undefined;
} catch (_) {
return undefined;
}
}

function readSupportedSessionModes(device: XRDevice, IWERGlobal: any): string[] {
try {
const privateDevice = readPrivateDevice(device, IWERGlobal);
const modes = (device as any).supportedSessionModes ?? privateDevice?.supportedSessionModes;
return Array.isArray(modes) ? modes : [];
} catch (_) {
return [];
}
}

function installNavigatorXRShim(device: XRDevice, IWERGlobal: any): void {
const xrSystem = (() => {
try {
const privateDevice = readPrivateDevice(device, IWERGlobal);
return privateDevice?.xrSystem ?? (navigator.xr as XRSystem | undefined);
} catch (_) {
return navigator.xr as XRSystem | undefined;
}
})();
if (!xrSystem) {
return;
}

const defineXR = (target: object | undefined) => {
if (!target) {
return;
}
try {
Object.defineProperty(target, 'xr', {
value: xrSystem,
configurable: true,
});
} catch (_) {}
};

defineXR(window.navigator);
defineXR(Object.getPrototypeOf(window.navigator));
}

function installMakeXRCompatibleShim(): void {
const install = (ctor: unknown) => {
const prototype = (ctor as { prototype?: Record<string, unknown> } | undefined)?.prototype;
if (!prototype) {
return;
}
Object.defineProperty(prototype, 'makeXRCompatible', {
value: () => Promise.resolve(),
configurable: true,
});
};

install((window as any).WebGLRenderingContext);
install((window as any).WebGL2RenderingContext);
}

export async function loadIWERIfNeeded(forceIWER = false): Promise<IWERLoadResult> {
if (forceIWER && window.xrDevice) {
return { supportsImmersive: true, iwerLoaded: true };
}

let supportsImmersive = false;
let iwerLoaded = false;

if ('xr' in navigator) {
if (!forceIWER && 'xr' in navigator) {
try {
const vr = await (navigator.xr as XRSystem).isSessionSupported?.('immersive-vr');
const ar = await (navigator.xr as XRSystem).isSessionSupported?.('immersive-ar');
supportsImmersive = Boolean(vr || ar);
} catch (_) {}
}

if (!supportsImmersive) {
console.info('Immersive mode not supported, loading IWER as fallback.');

// Load IWER first
const script = document.createElement('script');
script.src = `https://unpkg.com/iwer@${IWER_version}/build/iwer.min.js`;
script.async = true;
script.integrity = 'sha384-3G2UIBh0RX9Imd3PFwcHyXbqRYAeQo9FDMgQTOLcflo9H6LDHaxADB24vKC3b+OY';
script.crossOrigin = 'anonymous';

await new Promise<void>(resolve => {
script.onload = async () => {
console.info('IWER loaded as fallback.');
const IWERGlobal = (window as any).IWER || (globalThis as any).IWER;
if (!IWERGlobal) {
console.warn('IWER global not found after script load.');
supportsImmersive = false;
resolve();
return;
}

// Load iwer-devui after IWER
const devUIScript = document.createElement('script');
devUIScript.src = `https://unpkg.com/@iwer/devui@${IWER_DEVUI_version}/build/iwer-devui.min.js`;
devUIScript.async = true;
devUIScript.integrity =
'sha384-gPhqycVT+bNyiNIH8kMEWFjaysw6xH9NGYwuduRzK71Ro0Tp3hXByxqAI9sWrc9T';
devUIScript.crossOrigin = 'anonymous';

await new Promise<void>(devUIResolve => {
devUIScript.onload = () => {
console.info('IWER DevUI loaded.');
devUIResolve();
};
devUIScript.onerror = error => {
console.warn('Failed to load IWER DevUI:', error);
devUIResolve();
};
document.head.appendChild(devUIScript);
});

try {
const device: XRDevice = new IWERGlobal.XRDevice(IWERGlobal.metaQuest3);

const IWER_DevUI = (window as any).IWER_DevUI || (globalThis as any).IWER_DevUI;
if (IWER_DevUI?.DevUI) {
device.installDevUI(IWER_DevUI.DevUI);
console.info('IWER DevUI initialized with XR device.');
} else {
console.warn('IWER DevUI not found after script load, continuing without DevUI.');
}

await device.installRuntime();
window.xrDevice = device;
supportsImmersive = true;
iwerLoaded = true;
} catch (e) {
console.warn('IWER runtime install failed:', e);
supportsImmersive = false;
}
resolve();
};
script.onerror = () => {
console.warn('Failed to load IWER.');
supportsImmersive = false;
resolve();
};
document.head.appendChild(script);
});
if (forceIWER || !supportsImmersive) {
console.info(
forceIWER
? 'Installing bundled IWER for automated headless validation.'
: 'Immersive mode not supported, installing bundled IWER fallback.'
);
iwerLoaded = await installBundledIWER();
supportsImmersive = iwerLoaded;
}

return { supportsImmersive, iwerLoaded };
Expand Down
Loading
Loading