Chapter 47 Sensors and Microsoft.Devices
Microsoft::Devices::Sensors ports the Windows Phone 7 sensor model to SDL3 and Android NDK inputs. The CNA-original host utility layer in Chapter 48 is separate. Sensor callbacks may run outside the game thread, which makes lifecycle and synchronization part of the public usage contract.
47.1 Sensor model and delivery
SensorBase<TSensorReading> supplies CurrentValue, IsDataValid, TimeBetweenUpdates (default 2 ms), CurrentValueChanged, and abstract Start/Stop. Do not touch GraphicsDevice or other main-thread-only objects in a sensor handler; transfer the reading through a synchronized queue.
Desktop Accelerometer and Gyroscope install an SDL event watch rather than a polling thread. SDL_EVENT_SENSOR_UPDATE is handled on the thread that pushes it. Acceleration is converted from SI units to by division by 9.80665; angular velocity is already in radians per second. Readings receive UTC wall-clock timestamps.
Android Compass and Motion use ASensorManager, event queues, and ALooper on worker threads, without JNI. A Motion instance can own six workers for rotation, game rotation, gravity, linear acceleration, gyroscope, and magnetic field. The native route expects Android API 24 or later.
| Type | Implemented route | Boundary |
|---|---|---|
| Accelerometer | SDL3 on desktop, Android, and iOS | WP7 legacy ReadingChanged also fires |
| Gyroscope | SDL3 sensor events | CNAEXT State; no legacy event |
| Compass | Android NDK only | Unsupported elsewhere; Start() throws |
| Motion | Android NDK fusion only | Unsupported elsewhere; six possible workers |
47.2 Concurrency contract
Disposal is claimed atomically so only one racing caller performs cleanup. A losing caller waits for terminal state, and an RAII guard publishes that state even if cleanup throws. Accelerometer and Gyroscope track callbacks in flight so a handler can dispose its own sensor without waiting for itself.
Current value and validity are published under one lock by SetCurrentValueAndMarkDataValid. Separate locks previously allowed a reader to observe IsDataValid=true with the prior value. Update throttling compares elapsed time at the configured interval’s coarser resolution; the earlier reverse conversion overflowed signed 64-bit ticks for TimeSpan.MaxValue.
Compass and Motion use a two-phase start/stop protocol: reserve under the owner mutex, invoke the backend without that lock, then commit or roll back. A condition variable prevents a new start while an orphaned start’s cleanup call is still in flight. Stop deliberately remains able to supersede a start without waiting on it.
Historical note. Why sanitizer evidence matters here The first lifecycle rewrite appeared race-free under ordinary tests and manual review. ThreadSanitizer found that Stop cleared a transition flag before an orphaned Start had finished its backend cleanup, allowing a third Start to enter concurrently. The stress test reproduced both the race and a use-after-free in callback bookkeeping. The in-flight-call counter and condition variable are the current fix. UndefinedBehaviorSanitizer separately found the update-interval overflow.
Exceptions cannot cross SDL, Android C callbacks, or raw thread entry points. CNA records backend, operation, native error, device, timestamp, and severity through a noexcept diagnostic channel. The Android bridge distinguishes std::exception::what() from an unknown throw. Other device paths still retain older ad hoc counters, so the shared diagnostic record is not yet universal.
47.3 Compass and motion math
Compass uses upright heading when gravity meets the threshold; otherwise it derives flat heading from the quaternion. Results are normalized to , and an invalid quaternion maps to . Accuracy values High/Medium/Low/Unreliable map to 5/15/20/180 degrees. Without declination data, TrueHeading equals MagneticHeading.
Motion normalizes Android’s rotation quaternion and derives pitch, yaw, and roll. A process-wide CNAEXT landscape remap affects acceleration, rotation rate, gravity, and linear acceleration, but not Attitude: the requested axis reflection has determinant and cannot be represented by a quaternion. Fusion accepts the latest readings within 500 ms; it does not interpolate them to one timestamp.
47.4 Vibration
VibrateController is a process-lifetime singleton. Start(TimeSpan) accepts the WP7 range ; Start(TimeSpan,float) and StartLeftRight are CNAEXT intensity controls. SDL’s Android haptic route blends motor intensities; independent left/right controller rumble is available through GamePad, not this phone-style API.
DevicesShutdownCoordinator::Shutdown() must precede the application’s SDL_Quit(). It prevents the singleton’s later static destructor from closing an SDL device after SDL has already released native state. One global mutex serializes sensor and haptic subsystem initialization and preserves the documented per-instance-before-global lock order.
47.5 Known limitation and evidence status
All four concrete sensors expose Dispose(bool) publicly even though the base declares it protected. External Dispose(false) marks the instance disposed without stopping it, decrementing use counts, or releasing SDL ownership. This is an open API/lifetime defect; call the parameterless public Dispose().
Focused tests cover math, throttling, lifecycle, fake backends, reentrancy, and TSan stress. They do not constitute hardware QA. The Android source notes that its sign and zero conventions have not been checked on a physical device, and the Android demo is an older copy of the desktop demo. Android behavior is source-proven and host-tested where fakes permit, not device-verified.