iwsdk-quest-device-testing
Skill1 starsChanged 30 days agoMITView on GitHub
Tests an IWSDK WebXR app on a physical Meta Quest in Quest Browser: developer mode and adb checks, the dev server over Wi-Fi or USB (`adb reverse`) and its certificate, the production build over HTTPS for benchmarks, getting results off the headset, remote DevTools via chrome://inspect, metavr logs and screenshots, the real frame rate, OVR Metrics, a headset-only checklist, and Meta VR Glasses prep on a Quest 3/3S (head-gaze preview, `metavr device fov-sim`, hands-only run). Use when the user says "test on my Quest", "open it in the headset", "Enter VR button missing", "works in the emulator but not on the Quest", "test the production build on the Quest", "save results from the headset", "kicked out of VR when I save", "inspect Quest Browser", "remote debug", "adb reverse", "headset logs", "what FPS is it running at", "test for Meta VR Glasses without the device", "simulate the glasses field of view", or before any store build. For scripted control of a real session, hand off to `iwsdk-native-xr-test`.
- Reads credentials
- Sends data out
In this repository58 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- cf-cliSkill
- durable-objectsSkill
- game-gdd-authorSkill
- game-playtestSkill
- pmndrs-handleSkill
- pmndrs-pointer-eventsSkill
- pmndrs-uikitSkill
- prototype-fastSkill
- pwa-offlineSkill
- review-game-trailerSkill
- scenario-blender-expertSkill
- scenario-blender-retopologySkill
- scenario-blender-uv-bakingSkill
- threejs-perf-loadingSkill
- webapp-testingSkill
- workers-best-practicesSkill
- wranglerSkill
- xr-perceptual-constraints-displays-and-trackingSkill
- xr-platforms-audio-and-comfortSkill
- xr-referenceSkill
- xr-rendering-input-and-spatial-understandingSkill
- xr-ux-design-and-performance-budgetingSkill
- horizon-launch-roadmapSkill
- horizon-store-listing-assetsSkill
- horizon-web-store-publishSkill
- horizon-web-vrc-checklistSkill
- iwsdk-asset-pipelineSkill
- iwsdk-dev-workflowSkill
- iwsdk-performanceSkill
- iwsdk-project-setupSkill
- iwsdk-testingSkill
- iwsdk-vr-glassesSkill
- quest-browser-webxrSkill
- iwsdk-build-modelSkill
- iwsdk-compose-sceneSkill
- iwsdk-debugSkill
- iwsdk-depth-occlusionSkill
- iwsdk-devSkill
- iwsdk-grabSkill
- iwsdk-hostingSkill
- iwsdk-migrate-0-5Skill
- iwsdk-native-xr-testSkill
- iwsdk-physicsSkill
- iwsdk-pwa-packagingSkill
- iwsdk-raySkill
- iwsdk-uiSkill
- hz-immersive-designerSkill
- hz-iwsdk-webxrSkill
- hz-new-project-creationSkill
- hz-perfetto-debugSkill
- hz-quest-verify-firstSkill
- hz-simpleperf-debugSkill
- hz-store-pwaSkill
- hz-store-submitSkill
- hz-vr-debugSkill
- metavr-cliSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
16 rules. About 0 are common, 7 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 43
Ask before installing Android platform tools, OVR Metrics Tool, or anything on the headset.
- Distinctive: line 44
Keep the dev server on the local network or loopback.
- Very specific: line 46
Remove adb reverse routes when done.
- Distinctive: line 54
Check from the computer:
- Very specific: line 59
Start the app: npx @iwsdk/cli dev up (or npm run dev) and read the URLs:
- Very specific: line 75
Use the port dev status reports (scaffold default 8081).
- Distinctive: line 91
Use Console for errors and your debug logs, Network for 404s, CORS failures and asset sizes, Performance for CPU traces (DevTools adds …
- Very specific: line 124
After any launch, check metavr adb shell "pidof <pkg>", or look for ForegroundAppHandler: Changing in focus immersive app ...
- Very specific: line 129
Add a dev-only probe after World.create(...) resolves (remove it before release).
- Very specific: line 166
Don't assume a default refresh rate: Meta's frame-rate page only gives older-device defaults.
- Very specific: line 167
Read frameRate, and see iwsdk-performance before calling updateTargetFrameRate.
- Distinctive: line 195
Use a port the dev server doesn't use, and accept the certificate warning once more (new port).
- Very specific: line 243
Add results/ to .gitignore.
- Distinctive: line 289
If the view doesn't change, take the headset off and on, or sleep/wake it, or relaunch the page.
- Distinctive: line 293
Hands only: Put the controllers down (the session switches to hands) and complete every required flow: entering XR, menus, dialogs, …
- Very specific: line 304
Don't follow world.features.fieldOfViewMask from Meta's hosted IWSDK guide: IWSDK 1.0.1 rejects the key …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Testing an IWSDK app on a Meta Quest
Made by Navid Mirzaaghazadeh · part of iwsdk-webxr-skills · MIT
The emulator (IWER) proves the code paths. Only the headset proves input feel, comfort, text legibility, real frame rate, thermals and mixed-reality features. Do this early and after every feature, not just before release.
Related: scripted control of a real session → iwsdk-native-xr-test (official);
frame-time problems → iwsdk-performance; Quest CLI details → docs/metavr-cli-reference.md
in this library and the metavr-cli skill if installed.
Ground rules
- Work with one explicit device serial. If
adb deviceslists several, ask which one and pass-s <serial>to everyadbcommand. - Ask before installing Android platform tools, OVR Metrics Tool, or anything on the headset.
- Keep the dev server on the local network or loopback. Never expose it with a public tunnel without asking (it serves your source and, with AI tooling, a command bridge).
- Remove
adb reverseroutes when done. Never automate permission dialogs in the headset; ask the person wearing it.
0. Headset ready?
- Developer mode is on: Meta Horizon mobile app → headset icon → your headset → Headset Settings → Developer Mode (Meta's device setup page). Connect a USB-C data cable, put the headset on, and choose Always allow from this computer at the USB debugging prompt.
- Check from the computer:
adb devices # want: <serial> device (not unauthorized/offline) npm exec metavr -- device list # same via the project's metavr dev dependency - Start the app:
npx @iwsdk/cli dev up(ornpm run dev) and read the URLs:npx @iwsdk/cli dev status # data.runtimeUrls.local and data.runtimeUrls.network[]
1. Open the app on the headset
Pick one route. IWER is not injected in Quest Browser (userAgentException defaults to
/OculusBrowser/), so the headset always uses its native WebXR.
| Route | Steps | When |
| A. Same Wi-Fi (IWSDK's recommended method) | Open a data.runtimeUrls.network URL, e.g. https://192.168.1.20:8081/, in Quest Browser | Home networks |
B. USB + adb reverse | adb -s <serial> reverse tcp:8081 tcp:8081, then open https://localhost:8081/ | Corporate/guest Wi-Fi, client isolation, firewalls |
| C. Chrome port forwarding | chrome://inspect/#devices → Port forwarding… → add the port | Same as B, from Chrome's UI |
- Use the port
dev statusreports (scaffold default 8081). - Open the URL without typing in the headset:
npm exec metavr -- device browser "https://localhost:8081/". - The dev certificate is self-signed: accept the warning once per host. That's expected.
- Choose Enter XR. The scaffold sets
world.xr.offer: "always": IWSDK callsnavigator.xr.offerSession(...)after start-up and again after each session ends, so Quest Browser shows its own enter button; an app may also add its own. - Clean up route B:
adb -s <serial> reverse --remove tcp:8081.
2. Remote DevTools
USB (ADB) connection required.
- Open the page in Quest Browser.
- On the computer, open
chrome://inspect/#devicesin desktop Chrome. - Find the headset and the Quest Browser tab → inspect.
Use Console for errors and your debug logs, Network for 404s, CORS failures and asset sizes, Performance for CPU traces (DevTools adds overhead; compare runs under the same conditions). Check the console first: type errors can stop a system from initialising without any visible symptom.
3. Logs and captures from the device
npm exec metavr -- log -n 200 -l W # recent device log, warnings and up
npm exec metavr -- capture screenshot -o artifacts/quest.png
adb -s <serial> logcat -d | tail -200 # raw logcat, if metavr isn't installedmetavr log is a shortcut for adb logcat with tag and level filters. Whether Quest Browser
mirrors page console.* output into logcat is unverified; rely on chrome://inspect for page
logs. metavr options: npm exec metavr -- <command> --help.
Recording logcat for a whole session (metavr 1.8, observed 2026-10-25 on a Quest 3S):
- Piping
metavr adb logcat -f ...ormetavr adb shell "logcat -T 1 ..."into a file on the computer wrote nothing, even after minutes. - What worked was letting the headset write the file:
metavr adb shell "nohup logcat -v time -f /sdcard/Download/session.log -r 8192 -n 2 VrApi:I AndroidRuntime:E '*:S' > /dev/null 2>&1 &", thenmetavr adb pull /sdcard/Download/session.logwhen needed. The tested run also had aUnity:Vtag; add the tags your app logs under.VrApigives theFPS=lines. metavr adb -- logcat ...is rejected ("unexpected argument"); writemetavr adb logcatormetavr adb shell "logcat ...".
A launch that never happened (installed TWA or any APK; metavr 1.8, observed 2026-10-25):
metavr app launch <pkg> --cold-start --verifyprinted "Launched" and "no crash signal", yetpidof <pkg>was empty. Horizon OS had held the launch back. Logcat saidLaunch is blocked because: a Reprojected OS dialog is currently showing. Caching launch, thenInterstitial session (APP_RESUME) took more than 15sandappLaunchFailureBroadcastReceived.- The app starts once the person in the headset closes the dialog.
- After any launch, check
metavr adb shell "pidof <pkg>", or look forForegroundAppHandler: Changing in focus immersive app ... to (<pkg>).
4. Read the real session facts
Add a dev-only probe after World.create(...) resolves (remove it before release). frameRate,
supportedFrameRates and updateTargetFrameRate are WebXR APIs that Quest Browser 16.4+
supports; they may be missing from TypeScript's types, hence the cast.
import { VisibilityState } from '@iwsdk/core';
type RateSession = XRSession & {
frameRate?: number;
supportedFrameRates?: Float32Array;
updateTargetFrameRate?: (rate: number) => Promise<void>;
};
if (import.meta.env.DEV) {
world.visibilityState.subscribe((state) => {
if (state !== VisibilityState.Visible) return;
const s = world.session as RateSession | undefined;
console.log('[xr] mode/features', s?.enabledFeatures,
'frameRate', s?.frameRate,
'supported', s?.supportedFrameRates && Array.from(s.supportedFrameRates));
});
let sum = 0;
let frames = 0;
world.onXRFrame((_frame, delta) => { // delta is seconds
sum += delta;
if (++frames === 300) {
console.log('[xr] avg frame', ((sum / frames) * 1000).toFixed(2), 'ms',
'draw calls', world.renderer.info.render.calls,
'triangles', world.renderer.info.render.triangles);
sum = 0;
frames = 0;
}
});
}Don't assume a default refresh rate: Meta's frame-rate page only gives older-device defaults.
Read frameRate, and see iwsdk-performance before calling updateTargetFrameRate.
4a. Test the production build
Frame-rate and load-time numbers should come from the production build: the dev server serves
unbundled modules, the HMR client and emulator code. vite preview can serve dist/ over
HTTPS with the certificate iwsdkDev() already made (.iwsdk/https/_cert.pem holds both the
key and the certificate):
// vite.config.ts
import { existsSync, readFileSync } from 'node:fs';
const devCert = '.iwsdk/https/_cert.pem'; // written by iwsdkDev() on first dev run
const previewHttps = existsSync(devCert)
? { key: readFileSync(devCert), cert: readFileSync(devCert) }
: undefined;
export default defineConfig({
// ...existing config
preview: { host: '0.0.0.0', port: 8083, https: previewHttps },
});npm run build && npx vite preview # then open https://<LAN-IP>:8083/ in Quest BrowserUse a port the dev server doesn't use, and accept the certificate warning once more (new port). Checked with IWSDK 1.0.1 on 2026-10-16 from the computer (the HTTPS page and the §4b endpoint both answer); opening it in Quest Browser is unverified.
4b. Get results off the headset without DevTools
A Quest page can't write files, and copying from the remote console is slow. A small Vite plugin
gives the page a same-origin endpoint that saves JSON (benchmark summaries, per-second perf rows,
tuning exports) into the repo, on both the dev server and vite preview:
// vite-plugins/results-sink.ts
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import type { Connect, Plugin } from 'vite';
export function resultsSink(root: string): Plugin {
const dir = join(root, 'results');
const handler: Connect.NextHandleFunction = (req, res) => {
const kind = (req.url ?? '').replace(/^\/+/, '').split('?')[0];
if (req.method !== 'POST' || !/^[a-z0-9-]{1,40}$/.test(kind)) {
res.statusCode = 400;
return res.end('expected POST /__results/<kind>');
}
const chunks: Buffer[] = [];
let size = 0;
req.on('data', (c: Buffer) => { size += c.length; if (size <= 5 * 1024 * 1024) chunks.push(c); });
req.on('end', () => {
try {
if (size > 5 * 1024 * 1024) throw new Error('payload too large');
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
mkdirSync(dir, { recursive: true });
const file = join(dir, `${kind}-${new Date().toISOString().replace(/[:.]/g, '-')}.json`);
writeFileSync(file, `${JSON.stringify(body, null, 2)}\n`);
res.end(JSON.stringify({ saved: file }));
} catch (err) { res.statusCode = 400; res.end(String(err)); }
});
};
return {
name: 'results-sink',
configureServer: (server) => { server.middlewares.use('/__results/', handler); },
configurePreviewServer: (server) => { server.middlewares.use('/__results/', handler); },
};
}
// vite.config.ts: plugins: [iwsdkDev(), resultsSink(process.cwd())]
// in the app: fetch('/__results/bench', { method: 'POST', body: JSON.stringify(summary) })Add results/ to .gitignore. The endpoint exists only on the dev and preview servers (a
static host has none), and it accepts writes from anyone who can reach the port, so keep those
servers on the LAN.
5. Performance HUD
OVR Metrics Tool shows FPS and CPU/GPU frame times as an in-headset overlay. Install it from
Meta Quest Developer Hub or sideload it (Meta's WebXR performance workflow page), with the user's
permission. Record: target vs. actual FPS, GPU frame time, CPU frame time, in the heaviest scene
of the app, after 5+ minutes (thermals). Deeper tracing (Perfetto via
npm exec metavr -- perf capture) is covered by Meta's hz-perfetto-debug skill if installed;
whether it attributes Quest Browser WebXR frames usefully is unverified.
6. What only the headset proves
- Session starts from the app's button and from Quest Browser's own prompt
- Controllers: trigger/select, squeeze/grab, thumbsticks, A/B/X/Y, menu
- Hand tracking (requires
world.xr.features.handTrackinginiwsdk.config.json) - Locomotion comfort: teleport, snap/smooth turn, vignette; no player falling through floors
- Text and UI readable at the real viewing distance; panels not head-locked
- Holds the target frame rate in the heaviest scene; no stutter on asset loads
- Mixed reality: passthrough, room setup present, planes/meshes/anchors/hit-test/depth as requested
- Audio: spatial direction and volume
- Exit XR and re-enter: state resets correctly; ScreenSpace UI comes back
- Headset sleep / wake (proximity sensor) and system menu: app pauses on
VisibleBlurred
7. Preparing for Meta VR Glasses on a Quest
The glasses ship in spring 2027; Meta's advice until then is to test on a Quest 3 or 3S. Their
users aim with their eyes, pinch to select, often have no controllers, and see 70° × 66°. App
changes (config, gaze UI, controller-free flows) are in iwsdk-vr-glasses; this is the
headset routine.
- Head-directed gaze preview. With
world.xr.features.gazeTrackingset, add"dev": { "targetDevicePreview": { "gazeSimulation": "head" } }toiwsdk.config.json(IWSDK 1.0.1; dev server only, needsdev.emulator.iwerleft on). The native Quest Browser session keeps its viewer, hands, controllers and frame timing, plus a test gaze source that follows the head. The console's[iwsdk][gaze]lines show whether gaze was granted, missing, invalid or head-mapped. It proves plumbing, not eye-tracking accuracy. - Field-of-view simulation (ask first: it changes a headset setting until disabled or
rebooted). Needs a developer-mode Quest 3, 3S or Pro:
If the view doesn't change, take the headset off and on, or sleep/wake it, or relaunch the page. Check the boundary through the lenses: casts and screenshots only show the crop on Horizon OS v209+, and before v209 the simulation doesn't clip compositor layers (WebXR quad/cylinder layers).npm exec metavr -- device fov-sim enable # nominal 70° × 66° npm exec metavr -- device fov-sim enable --perceived # p90 perceived 58° × 54° npm exec metavr -- device fov-sim disable # always restore afterwards - Hands only. Put the controllers down (the session switches to hands) and complete every required flow: entering XR, menus, dialogs, scrolling, grabbing, locomotion, pause and exit. Then pick the controllers up mid-flow and put them down again: focus, held objects and open dialogs must stay valid (Meta's VR Glasses test procedure).
- Layout. In the simulated view, check the first view after launch and after a recenter, menus, captions, error messages, and targets near each edge. Note anything clipped or reachable only by an unexpected head turn.
- Sustained run. Heaviest scene for the content length or 45 minutes (§5), so the result can be compared once VR Glasses hardware (with its separate glasses and puck thermals) is available.
Don't follow world.features.fieldOfViewMask from Meta's hosted IWSDK guide: IWSDK 1.0.1
rejects the key (docs/meta-reference/web-iwsdk-docs-on-meta.md).
Troubleshooting
| Symptom | Check |
No Enter XR button / isSessionSupported false | Page not on HTTPS or localhost; world.xr disabled (--target browser projects have xr: false); a required XR feature the device lacks |
localhost refused on the headset | adb reverse missing, wrong serial, or wrong port; check adb -s <serial> reverse --list |
| Network URL times out | Different Wi-Fi, client isolation, or firewall on 8081 → route B |
| Certificate page every time | Host changed (new IP); accept again, or use route B for a stable localhost |
| IWER toolbar shows on the headset | dev.emulator.activation set to always, a custom userAgentException, or injectOnBuild: true |
| Works in IWER, black screen in headset | Read the console via chrome://inspect; then a full page reload (HMR can leave a stale session) |
| Dropped out of VR whenever a file is saved | Expected in dev: an edit reloads the page, which ends the XR session (headset and emulator). Batch edits, warn whoever wears the headset, re-enter XR; play-test on vite preview (§4a) |
| Assets missing only on device | Network tab: 404 from a hard-coded /path (use import.meta.env.BASE_URL), or CORS on a CDN |
device unauthorized | Re-plug and accept the USB debugging prompt in the headset |
Dev server rejects iwsdk.config.json after following Meta's VR Glasses page (unknown property "fieldOfViewMask") | That key isn't in IWSDK 1.0.1; remove it and use §7 (also suppressWhenHandPointerActive → suppressWhenDirectPointerActive) |
| Headset's field of view stays narrow | metavr device fov-sim disable, then take the headset off and on (a reboot also clears it) |
metavr app launch says "Launched" but nothing opens | An OS dialog is showing in the headset (logcat: Launch is blocked because). Ask the wearer to close it; confirm with pidof <pkg> (§3) |
Sources
- https://iwsdk.dev/guides/02-testing-experience.html (Wi-Fi and ADB methods, certificate, IWER activation)
- https://developers.meta.com/horizon/documentation/web/browser-remote-debugging/ (adb devices, adb reverse, chrome://inspect)
- https://developers.meta.com/horizon/documentation/web/webxr-frames/ (frameRate, supportedFrameRates, updateTargetFrameRate, Browser 16.4+)
- https://developers.meta.com/horizon/documentation/web/webxr-perf-workflow/ (OVR Metrics Tool)
- https://developers.meta.com/horizon/documentation/native/android/mobile-device-setup/ (developer mode; Meta's device setup page)
- https://github.com/facebook/immersive-web-sdk (1.0.1):
packages/vite-plugin-dev/src/types.ts(emulator defaults),packages/core/src/ecs/world.ts(session,visibilityState,onXRFrame),packages/cli/src/runtime-state.ts(runtimeUrls) docs/metavr-cli-reference.mdin this library (metavr device browser,log,capture screenshot,perf capture)- npm
@iwsdk/vite-plugin-dev@1.0.1dist/index.js(certificate cached in.iwsdk/https, written as_cert.pem) - https://vite.dev/config/preview-options (
preview.https,preview.host,preview.port), https://vite.dev/guide/api-plugin (configureServer,configurePreviewServer) - Production preview, results endpoint and the reload-ends-XR behaviour: an IWSDK 1.0.1 VR prototype, 2026-10-16
- Meta VR Glasses: https://developers.meta.com/horizon/essentials/field-of-view/ (2026-09-25;
fov-sim, v209 notes), /test-your-app/ (2026-09-25), /get-started-with-glasses/ (2026-10-13); IWSDK 1.0.1docs/concepts/xr-input/gaze.md(targetDevicePreview), npm@iwsdk/core1.0.1dist/project/validation.js, npm@iwsdk/vite-plugin-dev1.0.1dist/index.js(targetDevicePreviewrequires IWER) - Session logcat recording and the blocked launch: metavr 1.8.0.17.10 on a Quest 3S with a native APK, 2026-10-25
Last verified: 2026-10-25 (§3 logcat and launch notes); 2026-10-17 (the rest)
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

