Onboarding and Sessions
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_rejoinpops no UI and is silent — call it optimistically on every start.QAR_STATUS_ONBOARDING_SESSION_NOT_FOUNDis expected control flow: it means "this identity has no persisted slot here — onboard instead".QAR_STATUS_PAKE_ERRORmeans the code was wrong or expired (codes live ~10 seconds) — just re-prompt.- With
QarOnboardCodeExtalone,qar_runtime_onboardfinds your own Hub. A Hub has no fixed onboarding port (it claims one from its port range), so the lookup goes:- The
hub-endpoint.jsonfile 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. - Otherwise a discovery beacon from a Hub run by the same user on this PC, listening up to 6 s.
- Otherwise it fails with
QAR_STATUS_ONBOARDING_HUB_UNREACHABLE, asking for the Hub's host and port.
- The
- To reach a specific Hub, chain a
QarOnboardHostExtafter the code extension with bothhostnameandport(see API Conventions). The port is shown with the Hub's pairing code;port = 0is rejected withQAR_STATUS_ARGUMENT_NOT_SUPPORTED. - Both calls have
_asyncvariants whose result callback delivers the same(onboarding_id, session)pair exactly once — use those to keep your UI thread responsive, and pass aQarCancelTokenso 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:
- Instance A starts with the normal rejoin-or-onboard flow and gets a
QarSession*. - Instance A calls
qar_session_request_onboarding_invite. - Instance A serializes the returned
QarOnboardingInviteand sends the bytes over your IPC boundary. - Instance B creates its own
QarRuntimewith its own storage root. - Instance B deserializes the invite and calls
qar_runtime_onboardwithQarOnboardInviteExt. - Instance B persists its own returned
QarOnboardingIdfor 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_pathor its own persisted onboarding-id file. The tutorial example usesonboarding_and_rejoin.primary-stateandonboarding_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 withQarOnboardCodeExt) or the whole invite (qar_onboarding_invite_serialize, used withQarOnboardInviteExt). - Set
planned_peer_idand 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 })andinvite.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:
| What | How | Format |
|---|---|---|
| A launch invite: everything needed to reach this Hub, with its own one-time code, and the GPU to render on | First 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. |
| Arguments | Command line | One 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:
-
Read the first stdin line. If it starts with
{, it is the launch invite: turn it into a handle withqar_onboarding_invite_deserializeand callqar_runtime_onboardwithQarOnboardInviteExt. Anything else is a code typed by hand (when someone runs your app from a console): useQarOnboardCodeExt, 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) */ -
Onboard first, without chaining a
QarGraphicsDeviceId: QAROS creates the session on its target's GPU. Then read that adapter withqar_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 itsQarGraphicsDeviceIdinstead; a chained adapter always wins. -
Create your own app volume (
qar_app_volumes_get_or_create); QAROS registers none for an API app. -
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.
-
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:
| Call | Effect | When |
|---|---|---|
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:
- Onboarding and Rejoin
- Dynamic Loading tutorial for the DLL bootstrap used by the example