D3D11 Rendering
What you build
A Windows source application that renders straight into the render sender's shared D3D11 textures, with no CPU copy. It finds the first running target app, creates a D3D11.1 device on the session's adapter, chains that device into a render sender, and clears each eye's color view to a pulsing color: the left eye red, the right eye blue.
What you will learn:
- how to create the device on the session's adapter;
- how to hand the device to a sender with
qaros::d3d11::D3D11StreamParamsand what its fields decide; - the D3D11 frame cycle: the frame record, the shared textures it lends, and the status codes that keep it going;
- what the sender keeps alive for you, and the one rule the wrapper cannot check.
The program is examples/src/bin/d3d11_render_sender.rs in the Rust binding, a port of the C example d3d11_render_sender.c on the safe API (feature d3d11 of qaros). This page includes its code; the file is the authority. Read the CPU Rendering tutorial first: this one assumes its concepts.
Before you start
- Platform. Windows with a D3D11.1-capable GPU. On any other system the example prints that it requires Windows and exits with 0.
- 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. - A target. At least one target app in the session (the Visualizer, a headset). Joining as a local app starts the Hub when it is not running.
- Joining. The example joins the Hub as a local app, as the CPU example does: click Allow in the QAROS approval prompt on the first run of the executable; nothing is stored, and later runs join without a prompt (Local app).1
- The latest stable Rust. The D3D11 types come from the
windowscrate 0.62, a Windows-only dependency ofqaros(featured3d11) and of the examples.
In your own application, add qaros with the d3d11 feature to the [dependencies] section of your crate's Cargo.toml. The crates are not on crates.io, so the dependency names the QAROS repository, and Cargo finds the crate inside it:
[dependencies]
qaros = { git = "https://github.com/Quaternar/qaros", features = ["d3d11"] }
With a clone of the repository, point a path at its qar-streaming-rust/crates/qaros folder instead, relative to your Cargo.toml:
[dependencies]
qaros = { path = "../qaros/qar-streaming-rust/crates/qaros", features = ["d3d11"] }
The device and context you pass are windows crate 0.62 types, so your own D3D11 code depends on windows 0.62 as well.
Run it
From the qar-streaming-rust folder of the repository:
# Join the Hub as a local app: click Allow in QAROS on the first run.
cargo run --release -p qaros-examples --bin d3d11_render_sender -- --frames=300
The arguments are those of the CPU example: --frames=N (300 by default) and --qaros-library=PATH.
The program prints the runtime it loaded, qar_runtime_create: SUCCESS, the local-app join, Waiting for a target app in the peer list ..., qar_render_sender_create: SUCCESS, Rendering for target <target name> (peer <peer id>), and Submitted <N> frames. at the end. On the target, the "D3D11 Tutorial Volume" app volume shows the two pulsing eyes.
Against the stub on Windows (QAROS_DLL naming target\debug\qaros_stub.dll, and any code as the first argument, as in CPU Rendering), the example follows the same steps but prints qaros-stub lends no D3D11 textures: submitting the frames without drawing. and draws nothing: the stub's D3D11 emulation lends placeholder addresses, not textures, and Direct3D would crash on them. The stub run stores d3d11_render_sender.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.
Walkthrough
1. Windows only
#[cfg(windows)]
fn main() -> std::process::ExitCode {
qaros_examples::report(d3d11::run())
}
#[cfg(not(windows))]
fn main() {
println!("d3d11_render_sender requires Windows (QAR_ENABLE_D3D11).");
}
The D3D11 code lives in a #[cfg(windows)] module. Elsewhere main prints one line. The d3d11 feature of qaros follows the header the same way: it compiles where the bindings carry the D3D11 types (Windows, and on macOS only for the tests against qaros-stub) and to nothing on Linux.
2. The program, top to bottom
pub fn run() -> ExampleResult {
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(ID_FILE),
DISPLAY_NAME,
)?;
let mut volume_config = AppVolumeConfig::new("d3d11-volume.examples.qaros");
volume_config.display_name = Some("D3D11 Tutorial Volume".to_string());
volume_config.pose = Pose::from_position(Vec3::new(0.0, 0.0, -1.0));
let volume = session.app_volumes().get_or_create(&volume_config)?;
println!("Waiting for a target app in the peer list ...");
let target = wait_for_target(&session)?;
let (device, context) = create_d3d11(session.graphics_adapter()?)?;
let mut config = RenderSenderConfig::new(target.id);
config.layout = FrameLayout::SideBySide;
config.views = FRAME_VIEWS.to_vec();
config.app_volume = Some(volume);
let mut sender = create_sender(&session, &config, &device, &context)?;
println!(
"Rendering for target {} (peer {})",
target.display_name, target.id
);
// qaros-stub lends placeholder addresses instead of D3D11 textures; Direct3D crashes on
// them, so against the stub the frames go out undrawn.
let draws = library.runtime_info()?.flavor != "stub";
if !draws {
println!("qaros-stub lends no D3D11 textures: submitting the frames without drawing.");
}
let mut frames_shown = 0_usize;
for frame_index in 0.. {
if frames_shown >= frame_limit {
break;
}
match render_frame(&mut sender, &device, &context, frame_index, draws) {
Ok(shown) => frames_shown += usize::from(shown),
Err(error) => {
println!("{error}");
break;
}
}
}
println!("Submitted {frames_shown} frames.");
// The sender goes first and releases its own references on the device and context;
// then this function's, the session, the runtime and the library: the C sample's
// teardown order.
drop(sender);
drop(context);
drop(device);
drop(session);
drop(runtime);
drop(library);
Ok(())
}
The session comes from the same helper as the CPU example (join_session in examples/src/lib.rs: rejoin, else the pairing code, else a local-app join), and the app volume from session.app_volumes().get_or_create with the common name d3d11-volume.examples.qaros. The draws check matters only against qaros-stub (runtime flavor stub); against QAROS every frame is drawn, and your own application needs no such check.
3. Find the target
Like the CPU example, this one finds its target in the peer list, not through a render request (Rendering Streams). It serves one target, the first running target app, and polls the list every 50 ms until there is one:
/// The first running target app the session lists, polled until one is there.
fn wait_for_target(session: &Session) -> qaros::Result<PeerInfo> {
loop {
if let Some(target) = session.peers()?.into_iter().find(is_running_target) {
return Ok(target);
}
std::thread::sleep(TARGET_POLL_INTERVAL);
}
}
is_running_target is the CPU example's: a peer for which qar_peer_spec_is_target_app is true and whose app state is AppState::Running. The header deprecates the app state for qar_peer_spec_get_presence, but computes it from the presence: for every current peer, running means online, or unknown on a Hub that has not reconciled the peer, the targets the guide renders for (Working with Peers). The safe API reads the app state; the presence getter is raw only, through library.raw_api() with the session's as_raw().
4. Create the device on the session's adapter
The sender's shared textures live on the session's GPU. Create your device on the same adapter, so frames need no copy between GPUs (Choosing the GPU). Session::graphics_adapter returns the session's adapter; the example finds the DXGI adapter with that LUID and creates a D3D11.1 device on it. When the session names no adapter, it uses the default hardware adapter.
/// The DXGI adapter whose LUID is the session's, compared byte for byte in memory order (the
/// order `GraphicsAdapter::Luid` holds).
fn adapter_with_luid(luid: [u8; 8]) -> ExampleResult<IDXGIAdapter> {
// SAFETY: plain factory creation.
let factory: IDXGIFactory1 = unsafe { CreateDXGIFactory1() }?;
for index in 0.. {
// SAFETY: live factory; enumeration ends with DXGI_ERROR_NOT_FOUND.
let Ok(adapter) = (unsafe { factory.EnumAdapters1(index) }) else {
break;
};
// SAFETY: live adapter.
let adapter_luid = unsafe { adapter.GetDesc1() }?.AdapterLuid;
let mut adapter_bytes = [0_u8; 8];
adapter_bytes[..4].copy_from_slice(&adapter_luid.LowPart.to_le_bytes());
adapter_bytes[4..].copy_from_slice(&adapter_luid.HighPart.to_le_bytes());
if adapter_bytes == luid {
return Ok(adapter.cast()?);
}
}
Err(format!(
"the session's adapter (LUID {}) is not among this machine's DXGI adapters",
GraphicsAdapter::Luid(luid)
)
.into())
}
/// A D3D11.1 device and immediate context on the session's adapter when it names one by
/// LUID, else on the default hardware adapter (the C sample's `create_d3d11`).
fn create_d3d11(
session_adapter: Option<GraphicsAdapter>,
) -> ExampleResult<(ID3D11Device1, ID3D11DeviceContext1)> {
let adapter = match session_adapter {
Some(GraphicsAdapter::Luid(luid)) => Some(adapter_with_luid(luid)?),
Some(GraphicsAdapter::Uuid(_)) | None => None,
};
let driver_type: D3D_DRIVER_TYPE = if adapter.is_some() {
D3D_DRIVER_TYPE_UNKNOWN
} else {
D3D_DRIVER_TYPE_HARDWARE
};
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
// SAFETY: valid out-pointers; the adapter, when given, is live.
unsafe {
D3D11CreateDevice(
adapter.as_ref(),
driver_type,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
Some(&[D3D_FEATURE_LEVEL_11_1]),
D3D11_SDK_VERSION,
Some(&raw mut device),
None,
Some(&raw mut context),
)
}
.map_err(|error| format!("D3D11CreateDevice failed ({error})"))?;
let (Some(device), Some(context)) = (device, context) else {
return Err("D3D11CreateDevice returned no device or context".into());
};
let device1 = device
.cast::<ID3D11Device1>()
.map_err(|_| "D3D11.1 interfaces not available")?;
let context1 = context
.cast::<ID3D11DeviceContext1>()
.map_err(|_| "D3D11.1 interfaces not available")?;
Ok((device1, context1))
}
D3D11CreateDevice takes D3D_DRIVER_TYPE_UNKNOWN when you pass an adapter, and D3D_DRIVER_TYPE_HARDWARE when you do not. The sender needs the ID3D11Device1 and ID3D11DeviceContext1 interfaces, so the device is queried for them.
5. Hand the device to the sender
/// Color of both eyes side by side in texture 0, depth side by side in texture 1.
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,
},
];
fn create_sender(
session: &Session,
config: &RenderSenderConfig,
device: &ID3D11Device1,
context: &ID3D11DeviceContext1,
) -> qaros::Result<RenderSender> {
let mut d3d11 = D3D11StreamParams::new(device, context);
d3d11.color_bind_flags = (D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE)
.0
.cast_unsigned();
// QAROS creates the depth textures as DXGI_FORMAT_D32_FLOAT, a typed depth format that
// D3D11 cannot bind as a shader resource: with D3D11_BIND_SHADER_RESOURCE added,
// CreateTexture2D, and with it sender creation, fails with E_INVALIDARG.
d3d11.depth_bind_flags = D3D11_BIND_DEPTH_STENCIL.0.cast_unsigned();
d3d11.acquire_keyed_mutex_sync = true;
// SAFETY: this example uses the immediate context on this thread only, the thread that
// drives the session and the sender.
let sender = unsafe {
session.create_d3d11_render_sender(config, d3d11, &BlockingOptions::default())
}?;
println!("qar_render_sender_create: SUCCESS");
Ok(sender)
}
D3D11StreamParams::newstarts from the header's defaults (qar_stream_params_d3d11_default) and takes a reference on the device and the context. The sender keeps those references until it is destroyed, so the device cannot go away under it, whatever order your own values drop in.color_bind_flagsanddepth_bind_flagsdecide how you can use the shared textures. Render target and shader resource for color let you draw into it; depth stencil for depth lets you use it as your depth buffer. Leave shader resource off the depth flags: QAROS creates the depth textures asDXGI_FORMAT_D32_FLOAT, which D3D11 cannot bind as a shader resource, and sender creation then fails withDxgiError"The parameter is incorrect." They areD3D11_BIND_FLAGbits asu32, as in the header.acquire_keyed_mutex_sync = truelets the library acquire and release each texture's keyed mutex around the frame cycle. Set it to false only when you run the keyed-mutex chain yourself; the frame then lists each texture's keyed mutex with its acquire and release keys (D3D11Frame::keyed_mutexes, and The D3D11 path).create_d3d11_render_senderchains the parameters asQarStreamParamsD3D11and selectsQAR_GRAPHICS_API_D3D11. It returns the sameRenderSendertype as the CPU path;start_d3d11_render_senderis the form that does not block.- The frame views are declared as in the CPU tutorial: both eyes' color side by side in texture 0, depth in texture 1.
The creation is unsafe for one reason the wrapper cannot check: QAROS uses the immediate context you give it, the header does not say when or on which thread, and a D3D11 immediate context is not thread-safe. Use it only on the thread that drives the session and its senders, as this example does, or turn on the device's multithread protection.
6. Begin, clear, show
/// One frame: begin, clear the color views when `draws`, show. False when the stream is
/// waiting and no frame was shown.
fn render_frame(
sender: &mut RenderSender,
device: &ID3D11Device1,
context: &ID3D11DeviceContext1,
frame_index: usize,
draws: bool,
) -> qaros::Result<bool> {
let frame = match sender.begin_frame(&BlockingOptions::default()) {
Ok(frame) => frame,
Err(error) if is_waiting(&error) => return Ok(false),
Err(error) => return Err(error),
};
match frame.d3d11_frame() {
Ok(d3d11_frame) if draws => {
clear_color_views(device, context, &d3d11_frame, frame_index);
}
Ok(_) => {}
Err(error) => println!("{error}"),
}
// Show closes the frame cycle that begin-frame opened.
let shown = frame.show(&ShowParams {
near: NEAR_PLANE,
far: FAR_PLANE,
depth_scale: 1.0,
});
match shown {
Ok(()) => Ok(true),
Err(error) if is_waiting(&error) => Ok(false),
Err(error) => Err(error),
}
}
begin_frame blocks until the target wants a frame. Then d3d11_frame reads the frame record (qar_render_sender_frame_d3d11): the views and the shared textures they lie on. show closes the frame cycle that begin opened, so the example shows even when reading the record failed. The frame info is destroyed after show, as in the C example; Frame does that when it is consumed.
The example does not read the view poses: a clear needs none. A real renderer reads the pose and field of view of each view with frame.view(index) (the index D3D11Frame::view_index finds for an eye and color or depth) and draws the scene into the view's rectangle, as the CPU and Vulkan examples do.
The clear itself:
/// The C sample's `clear_color_views`: each color view cleared to an animated color, the
/// left eye red-ish, the right eye blue-ish, both pulsing.
#[allow(clippy::cast_precision_loss)]
fn clear_color_views(
device: &ID3D11Device1,
context: &ID3D11DeviceContext1,
frame: &D3D11Frame<'_>,
frame_index: usize,
) {
let pulse = (frame_index % PULSE_FRAMES) as f32 / PULSE_FRAMES as f32;
for view in frame.views() {
if view.kind != ViewKind::Color {
continue;
}
let Some(shared_texture) = frame.texture(view.texture_index as usize) else {
continue;
};
// Lent by the sender for this frame: no AddRef, no Release.
let Some(texture) = shared_texture.texture() else {
continue;
};
let mut render_target: Option<ID3D11RenderTargetView> = None;
// SAFETY: live device and texture; valid out-pointer.
if let Err(error) = unsafe {
device.CreateRenderTargetView(texture, None, Some(&raw mut render_target))
} {
eprintln!("CreateRenderTargetView failed ({error})");
continue;
}
let Some(render_target) = render_target else {
continue;
};
let right_eye = view.eye == Eye::Right;
let color = [
if right_eye { 0.1 } else { pulse },
0.2,
if right_eye { pulse } else { 0.1 },
1.0,
];
let (Ok(left), Ok(top), Ok(right), Ok(bottom)) = (
i32::try_from(view.start_x),
i32::try_from(view.start_y),
i32::try_from(view.end_x),
i32::try_from(view.end_y),
) else {
continue;
};
let rectangle = RECT {
left,
top,
right,
bottom,
};
// SAFETY: live context and render-target view; the rectangle lies on the texture.
unsafe { context.ClearView(&render_target, &color, Some(&[rectangle])) };
}
}
The textures belong to the sender. D3D11Texture::texture lends each one for the borrow of the frame, with no AddRef or Release, so it cannot be used after show; clone the interface if you need to keep one. The example creates a render-target view on the texture and clears the view's rectangle with ID3D11DeviceContext1::ClearView. Depth views are left as the sender hands them out, as in the C example. A real renderer writes depth too: QAROS uses it for occlusion and reprojection.
Status handling, by is_waiting:
Status (qaros::StatusCode) | C name | What the example does |
|---|---|---|
MEDIA_STREAMING_STREAM_IS_PAUSED, _IS_RECONNECTING, _NO_ACTIVE_TRANSFERS | QAR_STATUS_MEDIA_STREAMING_... | skips the frame and begins the next one; begin-frame holds a reconnecting or transfer-less stream about 16 ms, so the loop does not spin |
RENDERING_APP_VOLUME_UNRESOLVED | QAR_STATUS_RENDERING_PRODUCER_APP_VOLUME_UNRESOLVED | the same; begin-frame holds about 16 ms |
RENDERING_STREAM_IS_CLOSED, or any other error | QAR_STATUS_RENDERING_PRODUCER_STREAM_IS_CLOSED, ... | prints the error and ends the loop |
With one target there is nothing to recreate, so a closed stream ends the program. It still exits with 0. To serve a target again after STREAM_IS_CLOSED, drop the sender and create a new one while the target is still in the peer list (Rendering Streams).
Ownership and teardown
| Value | Owns | Keeps alive |
|---|---|---|
Library | qar_library_init for the process | |
Runtime | the QarRuntime | the library |
Session | the QarSession | the runtime |
RenderSender | the QarRenderSender | the session, and a reference on the D3D11 device and context |
Frame | the frame info, until shown or dropped | borrows the sender |
D3D11Frame | a copy of the frame record | borrows the frame; lends the textures |
run drops in the C example's order, explicitly: the sender, which releases its device references after the sender is destroyed; the example's own context and device; the session; the runtime (released with qar_runtime_handle_destroy, as the C samples release it); the library. The ownership above would keep the order whatever the drop order: a sender keeps its session, a session its runtime, and the device lives as long as the sender holds its reference. When the sender holds the last reference to its session, the session goes before the device references.
On the Vulkan path your device must outlive the session (Vulkan Rendering). A D3D11 device is reference counted, and the sender's references keep it alive until the sender is gone.
Troubleshooting
| Symptom | Cause | What to do |
|---|---|---|
d3d11_render_sender requires Windows (QAR_ENABLE_D3D11). | built for another system | run it on Windows |
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 or expired code | read the current code and run again at once |
stays at Waiting for a target app in the peer list ... | no running target app in the session | open the Visualizer or start a headset |
the session's adapter (LUID ...) is not among this machine's DXGI adapters | the session names an adapter DXGI does not list | check the GPU driver; report it with the LUID |
D3D11CreateDevice failed (...) or D3D11.1 interfaces not available | the adapter does not support D3D11.1 | use a GPU that does |
qar_render_sender_create failed with ... | the sender could not be created for this target | read the message; check the target is still running |
qar_render_sender_begin_frame failed with ..., then Submitted <N> frames. | the stream closed for good (QAR_STATUS_RENDERING_PRODUCER_STREAM_IS_CLOSED, possibly wrapped in QAR_STATUS_RENDERING_PRODUCER_UNABLE_TO_DO_BEGIN_FRAME) or another failure | read the message; start the example again |
From C
The C tutorial is D3D11 Rendering. The two programs do the same thing: join with a stored onboarding id, a pairing code or as a local app, take the first running target app from the peer list, create the device on the session's adapter, bind depth as depth stencil only, declare color and depth of both eyes, and skip the frame on paused, reconnecting, transfer-less and unresolved-volume statuses. The Rust example makes the same C calls in the same order through the safe API, except where the last rows say otherwise:
| C example | Rust example |
|---|---|
example_join_session | join_session |
example_is_running_target: role QAR_PEER_ROLE_TARGET, presence ONLINE or UNKNOWN | is_running_target: the app state AppState::Running, computed from the presence, so the same targets |
qar_session_get_graphics_device_id, then the DXGI adapter with that LUID | Session::graphics_adapter |
QarStreamParamsD3D11 filled by hand | D3D11StreamParams::new, which also keeps the device and context alive for the sender |
qar_render_sender_frame_d3d11 into a QarVideoFrameD3D11 | Frame::d3d11_frame, a copy of the record that lends the textures for the frame |
example_is_waiting | is_waiting |
log_result | qaros::Error, with the function, the status and the message |
qar_*_handle_destroy on every exit path | handles released on drop, with explicit drops for the order |
qar_runtime_handle_destroy | dropping the Runtime |
| D3D11 through COBJMACROS | D3D11 through the windows crate |
qar_render_sender_create with a 10 s timeout token | create_d3d11_render_sender with BlockingOptions::default(), no timeout |
qar_render_sender_try_begin_frame, polled with a 2 ms sleep | begin_frame, which blocks until the target wants a frame |
| exit code 1 when a frame failure ends the loop | exit code 0 |
Next
- Vulkan Rendering: the other GPU path, with the same device-on-the-session's-adapter rule.
- Rendering Streams: the D3D11 path in the guide.
- API Conventions: handle ownership and the teardown order on the C API.