Skip to main content
Version: 0.9.0 (next)

CPU Rendering

What you build​

A source application that floats a lit sphere, 25 cm in radius, in the middle of its own app volume. It finds every target app in the session (a visualizer, a headset), gives each one a render sender, and renders every frame on the CPU from that target's eye poses and fields of view, with color and depth for both eyes.

What you will learn:

  • why a source app finds its targets in the peer list instead of waiting for render requests;
  • how to declare the frame views of a stereo color-and-depth stream;
  • the frame cycle: begin a frame, read the pose and field of view of each view, write pixels, show;
  • which status codes mean "keep the sender and try again" and which mean "this sender is finished";
  • what the Rust handles own and in which order they are released.

The program is examples/src/bin/cpu_rendering_visualizer.rs in the Rust binding. This page includes its code; the file is the authority.

Before you start​

  • Platform. Windows for the real runtime. On macOS and Linux the example runs against qaros-stub, the in-memory stand-in for the runtime that ships with the binding's tests. The stub is enough to follow the frame cycle; it shows no picture.
  • QAROS. Installed on the Hub machine, where the example runs, or a runtime folder named by QAROS_RUNTIME_DIR, or a library file named with --qaros-library=PATH. The example finds the runtime the way every binding does (Language Bindings).
  • A target. At least one target app in the session: open the Visualizer, or start a headset. Joining as a local app starts the Hub when it is not running.
  • Joining. On Windows the example joins the Hub as a local app (Local app): the first run of the executable opens the QAROS approval prompt, you click Allow once, and later runs of the same executable join without a prompt. A local app has no onboarding id, so nothing is stored.1
  • The latest stable Rust.

Run it​

From the qar-streaming-rust folder of the repository, on Windows:

# Join the Hub as a local app: click Allow in QAROS on the first run.
cargo run --release -p qaros-examples --bin cpu_rendering_visualizer -- --frames=300

--frames=N (or --frames N) stops the example after N shown frames, counted over all targets; without it, the example stops after 300. An unsigned executable asks for Allow again after every rebuild that changes it.

Without QAROS, run it against the stub. Build the stub first; it is a test library, so cargo run does not build it:

cargo build -p qaros-stub
mkdir -p target/stub-run && cd target/stub-run
# Windows: qaros_stub.dll; Linux: libqaros_stub.so
QAROS_DLL="$PWD/../debug/libqaros_stub.dylib" cargo run -p qaros-examples --bin cpu_rendering_visualizer -- ANY-CODE --frames=3

Outside Windows the example does not try the local-app join, so the stub run passes a code: the stub accepts any code except WRONG (a rejected code) and UNREACHABLE (no Hub). The stub run stores cpu_rendering_visualizer.onboarding-id.txt in its folder, which a later run against QAROS from that folder would try to rejoin with: keep stub runs in a folder of their own (On macOS, against the stub). A first run and then a second one, which rejoins, print (with your own path):

Loading qar-streaming-c from: .../target/stub-run/../debug/libqaros_stub.dylib
Loaded .../target/debug/libqaros_stub.dylib
qar_runtime_create: SUCCESS
qar_runtime_onboard: SUCCESS
Looking for target apps in the peer list (open a visualizer or start a headset) ...
Rendering for Stub Visualizer (peer 56:56:56:56:56:56:56:56:56:56:56:56:56:56:56:56)
Submitted 3 frames.
Loading qar-streaming-c from: .../target/stub-run/../debug/libqaros_stub.dylib
Loaded .../target/debug/libqaros_stub.dylib
qar_runtime_create: SUCCESS
qar_runtime_rejoin: SUCCESS
Looking for target apps in the peer list (open a visualizer or start a headset) ...
Rendering for Stub Visualizer (peer 56:56:56:56:56:56:56:56:56:56:56:56:56:56:56:56)
Submitted 3 frames.

Without QAROS_DLL (or --qaros-library) the example uses the default lookup: it prints Loading the QAROS runtime (default lookup), and the Loaded line names the step that found the runtime, for example (the installed QAROS). Against a real Hub you then see the sphere in the "CPU Tutorial Volume" app volume on each target.

Walkthrough​

1. Declare the frame views​

A render sender streams one video frame per display frame. The frame views say what is in it and where: which eye, color or depth, and in which texture. The Rust example declares four views: both eyes' color side by side in texture 0, both eyes' depth side by side in texture 1.

