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 carriesStatusCode::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::onboardwithOnboardMethod::Codeand stores the new id, as the C example does. A rejected or expired code fails withStatusCode::PAKE_ERROR; read the current code and try again. - Local app. With neither, on Windows, it calls
Runtime::onboardwithOnboardMethod::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 oflocal_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 name | What the example does |
|---|---|---|
MEDIA_STREAMING_STREAM_IS_PAUSED | QAR_STATUS_MEDIA_STREAMING_STREAM_IS_PAUSED | keeps the sender; begin-frame waits through a pause, so it mostly shows up as Ok(None) |
MEDIA_STREAMING_STREAM_IS_RECONNECTING | QAR_STATUS_MEDIA_STREAMING_STREAM_IS_RECONNECTING | keeps the sender, tries the next frame |
MEDIA_STREAMING_NO_ACTIVE_TRANSFERS | QAR_STATUS_MEDIA_STREAMING_NO_ACTIVE_TRANSFERS | keeps the sender, tries the next frame |
RENDERING_APP_VOLUME_UNRESOLVED | QAR_STATUS_RENDERING_PRODUCER_APP_VOLUME_UNRESOLVED | keeps the sender; the call holds about 16 ms, so the loop does not spin |
RENDERING_STREAM_IS_CLOSED | QAR_STATUS_RENDERING_PRODUCER_STREAM_IS_CLOSED | drops the sender; the next listing creates a new one if the target still runs |
| anything else | drops 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:
| Value | Owns | Keeps alive |
|---|---|---|
Library | qar_library_init for the process | |
Runtime | the QarRuntime | the library |
Session | the QarSession, the subscriptions' callback state | the runtime |
Subscription (_peer_subscription) | the subscription's cancel token | |
RenderSender (in each Target) | the QarRenderSender | the session |
OwnedFrame | the frame info, until shown or dropped | the sender |
CpuFrame | borrowed 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_frameborrow the frame, so the compiler rejects any use aftershow. - A dropped frame is released. An
OwnedFramedropped withoutshowdestroys its frame info and closes the cycle without presenting. - The library stays loaded. Dropping the
Librarycallsqar_library_destroybut 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
begingave you, or thatshowreports 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::channeldisconnects only then.
Troubleshooting
| Symptom | Cause | What to do |
|---|---|---|
No persisted onboarding id in '...' | not on Windows, no code, and no id file in the working directory | pass the Hub's pairing code |
exit 4 or 5 after joining the QAROS Hub on this machine as a local app | the approval prompt was denied, closed or not answered within 120 s | run again and click Allow |
| exit 3 | the local-app join, which starts a Hub that is not running, reached no Hub | start 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_UNREACHABLE | with a code: no Hub of your user answered; the example finds only your own Hub, on the Hub machine | start QAROS and run again |
| a not-found error naming every place it looked | no runtime found | install QAROS, set QAROS_RUNTIME_DIR, or pass --qaros-library=PATH |
stays at Looking for target apps ... | no running target app in the session | open the Visualizer or start a headset |
No sender for <target> yet: ..., repeated | the target does not answer the sender creation within 10 s | check 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 included | the next listing creates a new sender when the target still runs |
| content swims or sits at the wrong depth | near and far in show do not match the frame, or poses not taken from this frame | render 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:
| C | Rust |
|---|---|
example_load_library: qar_library_load, qar_library_init | Library::load_default, Library::load |
example_create_runtime: qar_runtime_create | Library::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_create | session.app_volumes().get_or_create |
qar_peer_subscribe_updates, qar_query_peer_specs | session.subscribe_peer_updates, session.peers() |
example_is_running_target: role QAR_PEER_ROLE_TARGET, presence ONLINE or UNKNOWN | is_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_CPU | Session::create_render_sender |
qar_render_sender_try_begin_frame | RenderSender::try_begin_owned_frame (non-blocking); begin_frame / begin_owned_frame block |
qar_render_frame_info_get_view_pose, _get_view_fov | OwnedFrame::view, one ViewPose |
qar_render_sender_frame_cpu | OwnedFrame::cpu_frame |
qar_render_sender_show_frame | OwnedFrame::show |
qar_render_frame_info_handle_destroy, qar_render_stream_handle_destroy | drop |
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_code | is_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.