Skip to main content
Version: 0.9.0 (next)

Onboarding and Sessions

Language:

In QAROS you never "create" or "join" a session directly — every session is obtained through onboarding. This page covers the four situations an integrated application encounters and the teardown rules. The security machinery behind these calls is described in Security and Onboarding; from the API's perspective it is completely hidden.

The canonical startup flow​

Persist exactly one thing: the onboarding ID (a 16-byte UUID) returned by your first successful onboard. Then, on every start:

saved id? ──yes──> qar_runtime_rejoin ──ok──> session ✓
│ │
no SESSION_NOT_FOUND
│ │
└────────> show code-entry UI ──> qar_runtime_onboard
│
persist new id ──> session ✓
static QarResult obtain_session(
QarRuntime* runtime,
QarOnboardingId* io_saved_id, /* loaded from your app's settings */
const char* code_from_user, /* NULL until you prompted the user */
QarSession** out_session)
{
QarPeerPresentation who = qar_peer_presentation_default();
who.display_name = "My CAD Tool";
who.app_version = "2.4.0";

if (not qar_onboarding_id_is_zero(io_saved_id))
{
QarRejoinInit rejoin = qar_rejoin_init_default();
rejoin.onboarding_id = *io_saved_id;
rejoin.presentation = who;

QarResult r = qar_runtime_rejoin(
runtime, &rejoin, NULL, NULL, NULL, out_session);
if (qar_result_is_success(r))
return r; /* silent reconnect — the normal path */
if (not qar_result_has_code(r, QAR_STATUS_ONBOARDING_SESSION_NOT_FOUND))
return r; /* real error (hub unreachable, ...) */
/* no persisted slot -> fall through to onboarding */
}

QarOnboardInit onboard = qar_onboard_init_default();
QarOnboardCodeExt onboard_code = qar_onboard_code_ext_default();
onboard.presentation = who;
onboard_code.code = code_from_user; /* the short code shown by the hub */
onboard.header.next = &onboard_code.header;

QarResult r = qar_runtime_onboard(
runtime, &onboard, NULL, NULL, NULL, io_saved_id, out_session);
if (qar_result_has_code(r, QAR_STATUS_PAKE_ERROR))
{
/* wrong or expired code — re-prompt the user and call again */
}
/* on success: persist *io_saved_id before returning */
return r;
}

Key points:

  • qar_runtime_rejoin pops no UI and is silent — call it optimistically on every start.
  • QAR_STATUS_ONBOARDING_SESSION_NOT_FOUND is expected control flow: it means "this identity has no persisted slot here — onboard instead".
  • QAR_STATUS_PAKE_ERROR means the code was wrong or expired (codes live ~10 seconds) — just re-prompt.
  • With QarOnboardCodeExt alone, qar_runtime_onboard finds your own Hub. A Hub has no fixed onboarding port (it claims one from its port range), so the lookup goes:
    1. The hub-endpoint.json file the Hub writes into the QAROS data folder on start. The data folder is per Windows user, so this finds the Hub of the same user on this PC, with no network.
    2. Otherwise a discovery beacon from a Hub run by the same user on this PC, listening up to 6 s.
    3. Otherwise it fails with QAR_STATUS_ONBOARDING_HUB_UNREACHABLE, asking for the Hub's host and port.
  • To reach a specific Hub, chain a QarOnboardHostExt after the code extension with both hostname and port (see API Conventions). The port is shown with the Hub's pairing code; port = 0 is rejected with QAR_STATUS_ARGUMENT_NOT_SUPPORTED.
  • Both calls have _async variants whose result callback delivers the same (onboarding_id, session) pair exactly once — use those to keep your UI thread responsive, and pass a QarCancelToken so the user can abort.

Sibling onboarding: one app, several API instances​

If your application hosts a second QAROS API instance — a helper process, a plugin sandbox, or a C# binding living next to native code — that instance must not pair with a code again. Instead, the already-onboarded instance mints an invite and hands it over.

The complete pattern is:

  1. Instance A starts with the normal rejoin-or-onboard flow and gets a QarSession*.
  2. Instance A calls qar_session_request_onboarding_invite.
  3. Instance A serializes the returned QarOnboardingInvite and sends the bytes over your IPC boundary.
  4. Instance B creates its own QarRuntime with its own storage root.
  5. Instance B deserializes the invite and calls qar_runtime_onboard with QarOnboardInviteExt.
  6. Instance B persists its own returned QarOnboardingId for future rejoins.

The primary instance side looks like this:

/* In the onboarded instance: */
QarRequestInviteInit req = qar_request_invite_init_default();
QarOnboardingInvite* invite = NULL;
qar_session_request_onboarding_invite(session, &req, NULL, NULL, NULL, &invite);

