Protocol reference
Esta página aún no está disponible en tu idioma.
This page is VXP as it appears on the pipe, for anyone writing a client in any language. The Rust SDK’s types serialize to exactly these bytes. Read Build an extension first for the model behind it.
The current version is VXP 1.7.
Transport
Section titled “Transport”vrOS creates one endpoint per extension before it launches the process, and puts three variables in its environment:
| Variable | Holds |
|---|---|
VROS_EXT_ENDPOINT |
The endpoint to connect to |
VROS_EXT_TOKEN |
A token for this launch only. Send it back in hello; never log it |
VROS_EXT_ID |
Your extension id |
On Windows the endpoint is the named pipe \\.\pipe\vros.ext.<id>. On Linux it
is the Unix socket ext-<id>.sock in $XDG_RUNTIME_DIR/vros/. Read the variable
rather than rebuilding the name. The pipe can appear a moment after your process
starts, so retry the connect for a few seconds.
Every frame is one JSON-RPC 2.0 object on one line, terminated by \n.
- Batch arrays are refused.
- A frame over 3 MiB is corruption, not traffic.
- Whitespace, NUL, and a byte-order mark around a line are trimmed, and empty lines are skipped.
- Request ids are unsigned integers.
- Unknown fields are ignored everywhere. That is what lets minors add them.
A request names a namespace as its method. Its params is that
namespace’s request object, tagged by op:
{"jsonrpc":"2.0","id":7,"method":"kv","params":{"op":"get","key":"count"}}The reply’s result is the namespace’s reply object, tagged by result:
{"jsonrpc":"2.0","id":7,"result":{"result":"value","value":3,"revision":12}}Events from the host are notifications — no id — with method set to
"event":
{"jsonrpc":"2.0","method":"event","params":{"event":"panel_shown","panel":"overlay"}}Errors
Section titled “Errors”There are two kinds, and they never overlap.
Domain errors ride inside a successful reply as that namespace’s err
variant, with a code and usually a message:
{"result":"err","code":"value_too_large","message":"…"}Protocol errors are the JSON-RPC error object. They mean the request never
reached a namespace:
| Code | Meaning |
|---|---|
-32700 |
The line is not JSON |
-32600 |
Not a valid request: a batch, a wrong jsonrpc, a frame with no method |
-32601 |
Unknown namespace |
-32602 |
params does not match the namespace’s request shape |
-32603 |
Internal |
-32001 |
capability_denied — the namespace exists, but it was not granted to you |
-32002 |
A request arrived before a valid hello. The host disconnects |
Replies, events, and codes can all grow in later minors. Treat an op, a
result, an event, or a code you do not recognize as something to skip, not
as a reason to stop.
The first frame on every connection, within five seconds of launch.
{"jsonrpc":"2.0","id":1,"method":"hello","params":{"id":"com.example.hello","vxp_version":"1.7","token":"…"}}A welcome carries the version the host speaks, what the user granted — the manifest’s requests narrowed by the user’s consent, in the manifest’s own shape — the user’s locale, and, when vrOS can resolve it, the user’s vrOS content folder:
{"result":"welcome","vxp_version":"1.7","granted":{"bus":["vr.lifecycle"],"overlays":false,"capture":false,"audio":false,"media":false,"osc":false,"network":false,"secrets":false},"locale":"en-US","content_root":"C:\\Users\\you\\Documents\\vrOS"}face_provide and face_read appear in granted only when they are true. A
missing grant stays missing for the whole connection.
A rejection is followed by a disconnect:
{"result":"rejected","code":"version_unsupported","message":"host speaks 1.7 (N and N-1 majors)"}| Code | Why |
|---|---|
bad_token |
The token does not match this launch |
wrong_id |
The id is not the one this endpoint was created for |
version_unsupported |
Your major is older than the host’s previous major, or newer than its current one |
already_connected |
This extension already has a live connection |
The minor you send matters: some events go only to clients that said a new enough minor. Send the version your code actually understands.
Your private key/value store. Last write wins, and every key has a revision that only goes up. No capability.
op |
Fields | Reply |
|---|---|---|
get |
key |
value with value and revision. An absent key is value: null, revision: 0 |
set |
key, value (any JSON) |
written with the new revision and over_quota |
delete |
key |
deleted, even if the key was already gone |
list |
— | keys with keys |
Keys are at most 256 bytes and values at most 64 KiB. The store has a soft quota
of 1 MiB per extension. Going over it sets over_quota and badges the extension
in the pane; nothing is ever deleted to enforce it.
A set on a key your manifest declares in [[settings]] must pass that
setting’s kind and bounds.
Errors: key_too_large, value_too_large, internal.
secrets
Section titled “secrets”Named slots in the OS credential store. Needs secrets.
op |
Fields | Reply |
|---|---|---|
set |
slot, value |
ok |
get |
slot |
value with value, or null for an empty slot |
delete |
slot |
ok |
exists |
slot |
exists with exists |
A value is at most 2048 bytes, and a longer one is refused, never truncated. Secrets never appear in KV, config, logs, or anything vrOS displays.
Errors: not_persistable (Linux, where there is no credential backend yet — ask
per session instead), too_large, internal.
op |
Fields | Reply |
|---|---|---|
publish |
topic, payload (any JSON) |
ok |
subscribe |
pattern |
ok |
unsubscribe |
pattern |
ok |
You may always publish to and subscribe to your own prefix, ext.<id>..
Anything else, including vr.lifecycle and other extensions’ topics, needs a
matching pattern in your manifest’s bus list. A pattern is a literal topic or a
prefix ending in .*: ext.cat.kittyn.twitch.* covers
ext.cat.kittyn.twitch.chat.message.
Messages arrive as bus events. Subscribing to vr.lifecycle delivers
vr_lifecycle events instead.
Errors: not_allowed, internal.
Your manifest’s panels need no define: the host reads and compiles them when
your extension starts, and their names are the role keys, overlay and
dash-tab.
op |
Fields | Reply |
|---|---|---|
props |
panel, props |
applied |
define |
panel, source, optional width, height, anchor |
defined with revision |
remove |
panel |
removed |
props is a list of [name, value] pairs, merged latest-wins. The host
coalesces them to about 30 updates a second per panel. A value is tagged by type:
t |
v |
Slint type |
|---|---|---|
bool |
true |
bool |
i64 |
21 |
int (exact up to 2⁵³) |
f64 |
0.5 |
float |
str |
"text" |
string |
str_list |
["a","b"] |
[string] |
rows |
[[{"t":"str","v":"row"},{"t":"i64","v":-7}]] |
a model of rows |
assets |
["e:icon:1"] |
[image], from keys you uploaded with assets |
{"op":"props","panel":"overlay","props":[["count",{"t":"i64","v":21}],["title",{"t":"str","v":"Hello"}]]}define creates or replaces a runtime overlay panel from a whole .slint
source. It needs overlays. The source is compiled before the reply, so a
compile error comes back as compile_failed with the compiler’s message.
width and height are logical pixels, defaulted and clamped by the host.
anchor (world or hmd) is a hint for where the panel first appears; once the
user moves it, their placement wins. Re-defining bumps the revision. Runtime
panels are overlays only, and remove cannot remove a manifest panel.
Panel names are [a-z0-9-], at most 64 bytes. A source is at most 256 KiB.
Errors: invalid_name, source_too_large, compile_failed, no_such_panel,
not_allowed, internal.
The keyboard-focus contract
Section titled “The keyboard-focus contract”A panel receives VR-keyboard and hardware keys only if its root component declares both of these:
callback field-focus(bool)— fire it on every text field’shas-focusedge. The host consumes it; it never reaches your extension.in property <int> field-blur— the host bumps it when focus moves elsewhere. Answer with achanged field-blurhandler that clears your fields’ focus.
Several fields can share the one callback; the host nets each frame’s edges, so moving between two of your fields reads as still focused. When you remove a focused field or change pages, report the loss yourself: a destroyed item cannot be relied on to run its handler.
For precise click-away, put an ExtFocusRegion from @vros/theme.slint inside
each text box, with width: parent.width; height: parent.height; focused: input.has-focus;. Panels without regions keep working; their whole panel counts
as the field.
The theme module
Section titled “The theme module”import { ExtTheme, ExtFocusRegion } from "@vros/theme.slint"; is the only
import a panel may use. The host fills it with the user’s live settings.
ExtTheme.cis the current palette:bg0,bg1,bg2,line,tx-hi,tx-mid,tx-low,accent,accent-text,ok,warn,err,focus,live,live-text.ExtTheme.currentis the theme id:signal,graphite,aura,meow,ember,cobalt,mono,volt,crimson,daylight,hc-dark, orhc-light. OLED mode is a separate flag,ExtTheme.oled, and does not changecurrent. Prefer the palette to checking ids.- Sizes follow the density:
space-1tospace-6,control-h,hit-min,radius-card,radius-control,border-default, and the type sizestype-title,type-body,type-data,type-label,type-numeral.is-desktopis true on the desktop app. reduced-motionis the user’s setting;motion-pressandmotion-expandalready drop to zero under it.
assets
Section titled “assets”Images your panels display. You fetch and decode them; vrOS never loads a URL for you. No capability.
op |
Fields | Reply |
|---|---|---|
put |
key, width, height, frames, loop |
ok |
remove |
key |
ok |
clear |
— | ok |
Each frame is {"rgba_base64": "…", "delay_ms": 100}: straight RGBA8, exactly
width × height × 4 bytes once decoded. loop is the total play count, and 0
repeats forever.
{"op":"put","key":"e:icon:1","width":1,"height":1,"frames":[{"rgba_base64":"/////w==","delay_ms":100}],"loop":0}| Limit | Value |
|---|---|
| Size | up to 128 × 128 |
| Frames | up to 60, each 20–1000 ms |
| One asset | 2 MiB decoded |
| Your table | 1024 assets or 32 MiB; the least recently used are evicted to make room |
| Animating at once | 24 per panel |
A key is 1–128 bytes of letters, digits, and - _ . : /. It names an image in
your own table and nowhere else. The table lives as long as your connection.
Animations run on the renderer’s clock while the panel is visible; do not drive
them with props.
Errors: too_large, too_many_frames, budget_full (the host is busy — retry),
bad_key, bad_pixels.
commands
Section titled “commands”Named commands the user can bind. No capability.
op |
Fields | Reply |
|---|---|---|
register |
name |
ok, also for a name already registered |
unregister |
name |
ok, also for a name that was not registered |
A registered command does nothing until the user assigns it to one of the
extension action slots and binds that slot in SteamVR. Then each press arrives as
a command_invoked event. Registrations end with the connection, so register
again after every hello. Names follow the panel-name rule.
Errors: invalid_name, internal.
settings
Section titled “settings”[[settings]] in your manifest declares a form. vrOS renders it in the
Extensions pane, even while your extension is stopped, and the values are
ordinary KV rows under the declared keys.
[[settings]]key = "send_target"kind = "text"label = "Send target"default = "127.0.0.1:9000"placeholder = "host:port"
[[settings]]key = "rate"kind = "number"label = "Updates per second"default = 10min = 1max = 60
[[settings]]key = "mode"kind = "select"label = "Mode"default = "auto"choices = [ { value = "auto", label = "Automatic" }, { value = "manual", label = "Manual" },]kind |
default |
Rules |
|---|---|---|
text |
a string of at most 1 KiB | placeholder allowed |
toggle |
true or false |
— |
number |
an integer within bounds | min and max required, min < max, both within ±1,000,000 |
select |
one of the choice values | 2–8 choices, each with a unique value |
At most 16 settings. Keys are [a-z0-9_-], at most 64 bytes; labels at most 200
bytes. A manifest that breaks these fails to load, with the reason on the pane.
op |
Fields | Reply |
|---|---|---|
get |
key |
value with key, value, is_default |
list |
— | values with entries, in manifest order |
The value is what is stored, or the default if nothing is. There is no settings
write on the wire: when the user edits a field you get a settings_changed
event, and your own writes are ordinary kv sets to the same keys.
Errors: no_such_key, no_schema, internal.
strings
Section titled “strings”Locale-resolved text from the catalogs built into vrOS.
op |
Fields | Reply |
|---|---|---|
get |
prefix, optional locale |
strings with the locale actually served and entries |
Only keys under ext. are served, and today that means the keys vrOS’s own
bundled extensions use. Your own keys are not in those catalogs, so expect
empty results for your prefix and ship your own translations. An unknown locale
serves en-US and says so. Re-fetch on locale_changed.
Errors: prefix_not_allowed, internal.
Needs audio. vrOS decodes a sound once, keeps it in memory, and plays it on its
named channels:
| Channel | Plays into |
|---|---|
soundboard |
The soundboard output |
mic |
The device that feeds the user’s microphone, such as a virtual cable. Nothing is captured |
ui |
Interface feedback; follows the system default output |
op |
Fields | Reply |
|---|---|---|
load |
path, normalize |
loading with the new sound id, then an audio_loaded event |
unload |
sound |
ok |
pre_arm |
channel |
ok — opens the channel’s output ahead of the first play |
play |
sound, targets, looping |
ok |
stop |
sound |
ok |
stop_all |
— | ok |
get_status |
— | status with playing |
list_channels |
— | channels |
list_devices |
— | devices with endpoints and default_endpoint |
path must be absolute, at most 256 MiB, and decode in under 30 seconds; the
welcome’s content_root is the user’s vrOS folder. load answers at once and
the decode finishes in the background. Play a sound only after its
audio_loaded event says loaded — playback never decodes.
{"op":"play","sound":"s-00000001","targets":[{"channel":"soundboard","gain_pct":80},{"channel":"mic","gain_pct":40}],"looping":false}Each target has its own gain, 0–100. Targets that cannot play are skipped, and
only if none of them could does play fail. Status rows list the targets that
actually accepted the sound. Playing a sound again replaces its earlier playback.
Errors: not_found, undecodable, too_long, cache_full, unknown_sound,
unknown_channel, sample_not_cached, no_valid_targets, no_endpoint,
internal.
Needs media. Plays a video or audio stream in a vrOS player. Windows only
today; elsewhere every request answers unsupported.
op |
Fields | Reply |
|---|---|---|
open |
key, url, title, and the options below |
opened with session and source_generation |
set |
session and any of the options except native_controls |
applied with source_generation |
reload |
session, url, optional title |
applied with the new source_generation |
close |
session |
closed |
status |
session |
status |
The options, with their defaults: max_fps (30), volume_pct (100), muted
(false), paused (false), show_video (true), native_controls (false), and
controls_on_player (false).
keyis your own name for the slot,[a-z0-9-], at most 64 bytes; one open session per key.urlis at most 8 KiB andtitleat most 256 bytes. Treat the URL as a credential: never log or store it.max_fpsis 1–120,volume_pct0–100.- vrOS runs four sessions at a time across all extensions; a fifth gets
too_many.
Hiding the video or changing a value keeps the stream and its audio running.
reload is the only thing that changes the source, and it bumps
source_generation.
Status comes as media_snapshot events: complete, coalesced, and delivered only
to clients that said hello with 1.4 or newer. Each session reports playback
(opening, buffering, playing, paused, ended, or failed with a code)
and, separately, video (disabled, warming, ready, recovering), so
healthy audio can sit beside recovering video.
With native_controls: true the player draws its own pause, volume, and stop
controls, and each finished gesture arrives as a media_control event carrying
an absolute action — set_paused, set_volume, or stop. Apply it with your
own set or close; check its source_generation against the session’s
current one first. controls_on_player puts those controls on the video rather
than below it.
Errors: invalid_url, invalid_key, invalid_value, unsupported,
too_many, no_such_session, busy, endpoint_unavailable, open_failed,
playback_failed, dimension_limit.
Device-neutral face tracking. A provider (face_provide) offers a source;
the user picks sources for eyes and mouth on the Face Tracking page; a
consumer (face_read) reads what was picked. Both grants are off until the
user turns them on for your extension, and either can be revoked while you run.
Face updates are delivered only to clients that said hello with 1.5 or newer.
op |
Fields | Reply |
|---|---|---|
register |
source |
registered with generation |
unregister |
source_id |
ok |
publish |
source_id, generation, sample |
ok |
subscribe |
eyes, mouth |
ok |
unsubscribe |
— | ok |
A source describes itself honestly:
{"op":"register","source":{"id":"sim","name":"Simulated eyes + mouth","device":"Demo","transport":"VXP","capabilities":{"combined_gaze":true,"eye_lids":true,"expressions":["jaw_open"]},"max_rate_hz":30}}The id is [a-z0-9-], at most 64 bytes; the three text fields at most 128 bytes;
max_rate_hz 1–240. capabilities can claim combined_gaze, per_eye_gaze,
eye_lids, and a list of mouth expressions from the fixed set of 37
(jaw_open, mouth_smile_left, tongue_out, …). An extension can register up
to 4 sources, and vrOS holds 16 in all.
Publish only when asked. Every face_update event lists demands: which of
your sources are wanted, for eyes, mouth, or both. Publish nothing otherwise —
the host answers not_demanded — and stop the moment the demand goes away.
A sample carries an increasing sequence, its age_ms since you acquired it
(under 250, or it is stale), and eyes, mouth, or both:
- Gaze directions are unit vectors in headset space: +X right, +Y up, −Z forward.
- Eyelid
closed_left/closed_rightrun 0 open to 1 closed. convergence_mis metres;confidenceis 0–1.- Mouth
weightsare{"expression": "jaw_open", "value": 0.5}, each 0–1.
Send only what you measured. A missing channel is unknown, never zero, and missing gaze never means closed eyes.
A consumer calls subscribe and reads face_update events. Each is a complete
snapshot, never a delta: the grants, the selected eyes and mouth with their
age, and the demands. Drain them continuously, and add your own delivery delay
to age_ms before trusting a sample.
Errors: not_granted, invalid_source, invalid_sample, too_many,
no_such_source, stale_generation, out_of_order, not_demanded,
unsupported.
Events
Section titled “Events”event |
Fields | When |
|---|---|---|
shutting_down |
— | vrOS is stopping you. Flush and exit 0 |
vr_lifecycle |
state: detached, attached, quitting |
The VR runtime came or went, if you subscribed to vr.lifecycle |
bus |
topic, payload |
A message on a topic you subscribed to |
panel_shown |
panel |
The panel became visible anywhere: overlay, Dash tab, or desktop |
panel_hidden |
panel |
…and stopped being visible anywhere. Always paired with panel_shown |
panel_event |
panel, callback, args |
A panel callback fired. args are typed values, as in props |
command_invoked |
command |
The user triggered one of your commands |
settings_changed |
key, value |
The user edited a setting. The value already passed validation |
locale_changed |
locale |
The UI language changed. Re-fetch strings and re-push your words |
audio_loaded |
sound, outcome |
A load finished: loaded with duration and levels, or failed with a code |
media_snapshot |
granted, sessions |
Media status changed (1.4+) |
media_control |
key, session, source_generation, action |
A player gesture on a session with native controls (1.6+) |
face_update |
state |
Face grants, selections, or demands changed, or a new sample arrived (1.5+) |
Version history
Section titled “Version history”| VXP | Added |
|---|---|
| 1.0 | The handshake, kv, secrets, bus, panel, commands, and the lifecycle, bus, panel, and command events |
| 1.1 | settings and strings, [[settings]] in the manifest, and the settings_changed and locale_changed events |
| 1.2 | audio, the audio_loaded event, and content_root in the welcome |
| 1.3 | assets and the assets prop type, the anchor hint on define, and number and select settings |
| 1.4 | media, and the media_snapshot event |
| 1.5 | face, the face_provide and face_read capabilities, and the face_update event |
| 1.6 | Native player controls: native_controls, the media_control event, and title on reload |
| 1.7 | controls_on_player on open and set |
A manifest can also grow keys that an older host refuses. The quickstart lists which.