From 89f474e284827812ba99d88e0c0b98fe98c3438c Mon Sep 17 00:00:00 2001 From: Adrian Warecki Date: Wed, 30 Sep 2026 18:10:13 +0200 Subject: [PATCH] tools: plugin: add architecture documentation Add architecture.md describing the internals of the SOF ALSA plugin and link it from the existing README so the usage guide points at it. Signed-off-by: Adrian Warecki --- tools/plugin/README.md | 3 + tools/plugin/architecture.md | 260 +++++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 tools/plugin/architecture.md diff --git a/tools/plugin/README.md b/tools/plugin/README.md index e225437b404f..3bdd3bec2155 100644 --- a/tools/plugin/README.md +++ b/tools/plugin/README.md @@ -5,6 +5,9 @@ is still WIP with many rough edges that need refined before production deployment, however the plugin is usable today as a rapid development framework for SOF infrastructure and processing. +For an internals overview (process model, data flow, IPC, module loading and +symbol resolution) see [architecture.md](architecture.md). + ### Features * aplay & arecord usage working today * alsamixer & amixer usage not working today diff --git a/tools/plugin/architecture.md b/tools/plugin/architecture.md new file mode 100644 index 000000000000..5276896de3f8 --- /dev/null +++ b/tools/plugin/architecture.md @@ -0,0 +1,260 @@ +# SOF ALSA Plugin Architecture + +This directory contains the SOF ALSA plugin: infrastructure that runs unmodified +SOF firmware natively on a Linux host, so topologies and processing modules can be +exercised with `aplay`/`arecord` without a physical DSP. + +## Overview + +The plugin is split across **two processes** that share the SOF firmware code but +play different roles: + +1. **ALSA client** - the plugin loaded inside the `aplay`/`arecord` application + (`libasound_module_pcm_sof.so`). It implements an ALSA `ioplug` device named + `sof:...` and moves PCM data to/from the daemon. +2. **`sof-pipe` daemon** - a standalone process that links the real SOF core + (`libsof.a`, built with `CONFIG_LIBRARY`) and actually instantiates and runs + pipelines, components and modules. + +The two processes communicate over three POSIX IPC channels: + +* **Shared memory (SHM)** - an audio ring buffer per PCM, plus a global state + block holding endpoint configuration and kcontrols. +* **Unix domain socket** - the IPC4 message transport (topology setup, pipeline + state changes). +* **POSIX semaphores** - per-pipeline `ready`/`done` flow control. + +```mermaid +graph LR + subgraph APP["aplay / arecord"] + A[Application] --> B["alsaplug/pcm.c
ioplug device"] + end + subgraph DAEMON["sof-pipe daemon"] + D["pipe/main.c
IPC socket loop"] --> E["pipe/ipc4.c
IPC dispatch + dlopen"] + E --> F["pipe/pipeline.c
pipeline threads"] + F --> G["libsof.a
SOF core"] + F --> H["module .so
loaded via dlopen"] + end + B <--> |SHM audio ring| H + B <--> |Unix socket IPC4| D + B <--> |ready/done semaphores| F +``` + +## Component Layout + +### Shared (`tools/plugin/`) + +| File | Responsibility | +|------|----------------| +| `common.h` | Structures shared by both processes: `plug_shm_endpoint` (audio ring buffer), `plug_shm_glb_state` (global state, endpoint configs, kcontrols) and the ring-buffer accessor inlines. Defines the shared-memory contract. | +| `common.c` | Shared SHM, socket, semaphore and timing helpers. | + +### ALSA client (`tools/plugin/alsaplug/`) + +| File | Responsibility | +|------|----------------| +| `plugin.c` | Parses the ALSA configuration and command line (topology name, PCM ID, card/device/config). | +| `pcm.c` | The `snd_pcm_ioplug` implementation and PCM entry point `SND_PCM_PLUGIN_DEFINE_FUNC(sof)`. Owns `hw_params`, `prepare`, `start`, `transfer`, `pointer` and `stop`. | +| `conf.c` | ALSA configuration load hook. | +| `ctl.c` | Separate ALSA ctl (mixer/kcontrol) plugin. | +| `tplg.c` | Client-side topology parsing; emits the IPCs that instantiate widgets and pipelines. | +| `tplg_ctl.c` | kcontrol handling derived from the topology. | +| `plugin.h` | The `snd_sof_plug_t` client context type. | + +### Daemon (`tools/plugin/pipe/`) + +| File | Responsibility | +|------|----------------| +| `main.c` | Daemon entry point: argument parsing, SOF instance creation, global SHM setup, IPC accept loop. | +| `ipc4.c` | IPC dispatch: the `module_id` to library map, `dlopen` of modules, and the local before/after hooks around the real `ipc_cmd()`. | +| `pipeline.c` | SOF core initialisation (`pipe_sof_setup`) and the per-pipeline worker threads. | +| `cpu.c` | CPU affinity and realtime priority helpers. | +| `pipe.h` | The `sof_pipe` daemon context type. | + +### Endpoints and modules (`tools/plugin/modules/`) + +| File | Responsibility | +|------|----------------| +| `shm.c` | Host endpoint (`shmread`/`shmwrite`): bridges a pipeline buffer to the SHM ring buffer shared with the client. Legacy `comp_driver` component. | +| `alsa.c` | DAI endpoint (`aplay`/`arecord`): opens a real ALSA device and bridges a pipeline buffer to hardware. Legacy `comp_driver` component. | +| `ov_noise_suppression/` | An OpenVINO noise-suppression processing module using `module_interface`. Built only when OpenVINO is available. | + +## ALSA Integration + +The client uses the ALSA external I/O plugin mechanism (`snd_pcm_ioplug`), +registered through `SND_PCM_PLUGIN_DEFINE_FUNC(sof)`. Playback and capture install +distinct callback tables; the `transfer` callback is `plug_pcm_write` for playback +and `plug_pcm_read` for capture. + +Hardware constraints advertised by `plug_hw_constraint` are deliberately wide +(formats S16/S24/S32/FLOAT, 1-8 channels, 1-192 kHz) because the effective +constraints are only known once the topology has been parsed. + +## Startup Sequence + +The daemon must be started before any client connects. + +```mermaid +sequenceDiagram + participant Cli as ALSA client + participant Sock as Unix socket + participant Dae as sof-pipe daemon + participant Core as SOF core + + Dae->>Core: pipe_sof_setup() builds the SOF instance + Dae->>Dae: create global SHM, enter accept() loop + Cli->>Cli: parse ALSA conf and topology + Cli->>Sock: connect IPC client socket + Cli->>Dae: hw_params sends topology setup IPCs + Dae->>Core: ipc_cmd() instantiates modules and pipelines + Cli->>Dae: prepare transitions pipelines PAUSED then RUNNING +``` + +## IPC and Module Loading + +The client translates the parsed topology into IPC4 messages sent over the socket. +The daemon handles each message in `pipe_ipc_do`, which wraps the real core handler +with local hooks: + +```text +pipe_sof_ipc_cmd_before() daemon-local actions before the core +pipe_ipc_message() ipc_cmd() - the real SOF IPC handler +pipe_sof_ipc_cmd_after() daemon-local actions after the core +``` + +Key responsibilities of the hooks: + +* **`MOD_INIT_INSTANCE` (before)** - `pipe_register_comp` `dlopen`s the module's + shared object per a `module_id` to library map. This runs the module's + registration constructor before the core instantiates the component. +* **`GLB_CREATE_PIPELINE` (after)** - allocate a pipeline thread context and its + semaphores. +* **`SET_PIPELINE_STATE = RUNNING` (after)** - start the pipeline worker thread. +* **`SET_PIPELINE_STATE = PAUSED` (before)** - stop the worker thread. +* **`GLB_DELETE_PIPELINE` (after)** - free the thread context. + +Each connected client is served by its own `handle_ipc_client` thread. + +## SOF Instance and Scheduling + +`pipe_sof_setup(struct sof *sof)` (called from `main()` with `sof_get()`) brings up +the same core used on target: component registry (`sys_comp_init`), position +tracking, notifier, IPC (`ipc_init`) and the LL and EDF schedulers. Only the +platform layer differs from a real DSP - host threads replace the DSP scheduler, +a socket replaces the IPC mailbox, and the SHM/ALSA endpoints replace DMA/DAI. + +Because there are no DSP interrupts, each pipeline runs as a `pthread`. The worker +loop waits on the `ready` semaphore, invokes the real `pipeline_copy()`, then posts +the `done` semaphore: + +```c +do { + if (pipeline->status != COMP_STATE_ACTIVE) break; + if (pipe_users <= 0) break; + pipe_copy_ready(pd); /* sem_timedwait(ready) */ + err = pipeline_copy(pd->pcm_pipeline); /* real SOF pipeline run */ + pipe_copy_done(pd); /* sem_post(done) */ +} while (1); +``` + +`pipeline_copy()` walks the component graph and, for each module, calls into the +Module Adapter, which invokes the module callback. The semaphores pace one period +per cycle between the client and the pipeline thread. + +## Audio Data Flow + +Audio crosses the process boundary through the `plug_shm_endpoint` ring buffer. The +daemon-side `shm` endpoint maps the same named SHM object as the client, so both +sides share one ring buffer with read/write positions and wrap counters. + +```mermaid +sequenceDiagram + participant App as aplay + participant SHM as SHM ring + participant Shm as shm endpoint + participant Pipe as pipeline + participant Alsa as alsa endpoint + participant HW as ALSA hw device + + App->>SHM: plug_pcm_write copies PCM into the ring then plug_ep_produce + App->>Pipe: sem_post ready + Pipe->>Shm: shmread_copy copies the ring into the pipeline buffer + Pipe->>Pipe: modules process the period + Pipe->>Alsa: aplay_copy writes to hardware via snd_pcm_writei + Pipe->>App: sem_post done +``` + +Capture is the mirror image: the `alsa` endpoint reads hardware with +`snd_pcm_readi`, the pipeline processes, the `shm` endpoint writes to the ring +buffer, and `plug_pcm_read` copies the ring buffer into the application. For +capture the semaphore order is reversed so the source/DAI pipeline runs first. + +## Module Interfaces + +A plugin pipeline contains two kinds of component: + +* **Endpoints (`shm`, `alsa`)** use the legacy `comp_driver` model + (`.ops.copy`, direct `comp_buffer` access). They form the host and DAI + boundaries of the pipeline and are not `module_interface` modules. +* **Processing modules** go through the Module Adapter and `struct + module_interface`, exactly as on target. The Module Adapter wraps the pipeline + `comp_buffer`s into `sof_source`/`sof_sink` and dispatches the module callback, + so a plugin-hosted processing module sees the same contract as firmware. + +`ov_noise_suppression` is the only `module_interface` processing module whose +source lives in this directory. Other processing modules the plugin runs +(for example `volume` and `mixer`) are built from `src/audio` and loaded as +shared objects. + +## Dynamic Module Loading and Symbol Resolution + +Modules are separate shared objects loaded on demand; this section explains how a +`.so` binds to the already-running core. + +A module `.so` exposes no bespoke API. At load time the relevant artefact is an ELF +constructor emitted by the `DECLARE_MODULE` macro, which calls `comp_register()` to +add the module's `struct comp_driver` (and, for processing modules, its +`struct module_interface` via the Module Adapter) to the core's driver list. The +core later selects the driver by UUID. + +`DECLARE_MODULE` expands differently per build configuration: + +* `CONFIG_LIBRARY_STATIC` (the core `libsof.a`) - expands to nothing; the core has + its infrastructure compiled in directly. +* `CONFIG_LIBRARY` (the loadable `.so` modules) - expands to an + `__attribute__((constructor))` that the dynamic loader runs automatically on + `dlopen`. +* Target DSP builds - place the initcall in a dedicated linker section. + +Symbol resolution relies on the ELF global namespace: + +1. A module `.so` is not linked against `libsof.a`; its references to core + functions (`comp_register`, `rzalloc`, `module_adapter_new`, `source_get_data`, + `sink_get_buffer`, and so on) are left undefined. +2. `sof-pipe` is linked with `-rdynamic` and pulls the whole core in with + `-Wl,--whole-archive`, placing every core symbol in the executable's dynamic + symbol table. +3. `dlopen(lib, RTLD_NOW)` resolves the module's undefined symbols against the + executable's exported symbols, binding the module to the single core instance + created by `pipe_sof_setup()`. +4. The constructor then runs `comp_register()`, registering into that one core + instance. + +Static-linking the core into each module would give every `.so` its own private +copy of the core's globals, so registration would populate a private list the +daemon never sees. A single core instance in the executable is therefore required. + +On the `MOD_INIT_INSTANCE` IPC the daemon `dlopen`s the module before `ipc_cmd()` +instantiates the component, so the driver is registered by the time the core +matches it by UUID. + +## Synchronization Primitives + +| Resource | Type | Role | +|----------|------|------| +| SHM `ctx` | `plug_shm_glb_state` | Global state, endpoint configs, kcontrols | +| SHM `pcm` | `plug_shm_endpoint` | Audio ring buffer, one per PCM | +| Semaphore `ready` | per pipeline | Producer signals data is available to process | +| Semaphore `done` | per pipeline | Pipeline signals the period has been processed | +| Unix socket | stream | IPC4 messages from client to daemon, with reply | +| `ipc_lock` | `pthread_mutex` | Serializes access to `ipc_cmd()` in the daemon |