From: Jaroslav Kysela Date: Mon, 1 Dec 2025 12:32:10 +0000 (+0100) Subject: ucm: implement ValueDefaults.BootCardGroup and define use X-Git-Url: https://git.alsa-project.org/?a=commitdiff_plain;h=554efca4975353a9058849cc0d407ff8dbdcc08c;p=alsa-lib.git 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 --- diff --git a/src/ucm/main.c b/src/ucm/main.c index 9a5dcfa4..cde7dcad 100644 --- a/src/ucm/main.c +++ b/src/ucm/main.c @@ -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; diff --git a/src/ucm/parser.c b/src/ucm/parser.c index 1de09da0..a9be3fea 100644 --- a/src/ucm/parser.c +++ b/src/ucm/parser.c @@ -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; } diff --git a/src/ucm/ucm_confdoc.h b/src/ucm/ucm_confdoc.h index 48dc45a9..894b59a9 100644 --- a/src/ucm/ucm_confdoc.h +++ b/src/ucm/ucm_confdoc.h @@ -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 diff --git a/src/ucm/ucm_local.h b/src/ucm/ucm_local.h index e09e6120..2b30a67f 100644 --- a/src/ucm/ucm_local.h +++ b/src/ucm/ucm_local.h @@ -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); diff --git a/src/ucm/utils.c b/src/ucm/utils.c index f4a1f5eb..2d15a035 100644 --- a/src/ucm/utils.c +++ b/src/ucm/utils.c @@ -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; +}