ucm: implement ValueDefaults.BootCardGroup and define use

We need a boot synchronization for multiple UCM cards where linking
is expected like AMD ACP or Intel AVS drivers. This method is
using a timestamp file which can be created and modified during
the boot process (e.g. from the alsactl tool).

The goal is to return a valid UCM configuration for standard
applications combining multiple ALSA cards into one UCM configuration
and cover the time window when all cards have not been probed yet.

Signed-off-by: Jaroslav Kysela <perex@perex.cz>
This commit is contained in:
Jaroslav Kysela 2025-12-01 13:32:10 +01:00
parent 5c4a683bd0
commit 554efca497
5 changed files with 356 additions and 12 deletions

View file

@ -1039,6 +1039,224 @@ static int set_defaults(snd_use_case_mgr_t *uc_mgr, bool force)
return 0;
}
/**
* \brief Read boot information from 'Boot' control element
* \param uc_mgr Use case manager
* \param boot_time Pointer to boot time output (or NULL)
* \param sync_time Pointer to synchronization time window output (or NULL)
* \param restore_time Pointer to restore time output (or NULL)
* \param primary Pointer to primary card flag output (or NULL)
* \return 0 on success, otherwise a negative error code
*
* Reads the 'Boot' control element with SND_CTL_ELEM_TYPE_INTEGER64 (count=3):
* - index 0 = boot time in CLOCK_MONOTONIC_RAW (only seconds)
* - index 1 = restore time in CLOCK_MONOTONIC_RAW (only seconds)
* - index 2 = primary card number (identifies group)
* Returns -1 for all parameters when the control element is not present
*/
static int boot_info(snd_use_case_mgr_t *uc_mgr, long long *boot_time, long long *sync_time,
long long *restore_time, long long *primary)
{
struct ctl_list *ctl_list;
snd_ctl_elem_id_t *id;
snd_ctl_elem_info_t *info;
snd_ctl_elem_value_t *value;
int err;
if (boot_time)
*boot_time = -1;
if (sync_time)
*sync_time = -1;
if (restore_time)
*restore_time = -1;
if (primary)
*primary = -1;
ctl_list = uc_mgr_get_master_ctl(uc_mgr);
if (ctl_list == NULL || ctl_list->ctl == NULL)
return 0;
snd_ctl_elem_id_alloca(&id);
snd_ctl_elem_info_alloca(&info);
snd_ctl_elem_value_alloca(&value);
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_CARD);
snd_ctl_elem_id_set_name(id, ".Boot");
snd_ctl_elem_info_set_id(info, id);
err = snd_ctl_elem_info(ctl_list->ctl, info);
if (err < 0)
return 0;
if (snd_ctl_elem_info_get_type(info) != SND_CTL_ELEM_TYPE_INTEGER64) {
snd_error(UCM, "Boot control element is not INTEGER64 type");
return -EINVAL;
}
if (snd_ctl_elem_info_get_count(info) != 4) {
snd_error(UCM, "Boot control element does not have count=4");
return -EINVAL;
}
snd_ctl_elem_value_set_id(value, id);
err = snd_ctl_elem_read(ctl_list->ctl, value);
if (err < 0) {
snd_error(UCM, "failed to read Boot control element: %s",
snd_strerror(err));
return err;
}
if (boot_time)
*boot_time = snd_ctl_elem_value_get_integer64(value, 0);
if (sync_time)
*restore_time = snd_ctl_elem_value_get_integer64(value, 1);
if (restore_time)
*restore_time = snd_ctl_elem_value_get_integer64(value, 2);
if (primary)
*primary = snd_ctl_elem_value_get_integer64(value, 3);
return 0;
}
/**
* \brief Wait using snd_ctl_read() and snd_ctl_wait() for boot synchronization
* \param uc_mgr Use case manager
* \param primary_card Primary ALSA card number
* \return 0 on success, 1 if reparse is required, negative error code on failure
*
* This function uses boot_info() to read the Boot control element and waits
* until the timeout has passed using snd_ctl_read() and snd_ctl_wait().
* No file synchronization is used.
*/
static int boot_wait(snd_use_case_mgr_t *uc_mgr, int *_primary_card)
{
char *boot_card_sync_time = NULL;
struct ctl_list *ctl_list;
snd_ctl_event_t *event;
long long boot_time_val, boot_synctime_val, restore_time_val, primary_card;
long long timeout = 20; /* default timeout in seconds */
long long timeout_guard = 5; /* guard time in seconds */
struct timespec start_time, now;
int err;
snd_ctl_event_alloca(&event);
if (_primary_card)
*_primary_card = -1;
err = get_value1(uc_mgr, &boot_card_sync_time, &uc_mgr->value_list, "BootCardSyncTime");
if (err == 0 && boot_card_sync_time != NULL) {
long sync_time;
if (safe_strtol(boot_card_sync_time, &sync_time) == 0 && sync_time > 0 && sync_time <= 240) {
timeout = (time_t)sync_time;
snd_trace(UCM, "BootCardSyncTime set to %ld seconds", (long)timeout);
} else {
snd_error(UCM, "Invalid BootCardSyncTime '%s', using default %ld seconds", boot_card_sync_time, (long)timeout);
}
free(boot_card_sync_time);
}
ctl_list = uc_mgr_get_master_ctl(uc_mgr);
if (ctl_list == NULL || ctl_list->ctl == NULL)
return -ENODEV;
err = snd_ctl_subscribe_events(ctl_list->ctl, 1);
if (err < 0) {
snd_error(UCM, "cannot subscribe to control events: %s", snd_strerror(err));
return err;
}
clock_gettime(CLOCK_MONOTONIC_RAW, &start_time);
/* increase timeout to allow restore controls using udev/systemd */
/* when timeout limit exceeds */
timeout += timeout_guard;
while (1) {
long long diff, remaining = 0;
clock_gettime(CLOCK_MONOTONIC_RAW, &now);
err = boot_info(uc_mgr, &boot_time_val, &boot_synctime_val, &restore_time_val, &primary_card);
if (err < 0)
goto _fin;
if (primary_card < INT_MIN || primary_card > INT_MAX) {
err = -EINVAL;
goto _fin;
}
if (_primary_card)
*_primary_card = primary_card;
snd_trace(UCM, "Boot info: boot_time=%lld, restore_time=%lld, primary=%lld",
boot_time_val, restore_time_val, primary_card);
if (boot_time_val == -1) {
snd_trace(UCM, "Boot control element not present, skipping boot wait");
return 0;
}
if (timeout > boot_synctime_val + timeout_guard) {
timeout = boot_synctime_val + timeout_guard;
snd_trace(UCM, "Boot sychronization time reduced from boot element to %lld", timeout);
}
diff = now.tv_sec - restore_time_val;
if (restore_time_val > 0) {
snd_trace(UCM, "Controls restored, skipping boot wait");
/* if restore was done before short time window, reparse */
return diff < timeout_guard;
}
diff = now.tv_sec - start_time.tv_sec;
if (diff < 0 || diff >= timeout) {
snd_trace(UCM, "Maximum wait time exceeded, proceeding");
break;
} else {
remaining = timeout - diff;
}
diff = now.tv_sec - boot_time_val;
snd_trace(UCM, "Boot time diff %lld now %lld", diff, now.tv_sec, boot_time_val);
if (diff < 0 || diff >= timeout) {
snd_trace(UCM, "Boot timeout reached, proceeding");
break;
} else {
remaining = timeout - diff;
}
snd_trace(UCM, "Boot waiting %lld secs", remaining);
err = snd_ctl_wait(ctl_list->ctl, remaining * 1000);
if (err < 0) {
snd_error(UCM, "snd_ctl_wait failed: %s", snd_strerror(err));
goto _fin;
}
if (err == 0)
continue; /* Timeout, no events */
while (snd_ctl_read(ctl_list->ctl, event) > 0) {
if (!(snd_ctl_event_elem_get_mask(event) & SND_CTL_EVENT_MASK_VALUE))
continue; /* Not a value change event */
if (snd_ctl_event_elem_get_interface(event) != SND_CTL_ELEM_IFACE_CARD ||
snd_ctl_event_elem_get_index(event) != 0 ||
strcmp(snd_ctl_event_elem_get_name(event), ".Boot") != 0)
continue;
snd_trace(UCM, "Boot control element value changed");
break;
}
}
err = 0;
_fin:
snd_ctl_subscribe_events(ctl_list->ctl, 0);
return 0;
}
/**
* \brief Import master config and execute the default sequence
* \param uc_mgr Use case manager
@ -1529,7 +1747,8 @@ int snd_use_case_mgr_open(snd_use_case_mgr_t **uc_mgr,
const char *card_name)
{
snd_use_case_mgr_t *mgr;
int err;
int err, boot_result = 0, ucm_card, primary_card;
char *s;
snd_trace(UCM, "{API call} open '%s'", card_name);
@ -1559,6 +1778,10 @@ int snd_use_case_mgr_open(snd_use_case_mgr_t **uc_mgr,
if (card_name[0] == '<' && card_name[1] == '<' && card_name[2] == '<')
card_name = parse_open_variables(mgr, card_name);
/* Application developers: This argument is not supposed to be set for standard applications. */
if (uc_mgr_get_variable(mgr, "@InBoot"))
mgr->in_boot = true;
err = uc_mgr_card_open(mgr);
if (err < 0) {
uc_mgr_free(mgr);
@ -1571,6 +1794,7 @@ int snd_use_case_mgr_open(snd_use_case_mgr_t **uc_mgr,
goto _err;
}
_reparse:
/* get info on use_cases and verify against card */
err = import_master_config(mgr);
if (err < 0) {
@ -1582,12 +1806,57 @@ int snd_use_case_mgr_open(snd_use_case_mgr_t **uc_mgr,
goto _err;
}
err = check_empty_configuration(mgr);
if (err < 0) {
snd_error(UCM, "failed to import %s (empty configuration)", card_name);
goto _err;
if (!mgr->card_group) {
err = check_empty_configuration(mgr);
if (err < 0) {
snd_error(UCM, "failed to import %s (empty configuration)", card_name);
goto _err;
}
}
/* Handle BootCardGroup timestamp file logic (conf version 8+) */
if (mgr->conf_format < 8 || !mgr->card_group)
goto _std;
/* Skip if guard time passed (boot_result == 1) */
ucm_card = uc_mgr_card_number(uc_mgr_get_master_ctl(mgr));
if (ucm_card >= 0 && boot_result == 0) {
/* If InBoot open argument is present, skip this wait loop */
if (mgr->in_boot)
goto _std;
boot_result = boot_wait(mgr, &primary_card);
if (boot_result < 0) {
snd_error(UCM, "boot_wait failed");
err = boot_result;
goto _err;
}
/* Check if this card is marked as primary (primary_card == 1) */
if (primary_card != ucm_card) {
/* Not the primary card, mark as linked */
uc_mgr_free_verb(mgr);
uc_mgr_free_ctl_list(mgr);
s = strdup("1");
if (s == NULL) {
err = -ENOMEM;
goto _err;
}
err = uc_mgr_add_value(&mgr->value_list, "Linked", s);
if (err < 0)
goto _err;
goto _std;
}
/* Reparse, if the restore time window is short */
if (boot_result > 0) {
uc_mgr_free_verb(mgr);
uc_mgr_free_ctl_list(mgr);
goto _reparse;
}
}
_std:
*uc_mgr = mgr;
snd_trace(UCM, "{API call} open '%s' succeed", card_name);
return 0;

View file

@ -466,6 +466,7 @@ static int evaluate_define_macro(snd_use_case_mgr_t *uc_mgr,
err = snd_config_merge(uc_mgr->macros, d, 0);
if (err < 0)
return err;
return 0;
}
@ -2919,6 +2920,24 @@ static int parse_master_file(snd_use_case_mgr_t *uc_mgr, snd_config_t *cfg)
if (err < 0)
return err;
/* parse ValueDefaults first */
err = snd_config_search(cfg, "ValueDefaults", &n);
if (err == 0) {
err = parse_value(uc_mgr, &uc_mgr->value_list, n);
if (err < 0) {
snd_error(UCM, "failed to parse ValueDefaults");
return err;
}
}
err = uc_mgr_check_value(&uc_mgr->value_list, "BootCardGroup");
if (err == 0) {
uc_mgr->card_group = true;
/* if we are in boot, skip the main parsing loop */
if (uc_mgr->in_boot)
return 0;
}
/* parse master config sections */
snd_config_for_each(i, next, cfg) {
@ -2969,13 +2988,8 @@ static int parse_master_file(snd_use_case_mgr_t *uc_mgr, snd_config_t *cfg)
continue;
}
/* get the default values */
/* ValueDefaults is now parsed at the top of this function */
if (strcmp(id, "ValueDefaults") == 0) {
err = parse_value(uc_mgr, &uc_mgr->value_list, n);
if (err < 0) {
snd_error(UCM, "failed to parse ValueDefaults");
return err;
}
continue;
}

View file

@ -462,6 +462,38 @@ boot).
\image html ucm-seq-boot.svg
#### Boot Synchronization (Syntax 8+)
The *BootCardGroup* value in *ValueDefaults* allows multiple sound cards to coordinate
their boot sequences. This value is detected at boot (alsactl/udev/systemd) time. Boot
tools can provide boot synchronization information through a control element named
'Boot' with 64-bit integer type. When present, the UCM library uses this control element
to coordinate initialization timing.
The 'Boot' control element contains:
- **index 0**: Boot time in CLOCK_MONOTONIC_RAW (seconds)
- **index 1**: Restore time in CLOCK_MONOTONIC_RAW (seconds)
- **index 2**: Primary card number (identifies also group)
The UCM open call waits until the boot timeout has passed or until restore state
is notified through the synchronization Boot element. The timeout defaults to 30 seconds
and can be customized using 'BootCardSyncTime' in 'ValueDefaults' (maximum 240 seconds).
If the 'Boot' control element is not present, no boot synchronization is performed.
Other cards in the group (primary card number is different) will have the "Linked"
value set to "1", allowing UCM configuration files to detect and handle secondary
cards appropriately.
Example configuration:
~~~{.html}
ValueDefaults {
BootCardGroup "amd-acp"
BootCardSyncTime 10 # seconds
}
~~~
### Device volume
It is expected that the applications handle the volume settings. It is not recommended

View file

@ -234,6 +234,8 @@ struct snd_use_case_mgr {
const char *parse_variant;
int parse_master_section;
int sequence_hops;
bool in_boot;
bool card_group;
/* UCM cards list */
struct list_head cards_list;
@ -308,7 +310,8 @@ void uc_mgr_free(snd_use_case_mgr_t *uc_mgr);
static inline int uc_mgr_has_local_config(snd_use_case_mgr_t *uc_mgr)
{
return uc_mgr && snd_config_iterator_first(uc_mgr->local_config) !=
return uc_mgr && uc_mgr->local_config &&
snd_config_iterator_first(uc_mgr->local_config) !=
snd_config_iterator_end(uc_mgr->local_config);
}
@ -331,11 +334,14 @@ struct ctl_list *uc_mgr_get_ctl_by_name(snd_use_case_mgr_t *uc_mgr,
const char *name, int idx);
snd_ctl_t *uc_mgr_get_ctl(snd_use_case_mgr_t *uc_mgr);
void uc_mgr_free_ctl_list(snd_use_case_mgr_t *uc_mgr);
int uc_mgr_card_number(struct ctl_list *list);
void uc_mgr_free_value(struct list_head *base);
int uc_mgr_add_value(struct list_head *base, const char *key, char *val);
int uc_mgr_check_value(struct list_head *value_list, const char *identifier);
const char *uc_mgr_get_variable(snd_use_case_mgr_t *uc_mgr,
const char *name);

View file

@ -334,6 +334,13 @@ __nomem:
return -ENOMEM;
}
int uc_mgr_card_number(struct ctl_list *ctl_list)
{
if (ctl_list == NULL)
return -ENOENT;
return snd_ctl_card_info_get_card(ctl_list->ctl_info);
}
const char *uc_mgr_config_dir(int format)
{
const char *path;
@ -908,3 +915,19 @@ const char *uc_mgr_alibcfg_by_device(snd_config_t **top, const char *name)
*top = config;
return name + 9;
}
int uc_mgr_check_value(struct list_head *value_list, const char *identifier)
{
struct ucm_value *val;
struct list_head *pos;
if (!value_list)
return -ENOENT;
list_for_each(pos, value_list) {
val = list_entry(pos, struct ucm_value, list);
if (strcmp(identifier, val->name) == 0)
return 0;
}
return -ENOENT;
}