- Rust 96.2%
- GLSL 3.5%
- Shell 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| demo | ||
| demo_basic | ||
| demo_xr | ||
| engine | ||
| engine_macros | ||
| pak | ||
| vertex_attributes_derive | ||
| .gitconfig | ||
| .gitignore | ||
| build.sh | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
BracketEngine 3
OpenGL editorless engine with entity component implementation and rapier physics
Setup
Requirements
- Rust via
rustup - C/C++ toolchain +
pkg-config(GLFW, Assimp) - OpenGL 4.6 capable GPU
Linux packages
Arch:
sudo pacman -S base-devel pkgconf glfw-x11 assimp mesa
Debian/Ubuntu:
sudo apt install build-essential pkg-config libglfw3-dev libassimp-dev libgl1-mesa-dev
Build
git clone https://gitlab.bracketproto.com/BracketProto/BracketEngine3.git
cd BracketEngine3
cargo build --release -p bracket_engine
cargo run --release --bin <package>
Packaged builds
build.sh produces a release binary plus assets.pak (engine assets merged with the package assets/) under builds/<target>/<package>/.
./build.sh <package> # linux + windows
./build.sh <package> linux # x86_64-unknown-linux-gnu
./build.sh <package> windows # x86_64-pc-windows-gnu (MinGW)
Architecture
run_app owns the window and frame loop and drives an EngineApp implementor. The runtime hub is Engine:
ec:ECManager(entities, components, behaviours, draw)physics:PhysicsManager(Deref toPhysicsWorld, pose interpolation)sound:SoundManager(rodio)environment:SceneEnvironment(ambient, clear color, fog)xr:XrManager(OpenXR session / tracking; alsoengine.start_xr())
Entity is a generational handle. Prefer ECManager (create_entity, add_component, hierarchy). Use ec.store() / ec.store_mut() (EntityStore) for bulk Transform/EntityInfo access and offline iteration.
Asset paths use utils::asset_path: resolve against assets.pak first, then loose assets/ directories.
Public types are re-exported from bracket_engine and bracket_engine::ec.
Frame loop
Per frame, in order:
- Awake / Start (
awakeon attach;starton first enable) - FixedUpdate (
Behaviour::fixed_update, physics step, rigid-body write-back) - Update
- LateUpdate
- Render (scene resolve / lights, then
Behaviour::render) - OnGUI
Transform/EntityInfoare engine-owned fields on every entity (not Behaviours)- Other built-ins are Behaviours (
#[derive(Behaviour)]); data-only types use an emptyimpl Behaviour - Storage is a per-entity component list (not ECS archetypes)
create_entityalways adds Transform + EntityInfo; default names areEntity,Entity (1), ...
OnDestroy runs on entity removal and shutdown. Removals via remove_entity / remove_self are deferred to the end of the current update phase (fixed_update / update / late_update / on_gui) so mid-callback despawn does not break behaviour iteration. Pending entities are treated as gone for entity_alive, finds, and further behaviour callbacks. Hierarchy destroy runs children before the parent. Dynamic/kinematic body transforms are interpolated for rendering between fixed steps using Time::fixed_alpha. Disable with engine.physics.set_interpolation_enabled(false).
Entities and components
- Hierarchy:
set_parent, parent/child helpers onEntity, world TRS viaECManager - Activation:
set_activefor a subtree; behaviours also have per-Behaviourenable - Spawn:
create_entity(Transform + EntityInfo; default nameEntity/Entity (N)) - Components (on
ECManagerandBehaviourContextfor the current entity):add_component/remove_component/remove_component_atget_component/get_component_mut(first instance; returnsOption)get_components/get_components_mut(all of type via callback&[C]/&mut [C])get_component_in_children/get_components_in_children/get_component_in_parent/get_components_in_parent(include_inactive: bool)has_component/component_count- Multi-instance by default unless
#[disallow_multiple_component]
- Behaviours:
#[derive(Behaviour)](+ optional#[disallow_multiple_component],#[require_component(T, ...)]), implementBehaviour(extendsComponent), attach withadd_component - Low-level:
ec.store()/ec.store_mut()for iteration and direct Transform/EntityInfo access BehaviourContextmirrors the component helpers for the current entity, plusenvironment,sound,physics/physics_mut,move_character,spawn_child,remove_self,set_enabled- Identity on
EntityInfo:name,tag(defaulttags::UNTAGGED),layer(defaultlayers::DEFAULT) - Lookup:
find/find_with_tag/find_with_layerand plurals;find_first/find_allwithEntityLookup;entities_withfor a component type - Scope helpers:
HierarchyScopewithget_component_in_scope/for_each_components_in_scope
Every entity has Transform and EntityInfo.
Layers
Layers are free-form strings. The only builtin name is layers::DEFAULT (registry bit 0). Other names get a bit on first use (up to 32).
- Membership:
EntityInfo.layer/set_entity_layer. Used for render culling and Rapier collision membership. - Collision matrix: global, symmetric.
set_layer_collision(a, b, collide)/get_layer_collision. Default is all pairs collide.ec.set_layer_collisionupdates the matrix and resyncs spawnedRigidBodygroups. - Render culling:
Camera.culling_mask(LayerMask) selects which membership bits a camera draws. Independent of the collision matrix. - Bodies take groups from
interaction_groups_for_layer(membership bit + matrix row). OptionalRigidBody.collision_groups(...)overrides at spawn.
Rendering
- Cameras:
Camera::perspective/Camera::orthographic; multiple per entity;debug: truefor free-fly; present viaec.prepare_render()/Camera::present_window/present_entities/present_view/rendering::frame::end(post from volumes); views viaCamera::view_for_entity/view_for_entity_at - Camera clear settings are per camera:
clear_color(Vec4),clear_color_enabled,clear_depth_enabled - Custom draws:
Behaviour::render+MeshRenderer::enqueue/SkinnedMeshRenderer::enqueue/Skybox::enqueue, orrendering::queue::add_* - Render targets:
RenderTexture/RenderTextureParams; assign with.target(...)orcamera.target. Sample viart.color(0)on materials, orrendering::queue::draw_tofor low-level manual draws without post/UI - Lights:
Light(directional/point/spot). Directional light feeds skybox sun params when present SceneEnvironment: ambient color, clear color, fog near/far/color- Geometry:
MeshRenderer,SkinnedMeshRenderer;ModelInstance::new(path)builds the tree inawake - Reflections:
ReflectionProbe(OnStart / Realtime / custom cubemap /recapture; capture runs inBehaviour::post_renderafter the render queue is built) - Sky:
Skybox(procedural default, or custom shader/material) - Materials:
StandardMaterial(PBR) orMaterial+Shader. Built-in GLSL and named SSBO bindings underengine/assets/shadersandresources/shaders - Primitives:
PrimitiveBuilderunit meshes; scale withTransform - Immediate overlay draw (no entities):
Sprite,BitmapFont - Post:
PostVolume+PostEffect(Bloom,Tonemap, custom);Camera.post(Auto/On/Off); writes toCamera.targetor the bound framebuffer (window / XR eye)
Physics
Rapier3D behind RigidBody + ColliderDesc. Collision/trigger callbacks land on Behaviour as ColliderHit (on_trigger_* / on_collision_*).
Colliders
- Primitives take full sizes:
cube(sx,sy,sz),sphere(diameter),capsule(height, diameter),cylinder(height, diameter). Same convention as scaled unit meshes fromPrimitiveBuilder. - From renderers:
trimesh_from_renderers()(static),convex_from_renderers()(dynamics),aabb_from_renderers() - Friction / restitution live on the collider (also settable via
RigidBodybuilders at spawn)
Bodies
- Kinds: static / kinematic / dynamic
- Common knobs:
gravity_scale,lock_rotations, damping, CCD, sleep - Forces:
apply_force/apply_impulse(and point/torque variants) - Kinematic:
apply_kinematic_pose(pose + next target + transform),set_kinematic_target(next step only) - Visual interpolation between fixed steps (see frame loop)
Queries (PhysicsManager / PhysicsWorld)
raycast/raycast_filtered- Shape casts / overlaps: sphere, cube, capsule (full sizes like
ColliderDesc) - Raw:
query_pipeline/query_pipeline_mut
Character controller
- Capsule kinematic mover. Adds a locked kinematic
RigidBodyif the entity has none. - High level:
ctx.move_characterorCharacterController::move_entity - Low level:
move_by(motion, dt, filter)absolute displacement for the step (no built-in gravity/jump) - Result:
CharacterMoveResult(flags,grounded,velocity,delta);CollisionFlags(NONE,SIDES,ABOVE,BELOW)
Joints: Joint + JointDesc (fixed, revolute, spherical, prismatic, rope, spring; local_anchor1 / local_anchor2, contacts_enabled).
Animation
- Load with
ModelInstance::new(path)on an entity (builds the node/mesh tree) - Skinned:
Animator+SkinnedMeshRenderer; static meshes useMeshRenderer Animator::play/cross_fade/ blend shapes /find_nodeplay_attakes ticks (clip.duration()). Wall-clock length isclip.length_seconds()
Audio
engine.sound (SoundManager): load/play 2D clips. Scene audio uses AudioSource on entities and a single AudioListener (usually on the camera).
UI
- Retained:
Canvas,RectTransform,Image/Text(orDrawablefor customUiDrawable) - Layout resolves during
ec.prepare_render()before present - Immediate:
Sprite/BitmapFont - imgui via
gui::with_ui
Profiler: profiler::set_enabled / profiler::scope (hierarchy filter + selection gizmo when open).
Window, time, input
- Window:
DisplayMode,PresentMode,RenderResolution; optional manual present viaec.prepare_render()/Camera::present_*/rendering::frame::end+swap_buffers Time:delta_time(),time_scale()/set_time_scale(),fixed_delta_time(),fixed_alpha()- Input: keyboard / mouse / gamepad, plus named
axis/actionbindings underbracket_engine::input - Host stats:
System::resources()for CPU/RAM; GL vendor/renderer/version after context init - XR:
XrManageronEngine,XrOrigin.tracking_origin,Camera.stereo_target_eye+mirror_blit_mode/.xr(),Engine::start_xr(). OpenXR loader required at runtime.
Source map
engine/src/app.rs:EngineApp,run_appengine/src/engine.rs:Engineengine/src/window.rs,time.rs,input/: display, clock, inputengine/src/system:SystemResourcesengine/src/ec/ec_manager.rs:ECManagerengine/src/ec/component.rs:Component,Behaviourengine/src/ec/behaviour_context.rs:BehaviourContextengine/src/ec/entity.rs:Entityengine/src/ec/entity_store.rs:EntityStore(low-level)engine/src/ec/layer.rs:LayerMask, layer registry, collision matrixengine/src/ec/components/: built-in components (Transform, renderers, physics, audio, UI, XR, ...)engine/src/ec/collision_events.rs: collision/trigger event dispatch for Behavioursengine/src/physics/:PhysicsManager,PhysicsWorld,ColliderDesc, hitsengine/src/rendering/: environment, queue, sprites, probes, postengine/src/resources/: meshes, materials, shaders, primitives, modelsengine/src/audio/:SoundManagerengine/src/xr/: OpenXR session, stereo frame, trackingengine_macros/:#[derive(Behaviour)](re-exported; also emits storageEngineComponentimpl)
Examples
Starter app
use bracket_engine::ec::{
AudioListener, Camera, Light, ModelInstance, ReflectionProbe, Skybox,
};
use bracket_engine::{run_app, window, Engine, EngineApp};
use nalgebra_glm as glm;
struct Game;
impl EngineApp for Game {
fn start(&mut self, engine: &mut Engine) {
window::set_display_mode(window::DisplayMode::Windowed);
window::set_vsync(true);
engine.environment.ambient_color = glm::vec3(0.05, 0.05, 0.08);
engine.environment.fog_far = 100.0;
let ec = &mut engine.ec;
let sky = ec.create_entity();
ec.add_component(sky, Skybox::new());
let camera = ec.create_entity();
ec.transform_mut(camera)
.unwrap()
.set_position(glm::vec3(0.0, 1.5, 4.0));
let mut cam = Camera::perspective(90.0, 0.01, 1000.0);
cam.debug = true;
ec.add_component(camera, cam);
ec.add_component(camera, AudioListener);
let scene = ec.create_entity();
ec.add_component(scene, ModelInstance::new("path/to/model.gltf"));
let light = ec.create_entity();
ec.transform_mut(light)
.unwrap()
.set_position(glm::vec3(2.0, 3.0, 1.0));
ec.add_component(
light,
Light::point(glm::vec3(1.0, 0.9, 0.8), 5.0, 20.0),
);
let probe = ec.create_entity();
ec.transform_mut(probe)
.unwrap()
.set_position(glm::vec3(0.0, 2.0, 0.0));
ec.add_component(
probe,
ReflectionProbe::on_start(128)
.box_size(glm::vec3(40.0, 20.0, 40.0))
.blend_distance(4.0)
.intensity(1.0),
);
}
}
fn main() {
run_app(Game, "My Game", 1280, 720);
}
Physics and triggers
use bracket_engine::Engine;
use bracket_engine::ec::{
Behaviour, ColliderHit, ECManager, ModelInstance, RigidBody, BehaviourContext,
};
use bracket_engine::Behaviour;
use bracket_engine::physics::{ColliderDesc, QueryFilter};
let ec = &mut Engine::current().ec;
let ground = ec.create_entity();
ec.add_component(ground, ModelInstance::new("path/to/ground.gltf"));
ec.add_component(
ground,
RigidBody::static_body(ColliderDesc::trimesh_from_renderers()).friction(0.5),
);
let ball = ec.create_entity();
ec.transform_mut(ball)
.unwrap()
.set_position(glm::vec3(0.0, 5.0, 0.0));
ec.add_component(
ball,
RigidBody::dynamic(ColliderDesc::sphere(1.0).friction(0.1)),
);
#[derive(Behaviour)]
struct Volume;
impl Behaviour for Volume {
fn on_trigger_enter(&mut self, _ctx: &mut BehaviourContext<'_>, hit: ColliderHit) {
let _ = hit.other;
}
}
let volume = ec.create_entity();
ec.add_component(
volume,
RigidBody::static_body(ColliderDesc::sphere(2.0).sensor(true)),
);
ec.add_component(volume, Volume);
let hit = Engine::current().physics.raycast_filtered(
glm::vec3(0.0, 10.0, 0.0),
glm::vec3(0.0, -1.0, 0.0),
100.0,
QueryFilter::default(),
);
let _ = hit;
Character movement
use bracket_engine::ec::{CharacterController, CollisionFlags};
use bracket_engine::physics::QueryFilter;
ec.set_layer_collision("Player", "Ball", false);
let player = ec.create_entity();
ec.set_entity_layer(player, "Player");
ec.add_component(
player,
CharacterController::new()
.height(1.7)
.radius(0.3)
.slope_limit(50.0),
);
// In Behaviour::update; move uses body groups from layer + collision matrix:
let Some(moved) = ctx.move_character(velocity * dt, dt, QueryFilter::default()) else {
return;
};
if moved.grounded {
velocity.y = velocity.y.min(0.0);
}
if moved.flags.intersects(CollisionFlags::ABOVE) {
velocity.y = velocity.y.min(0.0);
}
let _delta = moved.delta;
// Outside behaviours: CharacterController::move_entity(entity, motion, dt, filter)
Primitives
use std::rc::Rc;
use bracket_engine::ec::{MeshRenderer, RigidBody};
use bracket_engine::physics::ColliderDesc;
use bracket_engine::resources::{PrimitiveBuilder, StandardMaterial};
let mesh = Rc::new(PrimitiveBuilder::cube().build());
let mat = StandardMaterial::new();
let entity = ec.create_entity();
ec.transform_mut(entity)
.unwrap()
.set_scale(glm::vec3(1.0, 1.0, 1.0));
ec.add_component(entity, MeshRenderer::mesh(mesh, mat));
ec.add_component(
entity,
RigidBody::static_body(ColliderDesc::cube(1.0, 1.0, 1.0)),
);
Animation
use bracket_engine::Engine;
use bracket_engine::ec::{Animator, ECManager, ModelInstance};
let ec = &mut Engine::current().ec;
let entity = ec.create_entity();
ec.add_component(entity, ModelInstance::new("character.fbx"));
ec.transform_mut(entity)
.unwrap()
.set_scale(glm::vec3(0.01, 0.01, 0.01));
if let Some(mut animator) = ec.get_component_mut::<Animator>(entity) {
animator.play("Idle");
animator.play_at("Idle", 30.0);
animator.set_speed(1.0);
animator.set_normalized_time(0.0);
animator.cross_fade("Walk", 0.25);
let _hand = animator.find_node("Hand_R");
animator.set_blend_shape_weight("Smile", 1.0);
}
Materials and shaders
Material / StandardMaterial for PBR. Custom shaders: Shader + Material::new. Includes and buffer names live under engine/assets/shaders and resources/shaders.
Custom shader:
use std::{cell::RefCell, rc::Rc};
use bracket_engine::{
rendering::RenderType,
resources::{shaders::Shader, Material},
utils,
};
let vert = utils::asset_path("shaders/required/standard/standard.vert");
let frag = utils::asset_path("shaders/required/standard/standard.frag");
let shader = Rc::new(RefCell::new(Shader::new(&vert, &frag)));
let mut material = Material::new(shader, RenderType::Opaque as u32);
material.set_vec4("AlbedoColor", &glm::vec4(1.0, 1.0, 1.0, 1.0));
Custom SSBO binding:
layout(std430, binding = MyBindingName) readonly buffer MyBindingName { mat4 items[]; };
use bracket_engine::resources::shaders::{binding_manager, Ssbo};
let binding = binding_manager::binding("MyBindingName");
if binding >= 0 {
ssbo.update(&data, gl::DYNAMIC_DRAW);
ssbo.bind(binding as u32);
}
Parenting, tags, layers
use bracket_engine::Engine;
use bracket_engine::ec::{layers, ECManager, LayerMask};
let ec = &mut Engine::current().ec;
let parent = ec.create_entity();
let child = ec.create_entity();
ec.set_parent(child, Some(parent));
ec.set_entity_name(child, "Child");
ec.set_entity_tag(child, "Enemy");
ec.set_entity_layer(child, layers::DEFAULT);
ec.set_layer_collision("Enemy", "Player", false);
// camera.culling_mask = LayerMask::from_layer(layers::DEFAULT) | LayerMask::from_layer("Water");
let _ = ec.find("Child");
let _ = ec.find_with_tag("Enemy");
ec.set_parent(child, None);
ec.remove_entity(parent);
Overlay HUD
use bracket_engine::ec::{Canvas, RectTransform, Text};
use bracket_engine::rendering::FontAsset;
use bracket_engine::utils;
let font = FontAsset::load(&utils::asset_path(
"shaders/required/font/monospace.png",
));
let canvas = ec.create_entity();
ec.add_component(canvas, RectTransform::fullscreen());
ec.add_component(
canvas,
Canvas::overlay()
.reference(1920.0, 1080.0)
.match_width_or_height(0.5),
);
let label = ec.create_entity();
ec.add_component(
label,
RectTransform::top_left(glm::vec2(16.0, -16.0), glm::vec2(600.0, 48.0)),
);
ec.add_component(label, Text::new("Hello", font).font_size(1.0));
ec.set_parent(label, Some(canvas));
Audio
use bracket_engine::Engine;
use bracket_engine::ec::{AudioSource, ECManager};
let sound = &mut Engine::current().sound;
sound.load_sound("sfx/click.wav");
sound.play_2d_sound("sfx/click.wav", 1.0, 0.0);
let ec = &mut Engine::current().ec;
let source = ec.create_entity();
ec.add_component(
source,
AudioSource::new("sfx/footstep.wav")
.spatial(25.0)
.play_on_start(true),
);
Custom Behaviour
use bracket_engine::ec::{Behaviour, BehaviourContext};
use bracket_engine::Behaviour;
#[derive(Behaviour)]
struct Spinner {
speed: f32,
}
impl Behaviour for Spinner {
fn update(&mut self, ctx: &mut BehaviourContext<'_>, dt: f32) {
let Some(mut transform) = ctx.transform_mut() else {
return;
};
let yaw = glm::quat_angle_axis(self.speed * dt, &glm::vec3(0.0, 1.0, 0.0));
transform.set_rotation(yaw * transform.rotation());
}
}
let ec = &mut Engine::current().ec;
let entity = ec.create_entity();
ec.add_component(entity, Spinner {
speed: 90.0_f32.to_radians(),
});
Input and OnGUI
use bracket_engine::input;
let mut velocity = glm::vec3(0.0, 0.0, 0.0);
velocity.x += input::axis("Horizontal");
velocity.z += input::axis("Vertical");
if input::action_pressed("Jump") {
// jump
}
input::register_action(
input::ActionBinding::new("Interact")
.with_key(input::Key::E)
.with_gamepad_button(input::GamepadButton::ButtonX),
);
Device-level helpers remain (key_down, gamepad_stick, button_down, etc.).
use bracket_engine::ec::{Behaviour, BehaviourContext};
use bracket_engine::Behaviour;
#[derive(Behaviour)]
struct MyPanel {
speed: f32,
}
impl Behaviour for MyPanel {
fn on_gui(&mut self, _ctx: &mut BehaviourContext<'_>) {
bracket_engine::gui::with_ui(|ui| {
ui.window("Debug").build(|| {
ui.slider("Speed", 0.0, 10.0, &mut self.speed);
});
});
}
}
System resources
use bracket_engine::System;
let stats = System::resources();
if stats.cpu_ready {
let _cpu = stats.cpu_usage; // 0..=100
}
if stats.ram_ready {
let _ram_mb = stats.ram_used_mb;
let _ram_total_mb = stats.ram_total_mb;
}
let _gpu = (&stats.gpu_vendor, &stats.gpu_renderer, &stats.gpu_version);
Custom present
use bracket_engine::Engine;
use bracket_engine::ec::Camera;
use bracket_engine::window::{self, PresentMode};
window::set_present_mode(PresentMode::Manual);
let ec = &mut Engine::current().ec;
let other_camera = ec.create_entity();
ec.transform_mut(other_camera)
.unwrap()
.set_position(glm::vec3(4.0, 1.5, 0.0));
ec.add_component(other_camera, Camera::perspective(60.0, 0.01, 1000.0));
let primary = Camera::primary_entity(ec.store());
let cameras: Vec<_> = primary
.into_iter()
.chain(std::iter::once(other_camera))
.collect();
ec.prepare_render();
Camera::present_entities(ec, &cameras);
bracket_engine::rendering::frame::end();
window::swap_buffers();
Post-processing
use bracket_engine::ec::{Camera, PostMode, PostVolume};
use bracket_engine::rendering::{Bloom, PostEffect, PostEffectContext, Tonemap};
ec.add_component(
ec.create_entity(),
PostVolume::global()
.effect(Bloom::default().threshold(1.0).intensity(0.6).levels(5))
.effect(Tonemap::aces()),
);
ec.add_component(
ec.create_entity(),
PostVolume::local(glm::vec3(20.0, 10.0, 20.0))
.blend_distance(4.0)
.priority(10)
.effect(Bloom::default().intensity(0.9).levels(5)),
);
let cam = Camera::perspective(60.0, 0.01, 1000.0)
.post(PostMode::Auto);
struct Grayscale;
impl PostEffect for Grayscale {
fn name(&self) -> &str {
"Grayscale"
}
fn apply(&mut self, ctx: &mut PostEffectContext<'_>) {
let _ = ctx;
}
}
Volumes resolve by priority and local box blend. Bloom knobs: threshold, soft_knee, intensity, levels (2-8), scatter, clamp.
RenderTexture
use std::rc::Rc;
use bracket_engine::ec::Camera;
use bracket_engine::rendering::{RenderTexture, RenderTextureParams};
use bracket_engine::resources::{ColorFormat, DepthFormat};
let rt = RenderTexture::shared(512, 512);
let mut cam = Camera::perspective(60.0, 0.01, 1000.0).target(Rc::clone(&rt));
cam.priority = 10;
ec.add_component(portal_camera, cam);
material.set_texture("Albedo", rt.borrow().color(0));
let hdr = RenderTexture::with_params(
RenderTextureParams::new(1024, 1024)
.with_color_format(ColorFormat::RGBA16F)
.with_depth_format(Some(DepthFormat::Depth32FStencil8))
.with_mipmaps(true),
);
ec.prepare_render();
if let Some(view) = Camera::view_for_entity(ec.store(), portal_camera) {
bracket_engine::rendering::queue::draw_to(ec.store(), &hdr, &view, true);
}
bracket_engine::rendering::frame::end();
XR
OpenXR loader required at runtime. Create an XrOrigin (with tracking_origin) before starting. engine.start_xr() reads tracking from that component; otherwise use set_tracking_origin. start() / stop() can run at any time: the session waits until views are valid (is_running()) or times out. stop() or session loss restores the previous present mode. Also is_starting() / last_error().
.xr() sets stereo_target_eye to Both and mirror_blit_mode to LeftEye (desktop preview blits the left eye buffer). Override with .stereo_target_eye(...) / .mirror_blit_mode(...). A second window camera (StereoTargetEye::None) is a separate scene render. Window cameras still go through the normal present stack (priority, clear settings, targets, post). Mirror blit does not draw UI.
use bracket_engine::ec::{Camera, PostMode, XrController, XrHand, XrOrigin};
use bracket_engine::{Engine, TrackingOrigin};
let origin = ec.create_entity();
ec.add_component(
origin,
XrOrigin::new().tracking_origin(TrackingOrigin::Floor),
);
let head = ec.create_entity();
ec.set_parent(head, Some(origin));
ec.add_component(
head,
Camera::perspective(90.0, 0.01, 1000.0)
.xr()
.post(PostMode::Off),
);
engine.start_xr().expect("OpenXR session");
let left = ec.create_entity();
ec.set_parent(left, Some(origin));
ec.add_component(left, XrController::left()); // or XrController::new(XrHand::Left)
start() / start_xr() suggest common interaction profiles (khr/simple_controller plus microsoft/htc/oculus/valve). Override with suggest_bindings before start.