Host devices: the optional CNA::Devices layer

CNA snapshot 009d40f5  ·  Deep Dives › Input, audio, media & services  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Checked by reading modules/devices-ext, the SDL3 system, tray and camera services and the root CMake option at 009d40f5; the camera fragment was syntax-checked with g++ -fsyntax-only -DCNA_DEVICES -DCNA_RENDERER_EASYGL against TARGET headers. Nothing was executed; real dialogs, trays, cameras and permission prompts remain unverified.

CNA::Devices is a host-integration layer with no XNA or Windows Phone precedent (camera, dialogs, tray, clipboard, display, locale, power, system information, URL launching), and it is absent from a default build. This page states the build contract first, because code that compiles in a devices-enabled demo can otherwise look like an unconditional framework surface, then each class's real behaviour, its platform reach, its overlap with the always-built CNA::Input classes, and the callback boundary a caller must respect. The maintainer view, with the service table and failure contracts per class, is Device extensions and host services; the task walkthrough is Tutorial 117.

The option removes the types

CNA_DEVICES defaults to OFF (root CMakeLists.txt). Every header, source and test file of modules/devices-ext (18 public headers, 10 sources and 10 test files at this snapshot) is wrapped in #ifdef CNA_DEVICES, so with the default setting the library is built with no API symbols and its tests register no cases. Camera, FileDialog or SystemTray therefore do not throw at run time in a default build: they do not exist as C++ types. Enable the layer at configure time:

cmake -S . -B build -DCNA_DEVICES=ON

The definition reaches consumers through CNA's shared build-configuration target. Defining the macro by hand in an application that links a library configured without the option only moves the failure to link time, because the declarations then compile but their definitions were never built; code that must build either way should guard its own use of the headers.

The option does not gate Microsoft::Devices::Sensors: Accelerometer, Gyroscope, Compass, Motion and VibrateController compile in every configuration. The option's help text still reads "Enable CNA-specific device/sensor extensions beyond XNA 4.0 (battery, camera, clipboard, ...)", and the word "sensor" there is misleading: this namespace is the conditional one.

What each class does

The classes are thin translations over the selected platform's service interfaces, not direct SDL calls: they borrow a service from the current platform on each call, and the platform backend owns the native library. The SDL3 platform implements every service; other platforms implement some (see which platforms provide which service).

ClassBehaviour at this snapshot
ClipboardsetTextProperty returns false when there is no clipboard service or the write fails; the getters read empty or false without a service.
DisplayInfoA window's content scale (the larger of the window's display scale and its display's content scale, never their product, so a backend that reports both is not double-counted; 0 without a platform window) and its safe interactive area (empty without a window or display service).
LocaleThe host's preferred locale list.
MessageBoxSimple and multi-button native dialogs; synchronous.
FileDialogOpen-file, save-file and open-folder choosers; asynchronous, answered through a callback.
SystemTrayOwns one tray icon with a tooltip and a flat, callback-driven menu.
PowerInfoHost power state, battery percentage and remaining seconds.
SystemInfoLogical CPU core count and physical RAM in megabytes.
UrlLauncherOpen(url) forwards the string to the platform and returns its answer.
CameraPolls camera frames into a Texture2D (below).

There are no placeholder or "not implemented" throws in the module, but thin forwarding still has behavioural edges a caller should know:

  • Message boxes report support from the platform's message-box capability, and a platform failure never escapes: ShowSimple returns silently and Show returns -1, the same value as a dismissed dialog (MessageBox.cpp). Every dialog is shown with a null parent window, so it is never parented to the game window. The SDL3 service marks no button as the default or cancel button, and refuses an empty button list with a platform exception, which the wrapper turns into -1. (An alpha.1-era wrapper reported support unconditionally and did not reject an empty list.)
  • File dialogs: getIsSupportedProperty() applies target policy (false on Web and iOS, the platform capability on desktop and Android), but the three Show… functions gate only on the capability, so the policy changes what the query says, not whether a call reaches the service. Without a service the callback is invoked once with an empty list, so a waiting continuation never hangs; when the platform throws, the callback is not invoked at all.
  • PowerInfo's three properties each ask the platform separately (getStateProperty, getBatteryPercentProperty, getSecondsRemainingProperty in PowerInfo.cpp), so they can describe three different instants, visibly wrong when a charger is plugged in between reads. CNA::Input::Power::GetInfoEXT reads once and has no such gap.
  • SystemTray's constructor throws PlatformNotSupportedException when the platform has no tray service and PlatformException when the service returns no icon; the public class sets a tooltip and menu entries but offers no way to set the icon image.
  • UrlLauncher validates nothing: the string goes to the platform as given, so a game must not pass untrusted input to it.

Camera: poll, do not wait for a callback