size_t size = 0;
qar_onboarding_invite_serialized_size(invite, &size);
uint8_t* blob = malloc(size);
size_t written = 0;
qar_onboarding_invite_serialize(invite, blob, size, &written);
/* ship `blob` over your IPC of choice, then: */
qar_onboarding_invite_handle_destroy(invite);
/* In the sibling instance: */
QarOnboardingInvite* invite = NULL;
qar_onboarding_invite_deserialize(blob, blob_size, &invite);

QarOnboardInit init = qar_onboard_init_default();
QarOnboardInviteExt invite_mode = qar_onboard_invite_ext_default();
init.presentation = who;
invite_mode.invite = invite;
init.header.next = &invite_mode.header;

QarOnboardingId my_id = qar_onboarding_id_default();
QarSession* session = NULL;
qar_runtime_onboard(
runtime, &init, NULL, NULL, NULL, &my_id, &session);
qar_onboarding_invite_handle_destroy(invite); /* caller keeps ownership */

Two constraints matter here:

  • Give each runtime instance its own storage_folder_path or its own persisted onboarding-id file. The tutorial example uses onboarding_and_rejoin.primary-state and onboarding_and_rejoin.sibling-state.
  • Invites expire (qar_onboarding_invite_get_expires_unix) — mint them on demand, move them immediately, and do not stockpile them.

The compiled example in this repo now demonstrates the full two-instance path end to end:

  • Source: qaros/qar-streaming-c/examples/onboarding_and_rejoin.c
  • Example walkthrough: Onboarding and Rejoin

Onboarding several devices at once: one-time codes​

The code on the Hub's onboarding screen is one code at a time. To onboard several devices by code at the same time, ask the Hub for codes of your own from an onboarded session:

QarRequestOnboardingCodesInit request = qar_request_onboarding_codes_init_default();
request.count = 3; /* 1 to 64 */
request.lifetime_seconds = 300; /* 0 = 120 s, at most 600 s */
QarOnboardingInvite* invites[3] = { NULL, NULL, NULL };
qar_session_request_onboarding_codes(session, &request, NULL, NULL, NULL, invites);

char code[128];
qar_onboarding_invite_get_code(invites[0], code, sizeof(code), NULL);
/* hand `code` to one device; it onboards with QarOnboardCodeExt.code = code */
  • Every call returns new codes. Codes other callers hold, and the code on the onboarding screen, stay valid; none of yours is shown on the screen.
  • Each code works once, until its lifetime ends. A failed attempt also uses it up.
  • Hand a device either the code text (qar_onboarding_invite_get_code, used with QarOnboardCodeExt) or the whole invite (qar_onboarding_invite_serialize, used with QarOnboardInviteExt).
  • Set planned_peer_id and a device that pairs with one of these codes can only join under that peer id (QarOnboardPeerIdExt).
  • The code text is a secret. Do not log it or put it on a command line.
  • C#: session.RequestOnboardingCodes(new RequestOnboardingCodesOptions { Count = 3 }) and invite.GetCode().

Choosing the GPU​

Chain a QarGraphicsDeviceId into QarOnboardInit or QarRejoinInit to create the session's GPU devices — and with them every render sender's frame textures — on a specific adapter. Windows takes the adapter LUID, other platforms its UUID. Absent, QAROS selects one: for an app it launched, its target's GPU; otherwise the library picks one. Only NVIDIA adapters are supported; an unsupported adapter fails with QAR_STATUS_ARGUMENT_NOT_SUPPORTED.

QarGraphicsDeviceId adapter = qar_graphics_device_id_default();
adapter.id_type = QAR_GPU_DEVICE_ID_TYPE_LUID;
memcpy(&adapter.luid, my_adapter_luid, sizeof adapter.luid); /* e.g. VkPhysicalDeviceIDProperties::deviceLUID */

onboard.header.next = &onboard_code.header; /* mode extension first */
onboard_code.header.next = &adapter.header; /* then the adapter */

Render on the session's adapter, so frames need no cross-GPU copy. Whether you chained one or QAROS selected it, read the adapter in use after onboarding with qar_session_get_graphics_device_id (C# Session.GetGraphicsDeviceId()) and create your graphics device on it:

QarGraphicsDeviceId gpu = qar_graphics_device_id_default();
bool present = false;
qar_session_get_graphics_device_id(session, &gpu, &present);
/* present: match gpu.luid (Windows) or gpu.uuid against VkPhysicalDeviceIDProperties */

Joining under a planned peer id​

A process whose peer id was planned by someone else (for example a process another component starts and announces to the session) chains QarOnboardPeerIdExt next to the mode extension. The session then sees it under that id instead of a freshly minted one. The id is persisted with the identity slot, so later rejoins keep it. A zero id is rejected with QAR_STATUS_ARGUMENT_NOT_SUPPORTED. Most applications do not need it.

Launched by QAROS: QAROS API apps​

