LibretroWebXR — Headset Smoke Test
The manual VR checks that can't be scripted headlessly: the raycast menus, that the Quest exposes one gamepad per controller, in-VR voice (mic grant mid-XR), and two-headset networked multiplayer (presence, voice, one shared game streamed from a stable host, held-object ghosts, host-authoritative input, host migration). Tap a step to mark it done — progress is saved on this device.
Open these
Single headset (Part A):
https://dionysus.dk/webxr/libretrowebxr2/ ↩ open the app
Multi-core rack spike (Part C — perf gate):
https://dionysus.dk/webxr/libretrowebxr2/?rack=3 ↩ 3 live cores
Flat-screen build — no VR/room, same cores and netplay;
handy for a spotter at a PC joining the tester's room:
https://dionysus.dk/webxr/libretrowebxr2/desktop.html ↩ desktop build
Two headsets (Part B) — both open the same room,
different names. Order matters now: open Alice first, let her
room finish loading, then open Bob. The first peer in is the host for the
whole session.
https://dionysus.dk/webxr/libretrowebxr2/?session=ROOM&nick=Alice ← open FIRST (host)
https://dionysus.dk/webxr/libretrowebxr2/?session=ROOM&nick=Bob ← open SECOND (watcher)
Tip: also join the desktop build above as a third peer
(same ?session=ROOM). It is the only screen that spells the role out in
words — its MP badge reads ● Hosting · N watching or
● Watching host — and it makes the migration step in Part B testable.
Optional: &color=%23ff8866 picks your avatar colour.
For a cross-network test add &turn=turn:dionysus.dk:3478&turnUser=USER&turnCred=PASS
(needs the coturn server provisioned first).
Controls (quick reference)
- Grip (side squeeze) — grab / release a cartridge, the gamepad, or (in edit mode) a prop. Never sent to the game.
- Thumbstick — walk, when you're not holding the gamepad.
- Trigger on the wall Menu panel (left wall) — click a menu button. Drop the gamepad first, point, and pull.
- Hold the gamepad in one hand to play: trigger / face buttons / thumbstick = game input, per the wall Controls card.
Part A — Single headset
- Enter VR. Open the single-headset URL, tap Enter VR. The 3D room renders — shelves, a console, a CRT TV, wall panels.
- Boot a game. Grip a cartridge off a shelf and drop it onto the console. It boots on the TV. If pixels appear, COOP/COEP + SharedArrayBuffer + the WASM core all work.
- Play it — controller gamepad. Grip the gamepad, hold it, and play: thumbstick = D-pad, trigger/face buttons act per the Controls card. Confirms the Quest exposes one live
inputSource.gamepadper held controller. - Menu raycast — modes. Drop the gamepad. Point at the wall Menu panel and pull the trigger to click. Toggle Show Controls and Show Debug.
- Move mode. Menu → Move. Grip a shelf or poster, drag it somewhere new, release. It stays put.
- Change mode. Menu → Change. Grip-select a poster, then Cycle Selected → its art changes. Grip-select a shelf, Cycle Selected → its game collection swaps. Try Wallpaper / Floor / Lighting — the room repaints live.
- Add mode. Menu → Add. Add a Shelf, Console, Poster, and Portal — each spawns in front of you and is immediately grabbable.
- Portal + Export. Walk into the portal you added → it navigates to another room. Back in the menu, Export Room downloads / copies the room JSON.
Part C — Multi-core rack spike PERF GATE
This is the one measurement that decides the whole multi-console plan: can a standalone Quest run several libretro cores live at once? It auto-boots — you don't grab anything, you just enter VR and let it run while I read the framerate telemetry from the logs. (Headless desktop already passed: 3 cores boot, render, and input routes to the right core. Only Quest framerate is left.)
- Open the rack URL. Open
…/libretrowebxr2/?rack=3and tap Enter VR. Three CRT screens appear in a row in front of you, each running a different system (NES, Game Boy, SNES) at the same time. If all three show moving/idle game pictures, multi-instance cores work on Quest. - Let it run ~30 seconds. Just look around — turn your head, step side to side. The app ships framerate samples automatically every few seconds.
- Judge the smoothness. Does it feel smooth (no stutter/judder), marginal (occasional hitches), or bad (clearly choppy / nauseating)? Note which. I'll cross-check your impression against the measured
rack-perffps in the logs. - Try 2 cores too (optional). Re-open with
?rack=2and compare — if 3 is rough but 2 is smooth, that tells us the ceiling.
Note: in this spike the 3 screens are a perf probe only — they're not yet wired to the controllers or the cord/patch system. That's the next phase, and it's gated on this result.
Part B — Two headsets M0 M1.4
One room = one game = one host. The server picks the host: whoever joined first, and they keep it. Everyone else is display-only — they never boot a core, their TV is a live video of the host's screen, and their button presses travel to the host's game. So "Bob started his own game" is now a bug, not a feature. Both headsets on the same Wi-Fi (STUN covers that; TURN is only for the cross-network case below).
- Join in order. M1.4 Alice opens the room URL and waits for her room to load. Then Bob opens his. Both tap Enter VR. Alice is the host from now on — nothing Bob does can take the role.
- Presence. M0 Each of you sees the other as an avatar — floating head + two hands + a nameplate. Move your head and hands; the other sees it track.
- Confirm the roles. M1.4 There is no "you are the host" badge inside VR. Read it on a flat screen instead — three ways, any one is enough:
• the desktop peer's MP badge spells it out:● Hosting · 1 watchingvs● Watching host;
• on Bob's own page (take the headset off, or before you tap Enter VR) the header status line readsWatching Alice — …, and his title staysLibretroWebXR · idle— no core name;
• Alice's page never says "watching" anything. Alice = host, Bob = watcher is the precondition for every step below. If it came out backwards, both of you leave the room and redo step 1 in order. - Room + shelf inherited. M1.4 Bob is standing in Alice's room, looking at Alice's shelf — same layout, same posters, same games — even though Bob has no local ROMs of his own. Games only Alice owns still appear on Bob's shelf. Also check both in-VR: Menu → Multiplayer → the status line reads
ROOM (2p)on both headsets. - Host boots — shared TV + audio. M1.2 Alice grips a cart → console. Alice's TV boots it; Bob's TV shows Alice's actual frames (a live video of her screen, not a second copy), and Bob hears Alice's game audio too.
- Now Playing panel — the in-VR readout. NEW Look at the small Now Playing panel under Bob's TV: it names Alice's system, core and title. That is how you confirm from inside the headset that Bob is mirroring the room's game.
- Held-object ghost. M0 One player grips a cartridge and holds it. The other sees a translucent ghost cartridge in that player's hand.
- In-VR voice + mic grant. NEW On each headset: Menu → Voice: Off. Approve the Quest microphone prompt. The label flips to Voice: On. Talk — you hear each other, and the voice is spatial (quieter as you walk apart, panned by direction). Click Voice again → Voice: Muted. The key new check: does the Quest browser grant the mic during an XR session?
- Watcher input drives the host's game. M1.1 Bob holds his gamepad and presses buttons → Alice's game reacts. Both TVs show the same reaction at the same time (Bob is player 2).
- Watcher's cart insert is FORWARDED, not booted. M1.4 Bob grips a different cart off the shelf and drops it on his console. Correct: the game changes on Alice's screen — and therefore on both TVs, because Bob is still just watching her. Also correct: nothing changes, because Alice doesn't have that ROM (her refusal is a message on Bob's flat status line). Report as a BUG only if Bob's TV ends up showing a different game from Alice's — that is the old broken behaviour.
- Watcher's "Load ROM" is refused. M1.4 Bob leaves VR for a moment, presses the header's Load ROM and picks any local file. Correct: it is refused — "Only the room host picks the game" — and Bob's TV keeps showing Alice's game. Nothing of Bob's boots.
- Watcher can't spawn a console. M1.4 Bob, in VR: Menu → Add → Spawn Console. Correct: no new console appears with a game running in it (the request is refused because it would start a second core). Alice doing the same does work.
- Host uses "Load ROM" — watchers must follow. M1.4d Alice (not Bob) presses the header's Load ROM and picks a game for a different system than the cart she booted. Within a second or two: Bob's TV switches to the new game and Bob's Now Playing panel names it. This used to fail — the watcher froze on the previous game forever. If it fails again, say so loudly; it is a regression, not a new discovery.
- Watcher asks for a peripheral. OPTIONAL Bob grabs the light gun (or mouse). Correct: Alice's core reboots with the device attached and re-streams — Bob's picture comes back after her reload, and Bob's aim then drives Alice's game. Bob's own machine never boots anything.
- Host migration — Alice LEAVES. M1.4 Alice closes her tab (really quit — don't just take the headset off). Expect a ~15 second gap where Bob's picture freezes or goes blank: that pause is deliberate (a host who is only reloading gets to reclaim the role). Then Bob is promoted, boots the room's game himself, and his TV goes live off his own core. If the desktop peer is in the room, it keeps watching — now off Bob. Note the delay you actually saw; much longer than ~15 s is worth reporting.
- Host reload reclaims. OPTIONAL Rejoin with Alice first again. This time Alice reloads her page and is back within 15 s. Correct: Alice is still the host, Bob never gets promoted, and there is never a moment where both are running a game.
Two things that look broken and are NOT NOT A BUG
1. The other player's face is planted in your TV. Both peers currently spawn at the same spot, so on a watcher the remote avatar's head plane hangs right at your camera and hides most of the screen — it reads exactly like "the shared picture is broken". Step back a metre or two (or sideways) and look again before judging the stream. The picture behind it is correct; spawning joiners at an offset is a known to-do.
2. A watcher's console can still read as "live". The in-app rack /
console status can say a console is live on a display-only watcher even though
no core is running — the label only means "not paused", and a watcher's
console is coreless. It is a diagnostic wording quirk, not a double-boot.
How to tell a real double-boot apart: a real one shows a
different picture from the host's on the watcher's TV, and the watcher's
Now Playing panel names a different game. Same picture + same title = healthy, whatever
the "live" label says. (For a spotter at a PC console: __testApi.rack.running()
is the trustworthy check — every entry must be running:false on a watcher —
not __rack.live().)
Optional / advanced
- Cross-network (TURN). OPTIONAL Put the two headsets on different networks (e.g. one on a phone hotspot). Without TURN, voice/video may fail to connect (symmetric NAT). Re-open both with the
&turn=…&turnUser=…&turnCred=…params → they should connect via the relay. Requires the coturn server to be set up first (seedeploy/coturn.conf.example). - Local ROM folder. OPTIONAL Does the “ROM folder…” button appear and work in the Quest browser (File System Access API)? Known unknown — note what happens.
What to report back
For each failed step: which one, what you saw vs expected. Also note the
headset model + browser version. The current open questions:
(1) the Part C rack smoothness at ?rack=3 and ?rack=2
— the perf gate for the multi-console plan; (2) whether the mic grant worked
mid-XR (the Voice step); and (3) Part B's two never-tested-on-hardware steps —
the host's Load ROM sync and the host migration delay.
Read the NOT a bug box before filing anything from Part B. A screen recording of
any glitch helps a lot.