Camera (Camera.cpp) opens the first camera the platform reports, keeps the session for its lifetime and is neither copyable nor movable. The SDL3 provider requests RGBA32 frames and reports the session Lost if the negotiated format is anything else; getStateProperty() maps the session's state (Opening, Denied, Ready, Lost) on every call, because a permission prompt can resolve long after construction, and answers NotSupported when there is no session at all. The public CameraState::Closed is never returned by this wrapper, and Lost is best effort: SDL may keep delivering blank frames after a device is unplugged. TryAcquireFrame(texture) never blocks and returns false without touching the texture when there is no session, no fresh frame, a frame of a different size from the texture, or an inconsistent pixel count; it neither allocates nor resizes the texture, so create it with the camera's frame size.

#ifdef CNA_DEVICES
Camera camera;                              // a member: it cannot be copied or moved
std::unique_ptr<Texture2D> frame;

// Each Update or Draw:
if (camera.getStateProperty() == CameraState::Ready)
{
    if (!frame)
    {
        frame = std::make_unique<Texture2D>(
            graphicsDevice, camera.getFrameWidthProperty(), camera.getFrameHeightProperty());
    }
    if (camera.TryAcquireFrame(*frame))
    {
        DrawCameraPreview(*frame);
    }
}
#endif

Syntax-checked against the TARGET headers with -DCNA_DEVICES -DCNA_RENDERER_EASYGL (the graphics headers need a renderer definition); not executed. DrawCameraPreview is the game's own function.

An earlier revision never started SDL's camera subsystem anywhere, so camera enumeration was empty and Camera settled in NotSupported unless the host had started the subsystem itself: an initialization gap that looked like "no camera attached". At this snapshot the SDL3 camera provider starts the subsystem once, on first use, and deliberately never stops it, because a camera id is meaningful only inside the session that issued it (Sdl3Camera.cpp, EnsureCameraSubsystem). That fix is read from source: no test refers to Sdl3Camera or Sdl3CameraProvider, and the Camera tests run against a canned camera platform (CNA-VGAP-007). SDL3 is the only platform with a camera provider.

Platform support is per class

A namespace-level "desktop utilities" label is useful orientation but wrong as a capability test. The system tray exists on the SDL3 platform where SDL reports tray support and on native X11, and nowhere on Android, iOS, the web, Wayland or Win32 at this snapshot. The file dialog is not equivalent: its target policy allows Android, where SDL3 supplies a dialog implementation (an SDL fact not re-verified here), and refuses Web and iOS. Display scale and safe area need a real associated window. Message boxes and URL launching inherit whatever the host permits, even when the class reports support. Query the particular class, expect its callbacks to run on a platform thread, and keep a non-dialog fallback for sandboxed and web targets.

Overlap with CNA::Input

Two classes duplicate functionality that CNA::Input already has, and those CNA::Input versions are compiled unconditionally:

ConcernCNA::Input (always built)CNA::Devices (only with CNA_DEVICES=ON)
ClipboardClipboard::GetTextEXT/SetTextEXT/HasTextEXT (a void setter that swallows failure), plus the X11/Wayland primary selection and MIME-typed dataClipboard::getTextProperty/setTextProperty/getHasTextProperty, text only, with a setter that reports failure
Host powerPower::GetInfoEXT returning PowerStateEXT from one readingPowerInfo returning CNA::Devices::PowerState from three readings

They were written on parallel workstreams and use structurally identical but type-incompatible enumerations (PowerStateEXT and PowerState) while wrapping the same platform call. They cannot collide, because the namespaces are disjoint, but with CNA_DEVICES=OFF, the default, the CNA::Input versions are the only clipboard and host-battery APIs that exist. CNA::Input::Sensors is not a duplicate of Microsoft::Devices::Sensors: it is a stateless open, read and close call returning a raw vector, while the Windows Phone classes are stateful objects with events, throttling and disposal. CNA::Input::InputDevices has no counterpart here.

Lifetime and the callback boundary

File dialogs, tray menus and camera permission all cross native asynchronous boundaries. State captured by their callbacks must outlive the native operation, and a UI callback should hand its work back to the game thread before touching graphics state. The camera session and tray icon must be destroyed before the platform that created them, and the sensor and haptic side has its own shutdown coordinator (see Sensors and vibration).

⚠

Keep dialog and tray handlers non-throwing. On the SDL3 platform both the file-dialog result trampoline (Sdl3SystemServices.cpp) and the tray-entry trampoline (Sdl3Tray.cpp) call the caller's C++ callback directly from an SDL C callback with no try/catch, so an exception thrown by a handler would unwind across a C boundary. The sensor module, by contrast, already catches and records exceptions at its corresponding boundary.

What the tests can show

The ten test files exercise canned platforms that record calls instead of opening dialogs or cameras (a real dialog blocks on a human). They count only when the option is on: in a default build they contribute no tests at all, so a CI configuration that never sets CNA_DEVICES=ON proves neither the public types nor their forwarding. At this snapshot the sanitizer presets devices-asan, devices-tsan and devices-ubsan and the devices-tests.yml workflow do set it, which is desktop and headless evidence against canned services. Real cameras, native dialogs, tray integration and permission prompts still need validation on real hosts; nothing on this page was executed.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.