/// Color of both eyes side by side in texture 0, depth side by side in texture 1. View 0 and
/// view 1 are the left and the right eye for poses and fields of view.
const FRAME_VIEWS: [FrameViewSpec; 4] = [
FrameViewSpec {
kind: ViewKind::Color,
eye: Eye::Left,
texture_index: 0,
},
FrameViewSpec {
kind: ViewKind::Color,
eye: Eye::Right,
texture_index: 0,
},
FrameViewSpec {
kind: ViewKind::Depth,
eye: Eye::Left,
texture_index: 1,
},
FrameViewSpec {
kind: ViewKind::Depth,
eye: Eye::Right,
texture_index: 1,
},
];

The order matters. Every frame lists its views in the order you declared them, and the view index you pass to OwnedFrame::view is that position: view 0 is the left eye, view 1 the right eye (Rendering Streams, "Frame views"). A sender created with no views at all hands out frames without poses: reading the pose of view 0 then fails with QAR_STATUS_LOGIC_ERROR.

Send depth whenever you can. QAROS uses it for occlusion between app volumes and to warp your frame to the viewer's latest head pose.

2. Load the runtime and get a session​

Every session comes from onboarding: there is no "connect" call. The helpers in examples/src/lib.rs load the runtime, create a Runtime, then rejoin with the stored onboarding id, onboard with the pairing code, or join as a local app.

let arguments = Arguments::parse();
let frame_limit = frame_limit(&arguments, DEFAULT_FRAME_COUNT)?;

let library = load_library()?;
let runtime = create_runtime(&library, None)?;
let session = join_session(
&library,
&runtime,
arguments.pairing_code.as_deref(),
Path::new("cpu_rendering_visualizer.onboarding-id.txt"),
"CPU Renderer",
)?;

join_session tries the three in that order:

/// The startup of the CPU, D3D11, GUI panel and app volume examples: rejoin with the persisted
/// onboarding id, else onboard with the pairing code when one is passed (as `obtain_session`),
/// else, on Windows, join the Hub on this machine as a local app. Their C originals onboard by code
/// only.
pub fn join_session(
library: &Library,
runtime: &Runtime,
pairing_code: Option<&str>,
id_file: &Path,
display_name: &str,
) -> ExampleResult<Session> {
let presentation = presentation(display_name);
if let Some((_, session)) = rejoin_with_stored_id(library, runtime, id_file, &presentation)? {
return Ok(session);
}
if let Some(pairing_code) = pairing_code {
let (_, session) =
onboard_with_code(library, runtime, pairing_code, id_file, &presentation)?;
return Ok(session);
}
if cfg!(windows) {
join_as_local_app(runtime, &presentation)
} else {
Err(missing_pairing_code(id_file))
}
}
  • Rejoin. With a stored id it calls Runtime::rejoin. When the Hub has no session for that id, the error carries StatusCode::ONBOARDING_SESSION_NOT_FOUND, which is normal flow: it goes on to the next way. Any other rejoin error is real and ends the run.
  • Pairing code. With a code it calls Runtime::onboard with OnboardMethod::Code and stores the new id, as the C example does. A rejected or expired code fails with StatusCode::PAKE_ERROR; read the current code and try again.
  • Local app. With neither, on Windows, it calls Runtime::onboard with OnboardMethod::LocalApp, as Local App Onboarding explains, and waits up to 120 s for Allow. The returned onboarding id is the zero id: there is nothing to store, and the next run joins the same way. A refusal ends the run with the exit codes of local_app_onboarding, 3 to 7. Elsewhere it stops with "No persisted onboarding id".

3. Create the app volume​

A render sender needs an app volume. The mixer composites the stream inside that box, and a sender for a target is what attaches the target to it (App Volumes).

// A render sender requires the app volume its stream is shown in.
let mut volume_config = AppVolumeConfig::new("cpu-volume.examples.qaros");
volume_config.display_name = Some("CPU Tutorial Volume".to_string());
volume_config.pose = Pose::from_position(Vec3::new(0.0, 0.0, -1.0));
volume_config.size = AppVolumeSize {
width: 1.0,
length: 1.0,
height: 1.0,
};
let volume = session.app_volumes().get_or_create(&volume_config)?;

get_or_create is idempotent: the common name identifies the volume, so a second run gets the same one back. The pose puts its center 1 m in front of the room origin, in room space; the size is in metres. The App Volumes tutorial covers volumes in full: the box and the content inside it, and their updates.

4. Follow the target apps in the peer list​

Your application decides which targets it renders for; QAROS does not choose for it (Rendering Streams). This example serves every running target app. Do not wait for render requests to find targets: a target requests your content only once it is attached to your app volume, and a sender is what attaches it. So a new application that waits for requests waits forever.

/// One target app and the sender rendering for it.
struct Target {
peer: PeerId,
label: String,
sender: RenderSender,
}

/// Creates a sender for every running target app the list shows and drops the senders of
/// targets it no longer shows running.
fn follow_targets(
library: &Library,
session: &Session,
volume: AppVolumeId,
listed: &[PeerInfo],
targets: &mut Vec<Target>,
) {
targets.retain(|target| {
let still_running = listed
.iter()
.any(|peer| peer.id == target.peer && is_running_target(peer));
if !still_running {
println!("{} left", target.label);
}
still_running
});
for peer in listed.iter().filter(|peer| is_running_target(peer)) {
if targets.iter().any(|target| target.peer == peer.id) {
continue;
}
let label = if peer.display_name.is_empty() {
peer.id.to_string()
} else {
peer.display_name.clone()
};
match create_sender(library, session, volume, peer.id) {
Ok(sender) => {
println!("Rendering for {label} (peer {})", peer.id);
targets.push(Target {
peer: peer.id,
label,
sender,
});
}
Err(error) => println!("No sender for {label} yet: {error}"),
}
}
}

A peer is a target to render for when PeerInfo::is_target_app is true and PeerInfo::app_state is AppState::Running:

/// A target app to render for: its app state is `AppState::Running`. The public header deprecates
/// the app state for the peer presence (`qar_peer_spec_get_presence`) and computes it from the
/// presence: an offline or leaving peer reads `ShuttingDown`, a retired one `Retired`, an online
/// or not yet reconciled one what it claimed, and every current peer claims `Running` once it
/// joins. Running is then the presence's online or unknown, the targets the guide renders for.
pub fn is_running_target(peer: &PeerInfo) -> bool {
peer.is_target_app && peer.app_state == qaros::AppState::Running
}

The guide picks targets by their presence (qar_peer_spec_get_presence): online, or unknown on a Hub that has not reconciled the peer (Working with Peers). The safe API reads the app state instead; the presence getter is raw only, through library.raw_api() with the session's as_raw(). The app state is deprecated but computed from the presence, so for every current peer Running selects the same targets. On every listing, follow_targets drops the senders of targets that left or stopped running, and creates a sender for every new one.

The main loop subscribes to peer updates before it lists the peers, so a target that joins in between is not missed. Peer updates arrive on a library thread; qaros::channel forwards them into a channel the loop drains. The loop re-reads the list after any update, and at least every 2 s.

// Subscribe first, then list, so a target joining in between is not missed.
let (forward, peer_updates) = qaros::channel();
let _peer_subscription = session.subscribe_peer_updates(forward)?;
let started = Instant::now();
let mut targets: Vec<Target> = Vec::new();
let mut last_listing: Option<Instant> = None;
let mut frames_shown = 0_usize;
println!("Looking for target apps in the peer list (open a visualizer or start a headset) ...");

while frames_shown < frame_limit {
let updates_arrived = peer_updates.try_iter().count() > 0;
if updates_arrived || last_listing.is_none_or(|last| last.elapsed() >= PEER_LIST_INTERVAL) {
last_listing = Some(Instant::now());
follow_targets(&library, &session, volume, &session.peers()?, &mut targets);
}
let mut showed_a_frame = false;
targets.retain_mut(|target| match render_next_frame(target, started) {
Ok(showed) => {
showed_a_frame |= showed;
frames_shown += usize::from(showed);
true
}
Err(error) => {
println!("Stopped streaming to {}: {error}", target.label);
false
}
});
if !showed_a_frame {
std::thread::sleep(IDLE_WAIT);
}
}
println!("Submitted {frames_shown} frames.");
Ok(())
}

One thread does everything. qaros handles are not Send: the SDK is designed for single-threaded use per runtime. So the loop serves all targets in turn with the non-blocking begin (step 6), and sleeps 2 ms only when no target had a frame ready.

5. Create a render sender per target​

fn create_sender(
library: &Library,
session: &Session,
volume: AppVolumeId,
peer: PeerId,
) -> qaros::Result<RenderSender> {
let config = RenderSenderConfig {
views: FRAME_VIEWS.to_vec(),
app_volume: Some(volume),
color_format: PixelFormat::R8G8B8A8,
depth_format: PixelFormat::D32Float,
layout: FrameLayout::SideBySide,
..RenderSenderConfig::new(peer)
};
let timeout = library.create_cancel_token(Some(SENDER_CREATION_TIMEOUT))?;
session.create_render_sender(
&config,
&BlockingOptions {
cancel: Some(&timeout),
..BlockingOptions::default()
},
)
}

RenderSenderConfig::new(peer) starts from the C defaults: auto-reconnect on, side-by-side layout, BGRA8 color, D32 depth. The example sets its views, its app volume, RGBA8 color and the side-by-side layout. create_render_sender blocks until the target answers. A cancel token with a 10 s timeout bounds that wait. A target that does not answer yields an error, follow_targets prints No sender for ... yet, and the next listing tries again.

6. Begin, render, show​

/// Renders and shows the target's next frame when its sender has one; false while none is
/// ready or the stream is waiting.
fn render_next_frame(target: &mut Target, started: Instant) -> qaros::Result<bool> {
let mut frame = match target.sender.try_begin_owned_frame() {
Ok(Some(frame)) => frame,
Ok(None) => return Ok(false),
Err(error) if is_waiting(&error) => return Ok(false),
Err(error) => return Err(error),
};
let eyes = [frame.view(0)?, frame.view(1)?];
let seconds = started.elapsed().as_secs_f32();
let sphere_center = Vec3::new(0.0, 0.05 * (seconds * 1.5).sin(), 0.0);
{
let cpu_frame = frame.cpu_frame()?;
let mut textures = cpu_frame.textures;
for view in &cpu_frame.views {
let eye_index = usize::from(view.eye == Eye::Right);
let Some(texture) = textures.get_mut(view.texture_index as usize) else {
continue;
};
let camera = EyeRay::new(eyes[eye_index].pose, eyes[eye_index].fov);
render_view(
texture.data,
texture.pitch as usize,
view,
&camera,
sphere_center,
seconds,
);
}
}
match frame.show(&ShowParams {
near: NEAR_PLANE,
far: FAR_PLANE,
depth_scale: 1.0,
}) {
Ok(()) => Ok(true),
Err(error) if is_waiting(&error) => Ok(false),
Err(error) => Err(error),
}
}

try_begin_owned_frame starts the library's begin-frame wait on its first call and polls it on later calls. Ok(None) means no frame yet: the target does not want one, or the receiver paused the stream. Poll again later.

A frame you get carries the pose and field of view to render each view with. They are in your app volume's app content space, the space you render in (Coordinate Systems). frame.view(0) and frame.view(1) return them for the two eyes, as declared in step 1. The example turns each into rays through the pixels:

/// Rays through an eye's pixels, in app content space.
struct EyeRay {
origin: Vec3,
orientation: Quat,
tangents: [f32; 4],
}

impl EyeRay {
fn new(pose: Pose, fov: qaros::Fov) -> Self {
Self {
origin: pose.position,
orientation: pose.orientation,
tangents: [
fov.angle_left.tan(),
fov.angle_right.tan(),
fov.angle_up.tan(),
fov.angle_down.tan(),
],
}
}

/// The direction through view-local pixel (`u`, `v`) of a `width` x `height` view, scaled
/// so that its view-space Z is -1: the ray parameter at a hit is then the hit's depth
/// along the optical axis.
fn direction(&self, u: f32, v: f32, width: f32, height: f32) -> Vec3 {
let [left, right, up, down] = self.tangents;
let x = left + (u + 0.5) / width * (right - left);
let y = up - (v + 0.5) / height * (up - down);
rotate(self.orientation, Vec3::new(x, y, -1.0))
}
}

cpu_frame hands out the frame's textures as byte slices with their pitch, and the views with their pixel rectangles. A view whose start lies past its end is mirrored. The example writes color views as RGBA or BGRA bytes and depth views as 32-bit floats:

fn write_pixel(pixel: &mut [u8], view: &VideoFrameView, hit: Option<([u8; 4], f32)>) {
match view.format {
PixelFormat::D32Float | PixelFormat::R32Float => {
let depth = hit.map_or(1.0, |(_, distance)| forward_depth(distance));
pixel.copy_from_slice(&depth.to_ne_bytes());
}
PixelFormat::R8G8B8A8 => {
pixel.copy_from_slice(&hit.map_or([0; 4], |(color, _)| color));
}
PixelFormat::B8G8R8A8 => {
let [red, green, blue, alpha] = hit.map_or([0; 4], |(color, _)| color);
pixel.copy_from_slice(&[blue, green, red, alpha]);
}
other => log::warn!("leaving a {other:?} view untouched"),
}
}

The header does not specify the depth encoding. The example writes forward depth, 0 at the near plane and 1 at the far plane, as the Vulkan cube does:

/// D3D/OpenXR-style forward depth: 0 at near, 1 at far.
fn forward_depth(distance: f32) -> f32 {
FAR_PLANE * (distance - NEAR_PLANE) / (distance * (FAR_PLANE - NEAR_PLANE))
}

show presents the frame with the near and far planes the frame was rendered with, in app units. QAROS reprojects with exactly this metadata and your depth. Wrong values show up as swimming or misplaced content, never as an error code (Rendering Streams).

7. Keep the sender, or end it​

Some failures are part of the normal flow. The stream may be reconnecting, may have no transfer yet, or your app volume may not resolve yet where the receiver runs. For these, keep the sender and try the next frame (Troubleshooting):

/// A stream that is paused, reconnecting or has no transfer yet is not broken: keep the sender
/// and try the next frame. Like the C cube's `IsWaiting` this matches a wrapped cause too
/// (begin-frame reports these inside `RENDERING_UNABLE_TO_BEGIN_FRAME`). An app volume that
/// does not resolve yet is waiting as well: the header calls it recoverable. Any other error
/// ends the sender.
pub fn is_waiting(error: &qaros::Error) -> bool {
[
StatusCode::MEDIA_STREAMING_STREAM_IS_PAUSED,
StatusCode::MEDIA_STREAMING_STREAM_IS_RECONNECTING,
StatusCode::MEDIA_STREAMING_NO_ACTIVE_TRANSFERS,
StatusCode::RENDERING_APP_VOLUME_UNRESOLVED,
]
.into_iter()
.any(|code| error.has_cause(code))
}
Status (qaros::StatusCode)C nameWhat the example does
MEDIA_STREAMING_STREAM_IS_PAUSEDQAR_STATUS_MEDIA_STREAMING_STREAM_IS_PAUSEDkeeps the sender; begin-frame waits through a pause, so it mostly shows up as Ok(None)
MEDIA_STREAMING_STREAM_IS_RECONNECTINGQAR_STATUS_MEDIA_STREAMING_STREAM_IS_RECONNECTINGkeeps the sender, tries the next frame
MEDIA_STREAMING_NO_ACTIVE_TRANSFERSQAR_STATUS_MEDIA_STREAMING_NO_ACTIVE_TRANSFERSkeeps the sender, tries the next frame
RENDERING_APP_VOLUME_UNRESOLVEDQAR_STATUS_RENDERING_PRODUCER_APP_VOLUME_UNRESOLVEDkeeps the sender; the call holds about 16 ms, so the loop does not spin
RENDERING_STREAM_IS_CLOSEDQAR_STATUS_RENDERING_PRODUCER_STREAM_IS_CLOSEDdrops the sender; the next listing creates a new one if the target still runs
anything elsedrops the sender, prints Stopped streaming to ..., as above

Begin-frame reports the waiting codes wrapped inside RENDERING_UNABLE_TO_BEGIN_FRAME. Error::has_code compares only the outer code; Error::has_cause also matches a wrapped one, like the C qar_result_has_code. Use has_cause for these checks.

Ownership and teardown​

In C you destroy every handle yourself, in the right order: the sender before its session, the session before the runtime, the runtime before the library. In Rust each handle releases itself on drop and keeps its parent alive:

ValueOwnsKeeps alive
Libraryqar_library_init for the process
Runtimethe QarRuntimethe library
Sessionthe QarSession, the subscriptions' callback statethe runtime
Subscription (_peer_subscription)the subscription's cancel token
RenderSender (in each Target)the QarRenderSenderthe session
OwnedFramethe frame info, until shown or droppedthe sender
CpuFrameborrowed texture bytes

So the C order holds whatever order Rust drops them in. At the end of run, locals drop in reverse order of declaration: the targets and their senders, the subscription, the session, the runtime, then the library.

What the wrapper guarantees:

  • One open frame per sender. A second begin while a frame is open fails before it reaches the library.
  • Texture bytes cannot outlive the frame. The slices from cpu_frame borrow the frame, so the compiler rejects any use after show.
  • A dropped frame is released. An OwnedFrame dropped without show destroys its frame info and closes the cycle without presenting.
  • The library stays loaded. Dropping the Library calls qar_library_destroy but never unloads the DLL, because library threads may still run its code.