An operator can add your executable as a QAROS API app in the visualizer's Source Applications panel. QAROS then starts one process of it for all targets, and on every start hands it:

WhatHowFormat
A launch invite: everything needed to reach this Hub, with its own one-time code, and the GPU to render onFirst line of stdin, newline-terminated. Never on the command line, never logged, never shown on the Hub's onboarding screen.One line of JSON starting with {: the Hub's IP, its onboarding port, a one-time pairing code, the CA fingerprint and, when the target advertised one, its GPU. Pass it unchanged to qar_onboarding_invite_deserialize; do not parse it yourself.
ArgumentsCommand lineOne argument per line the operator entered

QAROS sets no environment variable for your app. The working directory is the executable's folder; stdout and stderr go to the Hub's log folder. To work as an API app:

  1. Read the first stdin line. If it starts with {, it is the launch invite: turn it into a handle with qar_onboarding_invite_deserialize and call qar_runtime_onboard with QarOnboardInviteExt. Anything else is a code typed by hand (when someone runs your app from a console): use QarOnboardCodeExt, which finds that user's Hub. Do not prompt for anything else: nobody is there to answer.

    QarOnboardInit init = qar_onboard_init_default();
    QarOnboardInviteExt invite_ext = qar_onboard_invite_ext_default();
    QarOnboardCodeExt code_ext = qar_onboard_code_ext_default();
    QarOnboardingInvite* invite = NULL;
    if(line[0] == '{')
    {
    qar_onboarding_invite_deserialize((const uint8_t*)line, strlen(line), &invite);
    invite_ext.invite = invite;
    init.header.next = &invite_ext.header;
    }
    else
    {
    code_ext.code = line;
    init.header.next = &code_ext.header;
    }
    /* qar_runtime_onboard(runtime, &init, ...), then qar_onboarding_invite_handle_destroy(invite) */
  2. Onboard first, without chaining a QarGraphicsDeviceId: QAROS creates the session on its target's GPU. Then read that adapter with qar_session_get_graphics_device_id (C# Session.GetGraphicsDeviceId()) and create your graphics device on it (above). An app that must render on a GPU of its own chains its QarGraphicsDeviceId instead; a chained adapter always wins.

  3. Create your own app volume (qar_app_volumes_get_or_create); QAROS registers none for an API app.

  4. Find the target apps in the peer list and create a sender for each one you choose to serve (Rendering Streams). QAROS does not choose your targets: the visualizer shows them as chosen by the app.

  5. Treat end of stdin as a request to stop. QAROS closes stdin on Stop and kills the process about 1.3 s later.

An app that exits within 10 s of starting is restarted with backoff (2 s, doubling up to 60 s) and the operator is notified of the first failure. The launch invite is valid for 2 minutes, for this start only, and works once: each restart brings a new one, so onboard again rather than replaying the old one. Apps written against the earlier contract, which read the line as a code, must accept the JSON invite. The code on the Hub's onboarding screen is separate and does not change when QAROS starts your app, so people can keep pairing devices with it meanwhile. The vulkan_cube example implements this contract.

Getting devices into the session​

Every peer reaches the session by running its own rejoin-or-onboard flow (this page) against the hub, either directly with a pairing code or via a serialized invite from an already-onboarded instance (see Sibling onboarding above). Once a peer has joined, it becomes visible through peer enumeration, and it is the peer itself that requests a render stream from your application — see Accepting render stream requests.

To bring an AR device in from your application, ask this machine's hub to start its target app with qar_session_invite_target_app (or _async). The connection string names the device kind and address: "hololens:<host>", "quest:<host>" or "android:<host>". The call returns once the device's peer has joined and gives you its peer id; qar_session_kill_target_app stops it again. Only the hub on your own machine answers, the invite is one-time (only an operator can mark a device for automatic re-invite), and a device another hub is serving is moved to this one.

Teardown: destroy vs. forget​

Teardown is two separate decisions:

CallEffectWhen
qar_session_handle_destroy(session)Tears down the active session but keeps the persisted identity, so the next start can rejoin silently.Every normal shutdown
qar_runtime_forget(runtime, &forget_init)Erases the persisted identity slot (certificate + state). Next start requires a fresh pairing code. Rejected with QAR_STATUS_ONBOARDING_SESSION_STILL_ACTIVE while the session is active — destroy the session handle first."Sign out" / "unpair this device"
qar_session_handle_destroy(session);
/* only for an explicit un-pair: */
QarForgetInit forget = qar_forget_init_default();
forget.onboarding_id = saved_id;
qar_result_log_if_error(qar_runtime_forget(runtime, &forget));

Compiled tutorial​

This page is backed by the compiled example onboarding_and_rejoin.c, including the sibling-instance onboarding flow. Use it as the reference implementation when wiring two QAROS API instances into the same application: