Audio and input architecture

CNA snapshot 009d40f5  ·  Development › Architecture Maps  ·  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. The layers and the input flow were read from the audio and input modules, CMake and Game::PollEvents, and cross-checked against CNA's input-backend and ALSA documents; the named tests exist but were not executed for this page, and no audio device or input hardware was exercised.

Audio and input both expose stable, game-facing state while platform-specific code handles devices and events, and both are real-time services: audio is pulled by a device callback on its own thread, and input is pushed once per frame by the game loop. Neither should be collapsed into “whatever SDL does”, because CNA also supports paths with no SDL in them. This page maps the layers, the event-to-snapshot flow that input actually uses at this snapshot, and the threading assumptions each subsystem relies on.

Audio layers

SoundEffect, SoundEffectInstance, DynamicSoundEffectInstance, Microphone, XACT (AudioEngine, WaveBank, SoundBank, Cue)
        │                                   FrameworkDispatcher::Update() pumps streams and media once per Update
        ▼
MixerEngine  (facade, internal header)  ← SOUND_ENABLED: exists only for SDL3 and ALSA
   ├─ SDL3_mixer route   src/Backend/Sdl3Mixer    (CNA_AUDIO_PLATFORM=SDL3)
   └─ CNA's own mixer     src/Backend/CnaMixer     (CNA_AUDIO_PLATFORM=ALSA; stb_vorbis and dr_libs decoders)
        │  both open the selected device through CreateSelectedAudioDevice()
        ▼
IAudioDevice / IAudioRecordingDeviceProvider     (independent of IPlatform)
   SDL3 · SDL2 · ALSA · NULL     → sound hardware, or a paced silent callback for NULL

Public APIs include SoundEffect, SoundEffectInstance, DynamicSoundEffectInstance, Microphone and the XACT-shaped types. MixerEngine (MixerEngine.hpp) is the internal facade the XNA classes are written against: the build supplies exactly one implementation, either the SDL3_mixer route or CNA's own mixer. CNA_AUDIO_PLATFORM selects the device implementation (SDL3, SDL2, NULL or ALSA; OPENAL and WASAPI are refused), independently of CNA_PLATFORM: a headless application may use SDL3 audio and a graphical one may choose deterministic NULL audio. Audio is not part of IPlatform.

Only SDL3 and ALSA have a mixer, and SOUND_ENABLED is defined for exactly those two. The two mixer backends are the only callers of CreateSelectedAudioDevice(), so at this snapshot the SDL2 and NULL devices are transports that no production playback path opens: with either selected, the XNA facade is compiled without a mixer, SoundEffect::Play() reports false and instances stay stopped, and SDL2 and NULL also have no capture provider. ALSA uses the ALSA device, loads libasound at run time and mixes with CNA's own engine. FrameworkDispatcher::Update() lives in this module but reaches into media and input: it pumps dynamic sound streams, microphone buffers, MediaPlayer and, when a touch device exists, TouchPanel::Update(); that coupling is the reason for the declared audio ↔ media cycle in the module graph.

Decode, voice state, mixing and device-callback lifetimes are distinct. IAudioBufferCallback::FillBuffer may run on a real-time native thread and, by contract, must not block, allocate or throw (avoid logging and any destruction that waits on the callback itself, too). IAudioDevice::Stop() is a callback barrier that waits for an in-flight callback, Close() guarantees the callback never runs again, and neither may be called from inside the callback. Shutdown therefore needs a callback barrier before buffers or voices the callback could still observe are released; AudioDeviceConformanceTests.StartStopAndCloseFormCallbackBarriers pins the device half, and the SDL3 mixer guards its borrowed tracks with a mixer-generation counter. The object-level lifetimes are in the ownership map and audio engine internals.

Native event to public snapshot

IPlatform::PollEvents(batch)        each implementation maps native events to PlatformEvent values and keeps
  └─ Game::PollEvents, per event         its own keyboard / mouse / gamepad / joystick state
      ├─ PlatformInputBridge::ProcessEvent   (always first)
      │    ├─ legacy InputManager accumulators (compatibility state for the raw bridge)
      │    ├─ mouse-click callbacks, text input and IME editing, device connect / disconnect
      │    └─ touch → TouchPanel event map → gesture detector
      └─ Game handling: quit, window, lifecycle, drop
end of the batch, once per frame
  ├─ IPlatformKeyboard::Update() and IPlatformMouse::Update()          every frame
  └─ IPlatformGamepad::Update() and IPlatformJoystick::Update()        only once the gamepad subsystem is up
        ▼
public queries in Update / Draw:  Keyboard::GetState ← keyboard snapshot · Mouse::GetState ← mouse snapshot
   GamePad::GetState ← gamepad snapshot · TouchPanel::GetState ← the panel's event map

The keyboard, mouse, gamepad and joystick state that game code reads is published by the platform services, not by the input bridge. PlatformInputBridge::ProcessEvent (SdlInputBridge.cpp) feeds the compatibility accumulators, text input and editing, mouse-click callbacks, device hot-plug notifications and the touch map; then the runtime calls each service's Update() once per frame, after the batch and before Update and Draw. Keyboard::GetState(), Mouse::GetState() and GamePad::GetState() read those stored snapshots and never poll a native API, so repeated reads within a frame return the same value. Touch is the exception in the other direction: TouchPanel accumulates from events itself and TouchPanel::Update() alone advances previous locations and retires releases. Controller and joystick services are updated lazily: Game no longer acquires the controller subsystem at startup, and the update runs only once something has asked for the gamepad or joystick service.

Relative mouse movement is the notable consume-on-read exception. In relative mode Mouse::GetState() calls IPlatformMouse::ConsumeRelativeDelta(), so a second read with no intervening motion returns zero; do not treat every getter as an immutable snapshot. The mapping table and the per-device fidelity notes are in CNA's own input backend document; the test suites live under modules/input/tests (for example the SdlInputBridge* golden, mouse, keyboard, text-input and touch suites and InputResetTests.cpp). Older prose that describes the bridge as the source of the keyboard and mouse snapshot is stale.

Threading assumption

The input pipeline assumes that the game-loop thread owns event processing and snapshot publication. Its process-wide state (the compatibility accumulators, the gesture detector, the touch panel's arrays and queues, the static click and text-input callbacks) is deliberately unsynchronized, and InputManager's own header states the rule: writes come from Game::PollEvents(), reads come from Update() and Draw() on the same thread, and nothing may call the setters or getters from a background thread. Absence of locks is not evidence of arbitrary thread safety. If a platform callback arrives on another thread, it must be marshalled into the platform event path rather than touching input state directly.

Audio is the opposite case: the device callback runs on another thread by design, so shared mixer state is protected (MixerLock for short control updates, a mutex around the dynamic-stream registry that FrameworkDispatcher::Update() snapshots before running each instance) and the callback itself is held to the no-block, no-allocate rule above. The full thread and callback map is in Thread and callback map.

First debugging points

SymptomInspect
No native inputthe platform's event pump, its capability flags and whether the accessor (GetKeyboard, GetMouse, GetGamepad) returns a service at all
Native event exists, public state stalethe end of Game::PollEvents (the service Update() calls), then PlatformInputBridge::ProcessEvent for text, touch and click paths, and the frame snapshot boundary
Controller onlylazy subsystem acquisition (IsSubsystemInitialized for the gamepad), device enumeration and update, and the four player-slot mapping
Audio starts then crashesthe callback barrier in Stop and Close, voice and buffer lifetime, mixer generation, and backend shutdown ordering
Silent NULL or SDL2 audiothis is the expected result of the selection: no mixer, so SoundEffect::Play() reports false. Verify CNA_AUDIO_PLATFORM before debugging anything else

Continue with input internals, audio engine internals and the platform architecture.

The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.

Tests and validation
Test architecture