Sensors
Implementation status: Accelerometer and Gyroscope use a real sensor whenever the selected CNA platform reports one (the SDL3 sensor API on the default platform); Compass and Motion are real on Android and report NotSupported elsewhere; VibrateController uses SDL haptics. This snapshot has 37 Devices/Devices-EXT test sources with 530 statically discoverable GoogleTest-family definitions (alpha.1: 36 and 525), including injectable backends for headless CI. Hardware-dependent tests self-skip when the required device is absent.
Build flag: the Microsoft::Devices sensors documented here are built in every configuration: the devices module is added unconditionally and is part of the CNA umbrella target. The CNA_DEVICES CMake option, which defaults to OFF, gates only the separate CNA-specific device extensions (CNA::Devices in the devices-ext module: clipboard, dialogs, camera and so on); configure with -DCNA_DEVICES=ON to compile those. The path-filtered devices-tests.yml workflow builds both Microsoft::Devices and CNA::Devices tests with UBSan on relevant pushes and pull requests and runs the suites named in its two test filters (which miss seven Microsoft::Devices suites and, by naming renamed dialog suites, four CNA::Devices ones), and is also manually dispatchable; that is desktop/headless evidence, not a physical-sensor laboratory.
Overview
XNA 4.0 introduced these sensor classes as part of its Windows Phone support, providing a straightforward API for tilt, rotation, heading and shake detection. CNA maps them onto two platform integrations: the selected platform's sensor service (the SDL sensor subsystem on the default SDL3 platform) for acceleration and angular velocity, and, on Android, the NDK <android/sensor.h> interface for magnetometer-based heading and fused orientation. This is separate from the selected graphics renderer.
The sensors are instances, not static state snapshots. Create a sensor object, call Start(), and read getCurrentValueProperty() once per frame (or subscribe to CurrentValueChanged / ReadingChanged if you prefer events); call Stop() when you are done. The static getIsSupportedProperty() tells you beforehand whether the selected platform has the sensor, and getStateProperty() reports a SensorState (NotSupported, Ready, Initializing, NoData, NoPermissions, Disabled). There is no AccelerometerState type and no GetState() on the sensors.
Where a sensor genuinely has no hardware or platform backing, the API is still present and callable — it reports NotSupported rather than fabricating data, so code that guards on getIsSupportedProperty() degrades gracefully without preprocessor guards. Microsoft::Devices::Environment::getDeviceTypeProperty() (new in this snapshot) returns Device on mobile targets and Emulator elsewhere, matching XNA's Microsoft.Devices.Environment.DeviceType.
Sensor classes at a glance
| Class | Backend | Where it is real | Status |
|---|---|---|---|
Accelerometer |
Platform sensor service (SDL3 sensor API) | Android and desktop (Linux, Windows, macOS) wherever the platform reports an accelerometer | Functional |
Gyroscope |
Platform sensor service (SDL3 sensor API) | Android and desktop (Linux, Windows, macOS) wherever the platform reports a gyroscope | Functional |
Compass |
Android NDK <android/sensor.h> |
Android only; NotSupported elsewhere |
Android-only |
Motion |
Android NDK fusion of 5 sensors | Android only; NotSupported elsewhere |
Android-only |
VibrateController |
SDL haptics | Any device with an SDL haptic device (gamepads excluded by design) | Functional |
The Compass/Motion restriction is a platform limitation, not an unimplemented stub: there is a complete Android implementation, and no equivalent magnetometer/fusion source is wired on other platforms, so those platforms answer honestly instead of returning zeros.
Accelerometer class
Accelerometer is an ordinary class: construct one, Start() it, read it, Stop() it. It derives from SensorBase<AccelerometerReading>, and up to ten instances may exist per application. Its members:
| Member | Type | Description |
|---|---|---|
Accelerometer::getIsSupportedProperty() |
bool (static) |
true when the current target is Android, iOS or desktop and the selected CNA platform reports a real accelerometer — for example a 2-in-1 laptop or a phone. Browser builds are excluded by policy unless keyboard emulation is on (below), which makes it true on desktop and in the browser. false when no such sensor is found. |
Start() / Stop() |
void |
Begin and end data acquisition. Start() throws AccelerometerFailedException if acquisition is already running or the platform cannot start it. |
getCurrentValueProperty() |
AccelerometerReading |
The latest reading. Throws System::InvalidOperationException if the sensor is not supported. |
getIsDataValidProperty() |
bool |
true once a valid reading has arrived. |
getStateProperty() |
SensorState (enum) |
NotSupported, Ready, Initializing, NoData, NoPermissions or Disabled. Read under the sensor subsystem's lock. |
CurrentValueChanged, ReadingChanged |
events | Raised when a new reading arrives; use them instead of polling if you prefer. |
getTimeBetweenUpdatesProperty() / set… |
System::TimeSpan |
The requested update interval. |
AccelerometerReading members
AccelerometerReading is the value returned by getCurrentValueProperty(). It carries two members:
| Member | Type | Description |
|---|---|---|
getAccelerationProperty() |
const Vector3& |
Current acceleration in G-forces. See axis breakdown below. |
getTimestampProperty() |
System::DateTimeOffset |
When the reading was taken. |
Acceleration axis conventions
The acceleration vector uses the same coordinate conventions as XNA 4.0 on Windows Phone, measured in G-forces (1 G ≈ 9.81 m/s²). SDL3 reports raw acceleration in m/s², and CNA applies the m/s²→G conversion so that readings match XNA's units rather than SDL's:
| Component | Physical meaning | Typical value (device flat) |
|---|---|---|
getAccelerationProperty().X |
Left/right tilt — positive toward the right edge of the device | 0.0 |
getAccelerationProperty().Y |
Forward/back tilt — positive toward the top edge of the device | 0.0 |
getAccelerationProperty().Z |
Up/down (gravity component) — approximately 1.0 when the screen faces up and the device is stationary | ~1.0 |
When the device is held upright (portrait orientation), gravity shifts from Z into the Y axis, so the Y component approaches −1.0 and the Z component approaches 0.0. A shake produces transient spikes across all three axes.
Keyboard emulation (CNAEXT, off by default)
Accelerometer::setKeyboardEmulationEnabledEXT(true) replaces the primary accelerometer with a named software sensor driven by the arrow keys, so a tilt-controlled game can be tried on a desktop or in a browser without hardware or motion permission. A reading is (Right − Left, Up − Down, −1) normalised to one g; no keys reads (0, 0, −1), and an unfocused game reads neutral. Readings arrive when Game pumps input, once per frame. The switch is process-wide (getKeyboardEmulationEnabledEXT() reads it); stop every running accelerometer before changing it, or it throws InvalidOperationException. Gyroscopes and controller sensors keep their physical behaviour. The window-orientation counterpart, getWindowProperty().setKeyboardOrientationEmulationEnabledEXT(true), maps Up, Left and Right to Portrait, LandscapeLeft and LandscapeRight, honouring SupportedOrientations.
Platform support
| Platform | Accelerometer / Gyroscope | Compass / Motion | VibrateController |
|---|---|---|---|
| Android | Live data via the platform sensor service (SDL3 sensor API) | Live data via the NDK sensor fusion path | SDL haptic rumble |
| Desktop (Linux / Windows / macOS) | Live data wherever the selected platform reports a sensor device | NotSupported |
SDL haptic rumble on a real haptic device |
Android is wired through CMake (the NDK toolchain, with the sensor code linking android), but there is no Android CI job and no Android CMake preset — Android builds are a manual step today.
Concurrency hardening
The platform sensor path is not a thin wrapper. Each sensor class guards its registrations, callback lifetime and state reads with its own subsystem mutex, native subsystem acquisition and release go through the platform (the SDL3 platform serializes them under its process-wide state lock), and the implementation tracks callbacks in flight so that Dispose() can be re-entered from a callback without a use-after-free. This matters because the same underlying sensor device can be reached from more than one sensor object.
VibrateController
VibrateController is a real implementation rather than a no-op: it borrows the selected platform's haptics service, and on the SDL3 platform that service initialises rumble with SDL_InitHapticRumble, plays it with SDL_PlayHapticRumble and drives a genuine SDL_HAPTIC_LEFTRIGHT effect where the device supports one; the headless and terminal platforms provide none.
It deliberately excludes gamepads from the devices it will open. That is intentional: gamepad rumble belongs to GamePad::SetVibration(), and letting VibrateController claim the same device would mean two APIs fighting over one motor. If you want a controller to rumble, use GamePad::SetVibration().
Known gap
Compass reports TrueHeading (getTrueHeadingProperty()) as equal to MagneticHeading. CNA has no magnetic-declination data source, so the two headings cannot be distinguished. If your game needs true north, apply your own declination correction.
Code examples
Checking support before use
Always guard sensor use with Accelerometer::getIsSupportedProperty() so that your game degrades gracefully on desktop builds without a sensor:
#include "Microsoft/Devices/Sensors/Accelerometer.hpp"
using namespace Microsoft::Devices::Sensors;
// In Game::Initialize() or your platform detection pass
if (Accelerometer::getIsSupportedProperty()) {
// Sensor is available; enable tilt controls
accel_ = std::make_unique<Accelerometer>();
try {
accel_->Start();
tiltControlsEnabled = true;
} catch (const AccelerometerFailedException&) {
tiltControlsEnabled = false; // fall back to keyboard/gamepad controls
}
}
Reading the accelerometer each frame (tilt-to-move)
A typical tilt-to-move mechanic reads the sensor once in Update() and maps the X/Y axes directly to horizontal and vertical forces on a physics body or velocity vector:
// Declared in your Game class
std::unique_ptr<Accelerometer> accel_;
// In Update(GameTime& gameTime)
if (tiltControlsEnabled && accel_->getIsDataValidProperty()) {
AccelerometerReading reading = accel_->getCurrentValueProperty();
const Vector3& g = reading.getAccelerationProperty();
// Map tilt to movement — dead-zone to filter hand tremor
const float deadZone = 0.1f;
float tiltX = g.X;
float tiltY = g.Y;
if (std::abs(tiltX) < deadZone) tiltX = 0.0f;
if (std::abs(tiltY) < deadZone) tiltY = 0.0f;
// Apply as velocity (scale to your game's units per second)
float speed = 200.0f;
playerVelocity.X = tiltX * speed;
playerVelocity.Y = tiltY * speed; // positive Y = forward on the device
}
Shake detection
A shake produces a spike in the acceleration magnitude above the normal gravity baseline of ~1 G. Compare the length of the vector against a threshold:
if (tiltControlsEnabled && accel_->getIsDataValidProperty()) {
float magnitude = accel_->getCurrentValueProperty().getAccelerationProperty().Length();
const float shakeThreshold = 2.5f; // 2.5 G — adjust per game feel
if (magnitude > shakeThreshold) {
// Player shook the device — trigger action
OnShakeDetected();
}
}
Stop the sensor when you are done
// In UnloadContent() or your shutdown path
if (accel_) accel_->Stop();
Gyroscope
Gyroscope is implemented on the same real SDL3 sensor path as Accelerometer, with the same concurrency hardening, and is available on Android and desktop (Linux, Windows, macOS). It reports angular velocity around each axis, which enables more precise orientation tracking and complements the accelerometer for robust tilt controls. It follows the same instance model: Gyroscope::getIsSupportedProperty(), Start(), getCurrentValueProperty().getRotationRateProperty() (a Vector3), Stop().
Compass and Motion
Compass exposes a magnetic heading and Motion exposes a fused device attitude. Both are backed on Android by a real NDK implementation that fuses five underlying sensors through <android/sensor.h>. Both follow the same instance model as the other sensors (Compass: getCurrentValueProperty().getMagneticHeadingProperty(); Motion: getCurrentValueProperty(), then getAttitudeProperty(), getGravityProperty() and friends).
On every non-Android platform they report NotSupported. Check getIsSupportedProperty() before use and provide a fallback if your game needs a heading on desktop or web.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Host devices: the optional CNA::Devices layer — The CNA_DEVICES build contract, what each CNA::Devices class really does, its platform reach, the overlap with CNA::Input, the camera polling model and the callback boundary callers must respect.
- Sensors and vibration: delivery, math and lifetime — Which thread delivers a CNA sensor reading and in which units, the Android compass and motion mathematics, the landscape remap, VibrateController semantics, the open Dispose(bool) defect and the evidence limits.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-035: devices-tests.yml's exact-name gtest filters skip eight Microsoft::Devices suites and never match the FileDialog and MessageBox suites — DEVICES_GTEST_FILTER misses 33 of 478 TEST definitions under modules/devices/tests, and CNA_DEVICES_GTEST_FILTER names FileDialogTests.* and MessageBoxTests.*, which match no suite, so the 12 FileDialog and MessageBox ca
- CNA-BUG-049: Accelerometer, Gyroscope, Compass and Motion declare Dispose(bool) public, so an external Dispose(false) marks the sensor disposed without cleanup — SensorBase declares Dispose(bool) protected, but the four sensor classes redeclare it public; Dispose(false) sets the disposed flag without Stop, unregistration or releasing the platform lease, and the destructor then sk
- CNA-PLAT-014: Compass and Motion work only on Android; desktop sensors come only through the SDL3 platform, and web targets are refused — Compass::IsSupported and Motion::IsSupported are false everywhere except Android, as SDL3 exposes no magnetometer or fused-orientation API; Accelerometer and Gyroscope read real sensors on Android, iOS and desktop, never