What it does not guarantee:

  • Your metadata. It cannot check that you rendered with the poses begin gave you, or that show reports the planes you used.
  • Threads. Handles are not Send, so the compiler keeps them on one thread. Callbacks still run on library threads; keep them short and never call blocking QAROS functions from them.
  • A dropped subscription guard stops delivery but frees nothing. The callback state lives until the session is destroyed, so the receiver from qaros::channel disconnects only then.

Troubleshooting​

SymptomCauseWhat to do
No persisted onboarding id in '...'not on Windows, no code, and no id file in the working directorypass the Hub's pairing code
exit 4 or 5 after joining the QAROS Hub on this machine as a local appthe approval prompt was denied, closed or not answered within 120 srun again and click Allow
exit 3the local-app join, which starts a Hub that is not running, reached no Hubstart QAROS and run again
Pairing code rejected or expired (QAR_STATUS_PAKE_ERROR)wrong code, or the code changed (they live about 10 s)read the current code and run again at once
QAR_STATUS_ONBOARDING_HUB_UNREACHABLEwith a code: no Hub of your user answered; the example finds only your own Hub, on the Hub machinestart QAROS and run again
a not-found error naming every place it lookedno runtime foundinstall QAROS, set QAROS_RUNTIME_DIR, or pass --qaros-library=PATH
stays at Looking for target apps ...no running target app in the sessionopen the Visualizer or start a headset
No sender for <target> yet: ..., repeatedthe target does not answer the sender creation within 10 scheck the target is running; each attempt blocks the loop for up to 10 s, so the Vulkan tutorial's example retries less often
Stopped streaming to <target>: ...a failure that is not part of the normal flow, RENDERING_STREAM_IS_CLOSED includedthe next listing creates a new sender when the target still runs
content swims or sits at the wrong depthnear and far in show do not match the frame, or poses not taken from this framerender with this frame's view results and report the planes you used

From C​

The C tutorial is CPU Rendering with a Visualizer. What corresponds:

CRust
example_load_library: qar_library_load, qar_library_initLibrary::load_default, Library::load
example_create_runtime: qar_runtime_createLibrary::create_runtime
example_join_session (rejoin, else code, else local app)join_session in examples/src/lib.rs (rejoin, else code, else local app)
qar_app_volumes_get_or_createsession.app_volumes().get_or_create
qar_peer_subscribe_updates, qar_query_peer_specssession.subscribe_peer_updates, session.peers()
example_is_running_target: role QAR_PEER_ROLE_TARGET, presence ONLINE or UNKNOWNis_running_target: PeerInfo::is_target_app and AppState::Running, the deprecated app state computed from the presence, so the same targets
qar_render_sender_create with QAR_GRAPHICS_API_CPUSession::create_render_sender
qar_render_sender_try_begin_frameRenderSender::try_begin_owned_frame (non-blocking); begin_frame / begin_owned_frame block
qar_render_frame_info_get_view_pose, _get_view_fovOwnedFrame::view, one ViewPose
qar_render_sender_frame_cpuOwnedFrame::cpu_frame
qar_render_sender_show_frameOwnedFrame::show
qar_render_frame_info_handle_destroy, qar_render_stream_handle_destroydrop
qar_session_handle_destroy, qar_runtime_handle_destroy, example_unload_library (qar_library_destroy, qar_library_unload)drop the values; the library is destroyed, not unloaded
example_is_waiting with qar_result_has_codeis_waiting with Error::has_cause

The two programs do the same thing: join, find the targets in the peer list, serve all of them, declare the views, render the sphere from each frame's poses with depth, and keep senders through waiting statuses. The C program renders on one thread; the Rust example splits the rows across all cores.

Next​

  • D3D11 Rendering: the same frame cycle with shared GPU textures, on a D3D11 device of your own.
  • Vulkan Rendering: the same frame cycle with sender-owned Vulkan images, with a device of your own on the session's GPU.
  • Rendering Streams: layouts, layout changes, hand tracking.

Footnotes​

  1. The example also takes a pairing code, as its first argument that does not start with --; it passes no Hub address, so the code finds only your own Hub, on the Hub machine (a Hub elsewhere needs its host and port, HubAddress): cargo run --release -p qaros-examples --bin cpu_rendering_visualizer -- <PAIRING-CODE> --frames=300. It then stores the onboarding id it gets back in cpu_rendering_visualizer.onboarding-id.txt in the working directory, and later runs rejoin with it (Onboarding and Rejoin). ↩