From 73580520bbeb300bd45b19d2df57699e477e0c88 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Mon, 23 Mar 2026 20:38:09 +0200 Subject: [PATCH 01/94] (---section submitted PRs START) From 2cc0472f6e5e1bef9750a5c46025a8859799e2c4 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 13 Feb 2026 17:44:14 +0200 Subject: [PATCH 02/94] schedule: zephyr_ll_user: make the heap accessible from user-space Rework the heap allocation. Instead of reusing module_driver_heap_init(), allocate the heap in zephyr_ll_user_resources_init(). A copy of heap pointer is stored into user-space accessible memory and zephyr_ll_user_heap_verify() is added to allow kernel code verify the heap object. CONFIG_SOF_ZEPHYR_SYS_USER_HEAP_SIZE is added to Kconfig to control size of the LL user heap. This will initially cover all user-space audio pipeline allocations. Signed-off-by: Kai Vehmanen --- src/include/sof/schedule/ll_schedule_domain.h | 1 + src/schedule/zephyr_ll_user.c | 83 ++++++++++++++++--- zephyr/Kconfig | 11 +++ 3 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/include/sof/schedule/ll_schedule_domain.h b/src/include/sof/schedule/ll_schedule_domain.h index 451ad7739f8f..c759e131b3ee 100644 --- a/src/include/sof/schedule/ll_schedule_domain.h +++ b/src/include/sof/schedule/ll_schedule_domain.h @@ -104,6 +104,7 @@ static inline struct ll_schedule_domain *dma_domain_get(void) #ifdef CONFIG_SOF_USERSPACE_LL struct task *zephyr_ll_task_alloc(void); struct k_heap *zephyr_ll_user_heap(void); +bool zephyr_ll_user_heap_verify(struct k_heap *heap); void zephyr_ll_user_resources_init(void); #endif /* CONFIG_SOF_USERSPACE_LL */ diff --git a/src/schedule/zephyr_ll_user.c b/src/schedule/zephyr_ll_user.c index aa33807b4aa3..b6b00691d7c9 100644 --- a/src/schedule/zephyr_ll_user.c +++ b/src/schedule/zephyr_ll_user.c @@ -17,25 +17,51 @@ LOG_MODULE_DECLARE(ll_schedule, CONFIG_SOF_LOG_LEVEL); * * This structure encapsulates the memory management resources required for the * low-latency (LL) scheduler in userspace mode. It provides memory isolation - * and heap management for LL scheduler threads. + * and heap management for LL scheduler threads. Only kernel accessible. */ struct zephyr_ll_mem_resources { struct k_mem_domain mem_domain; /**< Memory domain for LL thread isolation */ - struct k_heap *heap; /**< Heap allocator for LL scheduler memory */ + struct k_heap heap; /**< Heap allocator for LL scheduler memory */ }; static struct zephyr_ll_mem_resources ll_mem_resources; -static struct k_heap *zephyr_ll_heap_init(void) +/** + * Heap allocator for LL scheduler memory (user accessible pointer) + * + * Note: this is also user-writable, so kernel must not rely on this to + * be correct and must always validate it separately. + */ +APP_SYSUSER_DATA static struct k_heap *zephyr_ll_heap; + +static struct k_heap *ll_heap_alloc(void) { - struct k_heap *heap = module_driver_heap_init(); - struct k_mem_partition mem_partition; - int ret; + const size_t alloc_size = CONFIG_SOF_ZEPHYR_SYS_USER_HEAP_SIZE; + + BUILD_ASSERT(CONFIG_SOF_ZEPHYR_SYS_USER_HEAP_SIZE % CONFIG_MM_DRV_PAGE_SIZE == 0); + + void *mem = rballoc_align(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, alloc_size, + CONFIG_MM_DRV_PAGE_SIZE); + if (!mem) + return NULL; + + k_heap_init(&ll_mem_resources.heap, mem, alloc_size); /* - * TODO: the size of LL heap should be independently configurable and - * not tied to CONFIG_SOF_ZEPHYR_USERSPACE_MODULE_HEAP_SIZE + * k_heap_init() does not set these, so set the values + * manually here */ + ll_mem_resources.heap.heap.init_mem = mem; + ll_mem_resources.heap.heap.init_bytes = alloc_size; + + return &ll_mem_resources.heap; +} + +static void ll_heap_init(void) +{ + struct k_heap *heap = ll_heap_alloc(); + struct k_mem_partition mem_partition; + int ret; if (!heap) { tr_err(&ll_tr, "heap alloc fail"); @@ -60,25 +86,58 @@ static struct k_heap *zephyr_ll_heap_init(void) (void *)mem_partition.start, heap->heap.init_bytes, ret); if (ret) k_panic(); - - return heap; } void zephyr_ll_user_resources_init(void) { + int ret; + k_mem_domain_init(&ll_mem_resources.mem_domain, 0, NULL); - ll_mem_resources.heap = zephyr_ll_heap_init(); + ll_heap_init(); + + /* store a user-accessible pointer */ + zephyr_ll_heap = &ll_mem_resources.heap; /* attach common partition to LL domain */ user_memory_attach_common_partition(zephyr_ll_mem_domain()); + + ret = user_memory_attach_system_user_partition(zephyr_ll_mem_domain()); + if (ret) + k_panic(); +} + +/** + * Check if 'heap' is a valid heap pointer. + * + * Available only in kernel mode. + * + * @return true if valid + */ +bool zephyr_ll_user_heap_verify(struct k_heap *heap) +{ + return heap == &ll_mem_resources.heap; } +/** + * Returns heap object to use in user-space LL code. + * + * Can be called from user-space. + * + * @return heap pointer that can be passed to sof_heap_alloc() + */ struct k_heap *zephyr_ll_user_heap(void) { - return ll_mem_resources.heap; + return zephyr_ll_heap; } +/** + * Returns pointer to LL user-space memory domain. + * + * Available only in kernel mode. + * + * @return pointer to memory domain + */ struct k_mem_domain *zephyr_ll_mem_domain(void) { return &ll_mem_resources.mem_domain; diff --git a/zephyr/Kconfig b/zephyr/Kconfig index 998abae8a969..f1c28f4753d7 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -116,6 +116,17 @@ config SOF_ZEPHYR_USERSPACE_MODULE_HEAP_SIZE module has its own independent heap to which only it has access. This heap is shared between instances of the same module. +config SOF_ZEPHYR_SYS_USER_HEAP_SIZE + hex "Size of the shared LL user-space heap" + default 0x20000 + depends on SOF_USERSPACE_LL + help + The size of the shared heap used by the low-latency (LL) scheduler + user-space thread. This heap is shared across all pipelines running + on the LL thread and must be large enough to hold all host DMA + buffers, chain DMA data, SG elements, and module adapter buffers + for all simultaneously active pipelines. + config SOF_USERSPACE_PROXY bool "Use userspace proxy to support userspace modules" select SOF_USERSPACE_USE_DRIVER_HEAP From 191f33c3ff651a824196fd6862a36b16fec703ba Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 27 May 2026 13:23:20 +0300 Subject: [PATCH 03/94] schedule: zephyr_ll_user: make double-mapping conditional Only double-map the LL resources if CONFIG_CACHE_HAS_MIRRORED_MEMORY_REGIONS is set. Signed-off-by: Kai Vehmanen --- src/schedule/zephyr_ll_user.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/schedule/zephyr_ll_user.c b/src/schedule/zephyr_ll_user.c index b6b00691d7c9..bad2f6c9cbb6 100644 --- a/src/schedule/zephyr_ll_user.c +++ b/src/schedule/zephyr_ll_user.c @@ -79,6 +79,7 @@ static void ll_heap_init(void) if (ret) k_panic(); +#ifdef CONFIG_CACHE_HAS_MIRRORED_MEMORY_REGIONS mem_partition.start = (uintptr_t)sys_cache_uncached_ptr_get(heap->heap.init_mem); mem_partition.attr = K_MEM_PARTITION_P_RW_U_RW; ret = k_mem_domain_add_partition(&ll_mem_resources.mem_domain, &mem_partition); @@ -86,6 +87,7 @@ static void ll_heap_init(void) (void *)mem_partition.start, heap->heap.init_bytes, ret); if (ret) k_panic(); +#endif } void zephyr_ll_user_resources_init(void) From 2f89a5e66183de1c68d0270925ca5f4cea8c0209 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 8 May 2026 16:46:17 +0300 Subject: [PATCH 04/94] zephyr: lib: make sof_heap_alloc/free system calls Add a built option to make sof_heap_alloc/free available as system calls to user-space. Add a test case for the functions that runs in a user-space thread. Signed-off-by: Kai Vehmanen --- zephyr/CMakeLists.txt | 2 + zephyr/Kconfig | 7 +++ zephyr/include/rtos/alloc.h | 17 ++++++ zephyr/lib/alloc.c | 28 ++++++++-- zephyr/syscall/alloc.c | 36 +++++++++++++ zephyr/test/CMakeLists.txt | 3 ++ zephyr/test/userspace/test_heap_alloc.c | 72 +++++++++++++++++++++++++ 7 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 zephyr/syscall/alloc.c create mode 100644 zephyr/test/userspace/test_heap_alloc.c diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index 16575d5e4eec..9bdaa6453ad4 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -620,6 +620,8 @@ zephyr_library_sources_ifdef(CONFIG_SHELL zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/audio/module_adapter/module/generic.h) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/fast-get.h) +zephyr_syscall_header(include/rtos/alloc.h) +zephyr_library_sources_ifdef(CONFIG_SOF_USERSPACE_INTERFACE_ALLOC syscall/alloc.c) zephyr_library_link_libraries(SOF) target_link_libraries(SOF INTERFACE zephyr_interface) diff --git a/zephyr/Kconfig b/zephyr/Kconfig index f1c28f4753d7..4e223ac82aa4 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -29,6 +29,13 @@ config SOF_USERSPACE_INTERFACE_DMA help Allow user-space threads to use the SOF DMA interface. +config SOF_USERSPACE_INTERFACE_ALLOC + bool "Enable SOF heap alloc interface to userspace threads" + depends on USERSPACE + help + Allow user-space threads to use sof_heap_alloc/sof_heap_free + as Zephyr system calls. + config SOF_USERSPACE_LL bool "Run Low-Latency pipelines in userspace threads" depends on USERSPACE diff --git a/zephyr/include/rtos/alloc.h b/zephyr/include/rtos/alloc.h index e21e498f0471..658fc94c9819 100644 --- a/zephyr/include/rtos/alloc.h +++ b/zephyr/include/rtos/alloc.h @@ -99,9 +99,26 @@ void rfree(void *ptr); */ void l3_heap_save(void); +void *z_impl_sof_heap_alloc(struct k_heap *heap, uint32_t flags, size_t bytes, + size_t alignment); + +void z_impl_sof_heap_free(struct k_heap *heap, void *addr); + +/* + * This is ugly to define the signatures twice, but this + * is required to support userspace builds that do not export alloc. + */ +#ifdef CONFIG_SOF_USERSPACE_INTERFACE_ALLOC +__syscall void *sof_heap_alloc(struct k_heap *heap, uint32_t flags, size_t bytes, + size_t alignment); +__syscall void sof_heap_free(struct k_heap *heap, void *addr); +#include +#else void *sof_heap_alloc(struct k_heap *heap, uint32_t flags, size_t bytes, size_t alignment); void sof_heap_free(struct k_heap *heap, void *addr); +#endif + #if CONFIG_SOF_FULL_ZEPHYR_APPLICATION struct k_heap *sof_sys_heap_get(void); #else diff --git a/zephyr/lib/alloc.c b/zephyr/lib/alloc.c index 8192c2caff0f..6aae73db38ad 100644 --- a/zephyr/lib/alloc.c +++ b/zephyr/lib/alloc.c @@ -628,8 +628,8 @@ EXPORT_SYMBOL(rfree); * To match the fall-back SOF main heap all private heaps should also be in the * uncached address range. */ -void *sof_heap_alloc(struct k_heap *heap, uint32_t flags, size_t bytes, - size_t alignment) +void *z_impl_sof_heap_alloc(struct k_heap *heap, uint32_t flags, size_t bytes, + size_t alignment) { if (flags & (SOF_MEM_FLAG_LARGE_BUFFER | SOF_MEM_FLAG_USER_SHARED_BUFFER)) return rballoc_align(flags, bytes, alignment); @@ -643,7 +643,7 @@ void *sof_heap_alloc(struct k_heap *heap, uint32_t flags, size_t bytes, return (__sparse_force void *)heap_alloc_aligned_cached(heap, alignment, bytes); } -void sof_heap_free(struct k_heap *heap, void *addr) +void z_impl_sof_heap_free(struct k_heap *heap, void *addr) { if (heap && addr && is_heap_pointer(heap, addr)) heap_free(heap, addr); @@ -651,6 +651,28 @@ void sof_heap_free(struct k_heap *heap, void *addr) rfree(addr); } +#ifndef CONFIG_SOF_USERSPACE_INTERFACE_ALLOC + +/* + * Putting these as inlines in alloc.h breaks Zephyr native + * ztests like sof/test/ztest/unit/fast-get that include rtos/alloc.h + * but do not link lib/alloc.c . To to keep the tests happy, + * implement the functions here. + */ + +void *sof_heap_alloc(struct k_heap *heap, uint32_t flags, size_t bytes, + size_t alignment) +{ + return z_impl_sof_heap_alloc(heap, falgs, bytes, alignment); +} + +void sof_heap_free(struct k_heap *heap, void *addr) +{ + return z_impl_sof_heap_free(heap, addr); +} + +#endif /* CONFIG_SOF_USERSPACE_INTERFACE_ALLOC */ + static int heap_init(void) { sys_heap_init(&sof_heap.heap, heapmem, HEAPMEM_SIZE - SHARED_BUFFER_HEAP_MEM_SIZE); diff --git a/zephyr/syscall/alloc.c b/zephyr/syscall/alloc.c new file mode 100644 index 000000000000..fd3e486018fb --- /dev/null +++ b/zephyr/syscall/alloc.c @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. + +#include +#include +#include +#include + +static inline void *z_vrfy_sof_heap_alloc(struct k_heap *heap, uint32_t flags, + size_t bytes, size_t alignment) +{ + /* reject flags that bypass heap isolation */ + K_OOPS(flags & (SOF_MEM_FLAG_LARGE_BUFFER | SOF_MEM_FLAG_USER_SHARED_BUFFER)); + + /* user-space use of sof_heap_alloc() limited to this single heap */ + K_OOPS(!zephyr_ll_user_heap_verify(heap)); + + return z_impl_sof_heap_alloc(heap, flags, bytes, alignment); +} +#include + +static inline void z_vrfy_sof_heap_free(struct k_heap *heap, void *addr) +{ + /* user-space use of sof_heap_alloc() limited to this single heap */ + K_OOPS(!zephyr_ll_user_heap_verify(heap)); + + if (addr) { + uintptr_t start = (uintptr_t)heap->heap.init_mem; + uintptr_t addr_uc = (uintptr_t)sys_cache_uncached_ptr_get(addr); + K_OOPS(addr_uc < start || addr_uc >= start + heap->heap.init_bytes); + K_OOPS(K_SYSCALL_MEMORY_WRITE(addr, 1)); + } + z_impl_sof_heap_free(heap, addr); +} +#include diff --git a/zephyr/test/CMakeLists.txt b/zephyr/test/CMakeLists.txt index 6edf90ec840a..397e9e31b938 100644 --- a/zephyr/test/CMakeLists.txt +++ b/zephyr/test/CMakeLists.txt @@ -11,6 +11,9 @@ if(CONFIG_SOF_BOOT_TEST) zephyr_library_sources_ifdef(CONFIG_USERSPACE userspace/ksem.c ) + if(CONFIG_USERSPACE AND CONFIG_SOF_USERSPACE_INTERFACE_ALLOC) + zephyr_library_sources(userspace/test_heap_alloc.c) + endif() endif() if(CONFIG_SOF_BOOT_TEST_STANDALONE AND CONFIG_SOF_USERSPACE_INTERFACE_DMA) diff --git a/zephyr/test/userspace/test_heap_alloc.c b/zephyr/test/userspace/test_heap_alloc.c new file mode 100644 index 000000000000..8018e4b27b9d --- /dev/null +++ b/zephyr/test/userspace/test_heap_alloc.c @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright(c) 2026 Intel Corporation. + */ + +/* + * Test case for sof_heap_alloc() / sof_heap_free() use from a Zephyr + * user-space thread. + */ + +#include +#include +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(sof_boot_test, LOG_LEVEL_DBG); + +#define USER_STACKSIZE 2048 + +static struct k_thread user_thread; +static K_THREAD_STACK_DEFINE(user_stack, USER_STACKSIZE); + +static void user_function(void *p1, void *p2, void *p3) +{ + struct k_heap *heap = (struct k_heap *)p1; + void *ptr; + + __ASSERT(k_is_user_context(), "isn't user"); + + LOG_INF("SOF thread %s (%s)", + k_is_user_context() ? "UserSpace!" : "privileged mode.", + CONFIG_BOARD_TARGET); + + /* allocate a block from the user heap */ + ptr = sof_heap_alloc(heap, SOF_MEM_FLAG_USER, 128, 0); + zassert_not_null(ptr, "sof_heap_alloc returned NULL"); + + LOG_INF("sof_heap_alloc returned %p", ptr); + + /* free the block */ + sof_heap_free(heap, ptr); + + LOG_INF("sof_heap_free done"); +} + +static void test_user_thread_heap_alloc(void) +{ + struct k_heap *heap; + + heap = zephyr_ll_user_heap(); + zassert_not_null(heap, "user heap not found"); + + k_thread_create(&user_thread, user_stack, USER_STACKSIZE, + user_function, heap, NULL, NULL, + -1, K_USER, K_FOREVER); + + /* Add thread to LL memory domain so it can access the user heap */ + k_mem_domain_add_thread(zephyr_ll_mem_domain(), &user_thread); + + k_thread_start(&user_thread); + k_thread_join(&user_thread, K_FOREVER); +} + +ZTEST(sof_boot, user_space_heap_alloc) +{ + test_user_thread_heap_alloc(); + + ztest_test_pass(); +} From 2d1a82c8d1297e98e6c3cc6f148a76cdd0fd6502 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 27 May 2026 12:07:50 +0300 Subject: [PATCH 05/94] zephyr: boot_test: add capability to handle negative test Add infrastructure to handle boot tests that cause a Zephyr fatal error. sof_boot_test_set_fault_valid() can be used by tests to inform the SOF fault handler that a fault is expected and SOF execution should not be stopped. Signed-off-by: Kai Vehmanen --- src/include/sof/boot_test.h | 16 ++++++++++++++++ zephyr/wrapper.c | 26 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/include/sof/boot_test.h b/src/include/sof/boot_test.h index 1af6e7f2d8e0..dfa8671ee55f 100644 --- a/src/include/sof/boot_test.h +++ b/src/include/sof/boot_test.h @@ -13,6 +13,8 @@ #endif #include +struct k_thread; + #if CONFIG_SOF_BOOT_TEST #define TEST_RUN_ONCE(fn, ...) do { \ static bool once; \ @@ -36,4 +38,18 @@ void sof_run_boot_tests(void); +/** + * Mark a boot-test thread as expected to trigger a fatal error. + * + * @param thread Thread that is allowed to fault once, or NULL to clear. + */ +#if CONFIG_SOF_BOOT_TEST +void sof_boot_test_set_fault_valid(struct k_thread *thread); +#else +static inline void sof_boot_test_set_fault_valid(struct k_thread *thread) +{ + (void)thread; +} +#endif + #endif diff --git a/zephyr/wrapper.c b/zephyr/wrapper.c index c0c167b930fe..f43d2ed19a6b 100644 --- a/zephyr/wrapper.c +++ b/zephyr/wrapper.c @@ -6,6 +6,7 @@ */ #include +#include #include #include #include @@ -325,11 +326,36 @@ volatile int *_sof_fatal_null = NULL; struct arch_esf; +#if CONFIG_SOF_BOOT_TEST +static struct k_thread *sof_boot_test_fault_thread; + +void sof_boot_test_set_fault_valid(struct k_thread *thread) +{ + sof_boot_test_fault_thread = thread; +} + +static bool sof_boot_test_fault_expected(void) +{ + if (sof_boot_test_fault_thread != k_current_get()) + return false; + + sof_boot_test_fault_thread = NULL; + return true; +} +#endif + void k_sys_fatal_error_handler(unsigned int reason, const struct arch_esf *esf) { ARG_UNUSED(esf); +#if CONFIG_SOF_BOOT_TEST + if (sof_boot_test_fault_expected()) { + LOG_ERR("Expected fatal error as part of boot test"); + return; + } +#endif + /* flush and switch to immediate mode */ LOG_PANIC(); From 0d5f1a8690dd1511f730d8d3b4fa90c688d3b18e Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 27 May 2026 12:09:49 +0300 Subject: [PATCH 06/94] zephyr: test: userspace: add negative tests for sof_heap_alloc() Add negative tests to cover system call validation for heap allocation. Signed-off-by: Kai Vehmanen --- zephyr/test/userspace/test_heap_alloc.c | 133 ++++++++++++++++++++---- 1 file changed, 114 insertions(+), 19 deletions(-) diff --git a/zephyr/test/userspace/test_heap_alloc.c b/zephyr/test/userspace/test_heap_alloc.c index 8018e4b27b9d..54f9aa7e6d43 100644 --- a/zephyr/test/userspace/test_heap_alloc.c +++ b/zephyr/test/userspace/test_heap_alloc.c @@ -4,7 +4,7 @@ */ /* - * Test case for sof_heap_alloc() / sof_heap_free() use from a Zephyr + * Test cases for sof_heap_alloc() / sof_heap_free() use from a Zephyr * user-space thread. */ @@ -15,6 +15,7 @@ #include #include #include +#include LOG_MODULE_DECLARE(sof_boot_test, LOG_LEVEL_DBG); @@ -23,9 +24,22 @@ LOG_MODULE_DECLARE(sof_boot_test, LOG_LEVEL_DBG); static struct k_thread user_thread; static K_THREAD_STACK_DEFINE(user_stack, USER_STACKSIZE); +K_APPMEM_PARTITION_DEFINE(heap_alloc_test_part); +K_APP_BMEM(heap_alloc_test_part) static uint8_t non_heap_byte; + +enum heap_alloc_test_case { + HEAP_ALLOC_VALID, + HEAP_ALLOC_NULL_HEAP, + HEAP_ALLOC_FORBIDDEN_FLAGS, + HEAP_FREE_NON_HEAP_POINTER, + HEAP_FREE_INACCESSIBLE_POINTER, +}; + static void user_function(void *p1, void *p2, void *p3) { - struct k_heap *heap = (struct k_heap *)p1; + struct k_heap *heap = p1; + enum heap_alloc_test_case test_case = (enum heap_alloc_test_case)(uintptr_t)p2; + void *free_ptr = p3; void *ptr; __ASSERT(k_is_user_context(), "isn't user"); @@ -34,39 +48,120 @@ static void user_function(void *p1, void *p2, void *p3) k_is_user_context() ? "UserSpace!" : "privileged mode.", CONFIG_BOARD_TARGET); - /* allocate a block from the user heap */ - ptr = sof_heap_alloc(heap, SOF_MEM_FLAG_USER, 128, 0); - zassert_not_null(ptr, "sof_heap_alloc returned NULL"); + switch (test_case) { + case HEAP_ALLOC_VALID: + ptr = sof_heap_alloc(heap, SOF_MEM_FLAG_USER, 128, 0); + zassert_not_null(ptr, "sof_heap_alloc returned NULL"); + sof_heap_free(heap, ptr); + return; + case HEAP_ALLOC_NULL_HEAP: + (void)sof_heap_alloc(NULL, SOF_MEM_FLAG_USER, 128, 0); + break; + case HEAP_ALLOC_FORBIDDEN_FLAGS: + (void)sof_heap_alloc(heap, SOF_MEM_FLAG_LARGE_BUFFER, 128, 0); + break; + case HEAP_FREE_NON_HEAP_POINTER: + sof_heap_free(heap, &non_heap_byte); + break; + case HEAP_FREE_INACCESSIBLE_POINTER: + sof_heap_free(heap, free_ptr); + break; + default: + zassert_unreachable("unknown heap allocation test case"); + } + + zassert_unreachable("syscall security check did not fault"); +} - LOG_INF("sof_heap_alloc returned %p", ptr); +static void run_user_heap_alloc_case(enum heap_alloc_test_case test_case, struct k_heap *heap, + void *ptr, bool grant_ll_domain, bool expect_fault) +{ + int ret; - /* free the block */ - sof_heap_free(heap, ptr); + k_thread_create(&user_thread, user_stack, USER_STACKSIZE, + user_function, heap, (void *)(uintptr_t)test_case, ptr, + -1, K_USER, K_FOREVER); + + if (grant_ll_domain) + k_mem_domain_add_thread(zephyr_ll_mem_domain(), &user_thread); - LOG_INF("sof_heap_free done"); + if (expect_fault) + sof_boot_test_set_fault_valid(&user_thread); + + k_thread_start(&user_thread); + ret = k_thread_join(&user_thread, K_FOREVER); + zassert_equal(ret, 0, "user thread join failed: %d", ret); } -static void test_user_thread_heap_alloc(void) +static void test_user_thread_heap_alloc(enum heap_alloc_test_case test_case) { + bool grant_ll_domain = true; + bool expect_fault = false; + void *ptr = NULL; struct k_heap *heap; heap = zephyr_ll_user_heap(); zassert_not_null(heap, "user heap not found"); - k_thread_create(&user_thread, user_stack, USER_STACKSIZE, - user_function, heap, NULL, NULL, - -1, K_USER, K_FOREVER); + switch (test_case) { + case HEAP_ALLOC_VALID: + break; + case HEAP_ALLOC_NULL_HEAP: + case HEAP_ALLOC_FORBIDDEN_FLAGS: + case HEAP_FREE_NON_HEAP_POINTER: + expect_fault = true; + break; + case HEAP_FREE_INACCESSIBLE_POINTER: + ptr = sof_heap_alloc(heap, SOF_MEM_FLAG_USER, 128, 0); + zassert_not_null(ptr, "kernel heap allocation failed"); + grant_ll_domain = false; + expect_fault = true; + break; + default: + zassert_unreachable("unknown heap allocation test case"); + } + + run_user_heap_alloc_case(test_case, heap, ptr, grant_ll_domain, expect_fault); + + if (ptr) + sof_heap_free(heap, ptr); +} - /* Add thread to LL memory domain so it can access the user heap */ - k_mem_domain_add_thread(zephyr_ll_mem_domain(), &user_thread); +ZTEST(sof_boot, user_space_heap_alloc) +{ + test_user_thread_heap_alloc(HEAP_ALLOC_VALID); - k_thread_start(&user_thread); - k_thread_join(&user_thread, K_FOREVER); + ztest_test_pass(); } -ZTEST(sof_boot, user_space_heap_alloc) +ZTEST(sof_boot, user_space_heap_alloc_rejects_null_heap) +{ + test_user_thread_heap_alloc(HEAP_ALLOC_NULL_HEAP); + + ztest_test_pass(); +} + +ZTEST(sof_boot, user_space_heap_alloc_rejects_forbidden_flags) +{ + test_user_thread_heap_alloc(HEAP_ALLOC_FORBIDDEN_FLAGS); + + ztest_test_pass(); +} + +ZTEST(sof_boot, user_space_heap_free_rejects_non_heap_pointer) +{ + k_mem_domain_add_partition(zephyr_ll_mem_domain(), &heap_alloc_test_part); + + test_user_thread_heap_alloc(HEAP_FREE_NON_HEAP_POINTER); + + k_mem_domain_remove_partition(zephyr_ll_mem_domain(), &heap_alloc_test_part); + + ztest_test_pass(); +} + +ZTEST(sof_boot, user_space_heap_free_rejects_inaccessible_pointer) { - test_user_thread_heap_alloc(); + test_user_thread_heap_alloc(HEAP_FREE_INACCESSIBLE_POINTER); ztest_test_pass(); } From f629c90ec5141f62f845a7cbfce25fa09628e894 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 26 May 2026 19:35:35 +0300 Subject: [PATCH 07/94] zephyr: set SOF_USERSPACE_INTERFACE_ALLOC for LL user builds Enable CONFIG_SOF_USERSPACE_INTERFACE_ALLOC to allow user-space pipeline code to allocate memory with sof_heap_alloc(). Signed-off-by: Kai Vehmanen --- zephyr/Kconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/zephyr/Kconfig b/zephyr/Kconfig index 4e223ac82aa4..27f37d829b4e 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -39,6 +39,7 @@ config SOF_USERSPACE_INTERFACE_ALLOC config SOF_USERSPACE_LL bool "Run Low-Latency pipelines in userspace threads" depends on USERSPACE + select SOF_USERSPACE_INTERFACE_ALLOC select SOF_USERSPACE_INTERFACE_DMA help Run Low-Latency (LL) pipelines in userspace threads. This adds From 79479dbe3b6622b4dd6718a34ced6bbb7e169389 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 26 Feb 2026 17:45:16 +0200 Subject: [PATCH 08/94] schedule: allocate the scheduler objects with sof_heap_alloc() Ensure the scheduler objects and lists of schedulers are allocated such that they can be used with both kernel and user-space LL scheduler implementations. The SOF_MEM_FLAG_KERNEL flag is remevod. This flag has been a no-op for a while, and given scheduler list is not always in kernel anymore, it would be highly confusing to keep it. When CONFIG_SOF_USERSPACE_LL is set, the context of all schedulers is managed in the LL user-space domain. Signed-off-by: Kai Vehmanen --- src/schedule/schedule.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/schedule/schedule.c b/src/schedule/schedule.c index 5e56b6c490e0..7bcc30d755aa 100644 --- a/src/schedule/schedule.c +++ b/src/schedule/schedule.c @@ -47,15 +47,19 @@ int schedule_task_init(struct task *task, static void scheduler_register(struct schedule_data *scheduler) { struct schedulers **sch = arch_schedulers_get(); + struct k_heap *heap = NULL; +#ifdef CONFIG_SOF_USERSPACE_LL + heap = sof_sys_user_heap_get(); +#endif if (!*sch) { /* init schedulers list */ - *sch = rzalloc(SOF_MEM_FLAG_KERNEL, - sizeof(**sch)); + *sch = sof_heap_alloc(heap, 0, sizeof(**sch), 0); if (!*sch) { tr_err(&sch_tr, "allocation failed"); return; } + memset(*sch, 0, sizeof(**sch)); list_init(&(*sch)->list); } @@ -65,16 +69,21 @@ static void scheduler_register(struct schedule_data *scheduler) void scheduler_init(int type, const struct scheduler_ops *ops, void *data) { struct schedule_data *sch; + struct k_heap *heap = NULL; +#ifdef CONFIG_SOF_USERSPACE_LL + heap = sof_sys_user_heap_get(); +#endif if (!ops || !ops->schedule_task || !ops->schedule_task_cancel || !ops->schedule_task_free) return; - sch = rzalloc(SOF_MEM_FLAG_KERNEL, sizeof(*sch)); + sch = sof_heap_alloc(heap, SOF_MEM_FLAG_KERNEL, sizeof(*sch), 0); if (!sch) { tr_err(&sch_tr, "allocation failed"); sof_panic(SOF_IPC_PANIC_IPC); } + memset(sch, 0, sizeof(*sch)); list_init(&sch->list); sch->type = type; sch->ops = ops; From 96e2fb97006a670a4b8bf45ae29dd123200d3bbe Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 26 Feb 2026 18:00:10 +0200 Subject: [PATCH 09/94] zephyr: wrapper: modify platform_dai_wallclock() for user-space Don't use sof_cycle_get_64() if SOF built for user-space LL. Signed-off-by: Kai Vehmanen --- zephyr/wrapper.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zephyr/wrapper.c b/zephyr/wrapper.c index f43d2ed19a6b..349b27e44eab 100644 --- a/zephyr/wrapper.c +++ b/zephyr/wrapper.c @@ -266,7 +266,11 @@ void platform_dai_timestamp(struct comp_dev *dai, /* get current wallclock for componnent */ void platform_dai_wallclock(struct comp_dev *dai, uint64_t *wallclock) { +#ifndef CONFIG_SOF_USERSPACE_LL *wallclock = sof_cycle_get_64(); +#else + *wallclock = k_uptime_get(); +#endif } /* From 41a87d95ea62dea4de8deae973b5262f064d6e6a Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 27 Feb 2026 16:59:02 +0200 Subject: [PATCH 10/94] schedule: add scheduler_init_context() Add an optional method that allow the schedule.h user to get access to the thread context that will be used for scheduling. This is critical when the callbacks are run in user-space context and schedule.h client needs to grant access to objects like locks to the callback thread. Signed-off-by: Kai Vehmanen --- src/include/sof/schedule/schedule.h | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/include/sof/schedule/schedule.h b/src/include/sof/schedule/schedule.h index bbdcbbecf3b4..9418064beb02 100644 --- a/src/include/sof/schedule/schedule.h +++ b/src/include/sof/schedule/schedule.h @@ -158,6 +158,16 @@ struct scheduler_ops { * This operation is optional. */ int (*scheduler_restore)(void *data); + + /** + * Initializes context + * @param data Private data of selected scheduler. + * @param task task that needs to be scheduled + * @return thread that will be used to run the scheduled task + * + * This operation is optional. + */ + struct k_thread *(*scheduler_init_context)(void *data, struct task *task); }; /** \brief Holds information about scheduler. */ @@ -379,6 +389,24 @@ static inline int schedulers_restore(void) return 0; } +/** See scheduler_ops::scheduler_init_context */ +static inline struct k_thread *scheduler_init_context(struct task *task) +{ + struct schedulers *schedulers = *arch_schedulers_get(); + struct schedule_data *sch; + struct list_item *slist; + + assert(schedulers); + + list_for_item(slist, &schedulers->list) { + sch = container_of(slist, struct schedule_data, list); + if (task->type == sch->type && sch->ops->scheduler_init_context) + return sch->ops->scheduler_init_context(sch->data, task); + } + + return NULL; +} + /** * Initializes scheduling task. * @param task Task to be initialized. From 34430baa4f4e7a400ae15bf6fe615eefe345175f Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 4 Mar 2026 19:25:43 +0200 Subject: [PATCH 11/94] schedule: ll_schedule_domain: add domain_thread_init/free ops Add new domain ops that are called from privileged context and are used to set up resources like threads and initialize other kernel objects. Signed-off-by: Kai Vehmanen --- src/include/sof/schedule/ll_schedule_domain.h | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/include/sof/schedule/ll_schedule_domain.h b/src/include/sof/schedule/ll_schedule_domain.h index c759e131b3ee..1367c408899f 100644 --- a/src/include/sof/schedule/ll_schedule_domain.h +++ b/src/include/sof/schedule/ll_schedule_domain.h @@ -47,6 +47,20 @@ struct ll_schedule_domain_ops { void (*handler)(void *arg), void *arg); int (*domain_unregister)(struct ll_schedule_domain *domain, struct task *task, uint32_t num_tasks); +#if CONFIG_SOF_USERSPACE_LL + /* + * Initialize the scheduling thread and perform all privileged setup + * (thread creation, timer init, access grants). Called once from + * kernel context before any user-space domain_register() calls. + */ + int (*domain_thread_init)(struct ll_schedule_domain *domain, + struct task *task); + /* Free resources acquired by domain_thread_init(). Called from + * kernel context when the scheduling context is being torn down. + */ + void (*domain_thread_free)(struct ll_schedule_domain *domain, + uint32_t num_tasks); +#endif void (*domain_enable)(struct ll_schedule_domain *domain, int core); void (*domain_disable)(struct ll_schedule_domain *domain, int core); #if CONFIG_CROSS_CORE_STREAM @@ -182,6 +196,31 @@ static inline void domain_task_cancel(struct ll_schedule_domain *domain, domain->ops->domain_task_cancel(domain, task); } +#if CONFIG_SOF_USERSPACE_LL +/* + * Initialize the scheduling thread and do all privileged setup. + * Must be called from kernel context before user-space tasks register. + */ +static inline int domain_thread_init(struct ll_schedule_domain *domain, + struct task *task) +{ + assert(domain->ops->domain_thread_init); + + return domain->ops->domain_thread_init(domain, task); +} + +/* + * Free resources acquired by domain_thread_init(). + * Must be called from kernel context. + */ +static inline void domain_thread_free(struct ll_schedule_domain *domain, + uint32_t num_tasks) +{ + if (domain->ops->domain_thread_free) + domain->ops->domain_thread_free(domain, num_tasks); +} +#endif + static inline int domain_register(struct ll_schedule_domain *domain, struct task *task, void (*handler)(void *arg), void *arg) From 349d85b88ab13b3d385b98143d761fc7e41908f6 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Mon, 25 May 2026 13:52:46 +0300 Subject: [PATCH 12/94] schedule: zephyr_ll: implement user/kernel split with init_context() Use the new scheduler_init_context() and domain_thread_init/free() interfaces to separate LL scheduler logic into privileged and unprivileged parts. The latter can be used by the audio pipeline to add, remove and schedule tasks from the user-space audio thread. The privileged interfaces are used to set up the scheduler during firmware boot. Signed-off-by: Kai Vehmanen --- src/schedule/zephyr_domain.c | 172 ++++++++++++++++++++++++----------- src/schedule/zephyr_ll.c | 48 +++++++--- 2 files changed, 158 insertions(+), 62 deletions(-) diff --git a/src/schedule/zephyr_domain.c b/src/schedule/zephyr_domain.c index ebf5aea8d4d2..daace8bac708 100644 --- a/src/schedule/zephyr_domain.c +++ b/src/schedule/zephyr_domain.c @@ -126,7 +126,8 @@ static void zephyr_domain_thread_fn(void *p1, void *p2, void *p3) } #endif - dt->handler(dt->arg); + if (dt->handler) + dt->handler(dt->arg); #ifdef CONFIG_SCHEDULE_LL_STATS_LOG cycles1 = k_cycle_get_32(); @@ -289,63 +290,65 @@ static int zephyr_domain_unregister(struct ll_schedule_domain *domain, #else /* CONFIG_SOF_USERSPACE_LL */ -/* User-space implementation for register/unregister */ - -static int zephyr_domain_register_user(struct ll_schedule_domain *domain, - struct task *task, - void (*handler)(void *arg), void *arg) +/* + * Privileged thread initialization for userspace LL scheduling. + * Creates the scheduling thread, sets up timer, grants access to kernel + * objects. Must be called from kernel context before any user-space + * domain_register() calls. + */ +static int zephyr_domain_thread_init(struct ll_schedule_domain *domain, + struct task *task) { struct zephyr_domain *zephyr_domain = ll_sch_domain_get_pdata(domain); - int core = cpu_get_id(); - struct zephyr_domain_thread *dt = zephyr_domain->domain_thread + core; + struct zephyr_domain_thread *dt; char thread_name[] = "ll_thread0"; k_tid_t thread; + int core = task->core; - tr_dbg(&ll_tr, "entry"); + tr_dbg(&ll_tr, "thread_init entry"); - /* domain work only needs registered once on each core */ - if (dt->handler) - return 0; + if (core >= CONFIG_CORE_COUNT) + return -EINVAL; - __ASSERT_NO_MSG(task->core == core); + dt = zephyr_domain->domain_thread + core; - dt->handler = handler; - dt->arg = arg; + /* thread only needs to be created once per core */ + if (dt->ll_thread) + return 0; + + dt->handler = NULL; + dt->arg = NULL; /* 10 is rather random, we better not accumulate 10 missed timer interrupts */ k_sem_init(dt->sem, 0, 10); thread_name[sizeof(thread_name) - 2] = '0' + core; + /* Allocate thread structure dynamically */ + dt->ll_thread = k_object_alloc(K_OBJ_THREAD); if (!dt->ll_thread) { - /* Allocate thread structure dynamically */ - dt->ll_thread = k_object_alloc(K_OBJ_THREAD); - if (!dt->ll_thread) { - tr_err(&ll_tr, "Failed to allocate thread object for core %d", core); - dt->handler = NULL; - dt->arg = NULL; - return -ENOMEM; - } + tr_err(&ll_tr, "Failed to allocate thread object for core %d", core); + return -ENOMEM; + } - thread = k_thread_create(dt->ll_thread, ll_sched_stack[core], ZEPHYR_LL_STACK_SIZE, - zephyr_domain_thread_fn, zephyr_domain, - INT_TO_POINTER(core), NULL, CONFIG_LL_THREAD_PRIORITY, - K_USER, K_FOREVER); + thread = k_thread_create(dt->ll_thread, ll_sched_stack[core], ZEPHYR_LL_STACK_SIZE, + zephyr_domain_thread_fn, zephyr_domain, + INT_TO_POINTER(core), NULL, CONFIG_LL_THREAD_PRIORITY, + K_USER, K_FOREVER); #ifdef CONFIG_SCHED_CPU_MASK - k_thread_cpu_mask_clear(thread); - k_thread_cpu_mask_enable(thread, core); + k_thread_cpu_mask_clear(thread); + k_thread_cpu_mask_enable(thread, core); #endif - k_thread_name_set(thread, thread_name); + k_thread_name_set(thread, thread_name); - k_mem_domain_add_thread(zephyr_ll_mem_domain(), thread); - k_thread_access_grant(thread, dt->sem, domain->lock, zephyr_domain->timer); - user_grant_dai_access_all(thread); - user_grant_dma_access_all(thread); - tr_dbg(&ll_tr, "granted LL access to thread %p (core %d)", thread, core); + k_mem_domain_add_thread(zephyr_ll_mem_domain(), thread); + k_thread_access_grant(thread, dt->sem, domain->lock, zephyr_domain->timer); + user_grant_dai_access_all(thread); + user_grant_dma_access_all(thread); + tr_dbg(&ll_tr, "granted LL access to thread %p (core %d)", thread, core); - k_thread_start(thread); - } + k_thread_start(thread); k_mutex_lock(domain->lock, K_FOREVER); if (!k_timer_user_data_get(zephyr_domain->timer)) { @@ -368,6 +371,43 @@ static int zephyr_domain_register_user(struct ll_schedule_domain *domain, return 0; } +/* + * User-space register: bookkeeping only. The privileged thread setup has + * already been done by domain_thread_init() called from kernel context. + */ +static int zephyr_domain_register_user(struct ll_schedule_domain *domain, + struct task *task, + void (*handler)(void *arg), void *arg) +{ + struct zephyr_domain *zephyr_domain = ll_sch_domain_get_pdata(domain); + struct zephyr_domain_thread *dt; + int core; + + tr_dbg(&ll_tr, "register_user entry"); + + if (task->core >= CONFIG_CORE_COUNT) + return -EINVAL; + + core = task->core; + dt = zephyr_domain->domain_thread + core; + + if (!dt->ll_thread) { + tr_err(&ll_tr, "domain_thread_init() not called for core %d", core); + return -EINVAL; + } + + __ASSERT_NO_MSG(!dt->handler || dt->handler == handler); + if (dt->handler) + return 0; + + dt->handler = handler; + dt->arg = arg; + + tr_info(&ll_tr, "task registered on core %d", core); + + return 0; +} + static int zephyr_domain_unregister_user(struct ll_schedule_domain *domain, struct task *task, uint32_t num_tasks) { @@ -382,30 +422,58 @@ static int zephyr_domain_unregister_user(struct ll_schedule_domain *domain, k_mutex_lock(domain->lock, K_FOREVER); - if (!atomic_read(&domain->total_num_tasks)) { - /* Disable the watchdog */ + zephyr_domain->domain_thread[core].handler = NULL; + + k_mutex_unlock(domain->lock); + + /* + * In this user thread implementation, the timer is left + * running until privileged domain_thread_free() is called + * to clean up resources. + */ + + tr_dbg(&ll_tr, "exit"); + + return 0; +} + +/* + * Free resources acquired by zephyr_domain_thread_init(). + * Stops the timer, aborts the scheduling thread and frees the thread object. + * Must be called from kernel context. + */ +static void zephyr_domain_thread_free(struct ll_schedule_domain *domain, + uint32_t num_tasks) +{ + struct zephyr_domain *zephyr_domain = ll_sch_domain_get_pdata(domain); + int core = cpu_get_id(); + struct zephyr_domain_thread *dt = zephyr_domain->domain_thread + core; + + tr_dbg(&ll_tr, "thread_free entry, core %d, num_tasks %u", core, num_tasks); + + /* Still tasks on other cores, only clean up this core's thread */ + k_mutex_lock(domain->lock, K_FOREVER); + + if (!num_tasks && !atomic_read(&domain->total_num_tasks)) { + /* Last task globally: stop the timer and watchdog */ watchdog_disable(core); k_timer_stop(zephyr_domain->timer); k_timer_user_data_set(zephyr_domain->timer, NULL); } - zephyr_domain->domain_thread[core].handler = NULL; + dt->handler = NULL; + dt->arg = NULL; k_mutex_unlock(domain->lock); - tr_info(&ll_tr, "domain->type %d domain->clk %d", - domain->type, domain->clk); - - /* Thread not removed here, only the timer is stopped. - * Thread object cleanup would require k_thread_abort() which cannot - * be safely called from this context. The thread remains allocated - * but dormant until next registration or system shutdown. - */ - - tr_dbg(&ll_tr, "exit"); + if (dt->ll_thread) { + k_thread_abort(dt->ll_thread); + k_object_free(dt->ll_thread); + dt->ll_thread = NULL; + } - return 0; + tr_info(&ll_tr, "thread_free done, core %d", core); } struct k_thread *zephyr_domain_thread_tid(struct ll_schedule_domain *domain) @@ -450,6 +518,8 @@ APP_TASK_DATA static const struct ll_schedule_domain_ops zephyr_domain_ops = { #ifdef CONFIG_SOF_USERSPACE_LL .domain_register = zephyr_domain_register_user, .domain_unregister = zephyr_domain_unregister_user, + .domain_thread_init = zephyr_domain_thread_init, + .domain_thread_free = zephyr_domain_thread_free, #else .domain_register = zephyr_domain_register, .domain_unregister = zephyr_domain_unregister, diff --git a/src/schedule/zephyr_ll.c b/src/schedule/zephyr_ll.c index 575a82d91dda..1c1717d30ac6 100644 --- a/src/schedule/zephyr_ll.c +++ b/src/schedule/zephyr_ll.c @@ -360,17 +360,7 @@ static int zephyr_ll_task_schedule_common(struct zephyr_ll *sch, struct task *ta ret = domain_register(sch->ll_domain, task, &schedule_ll_callback, sch); if (ret < 0) - tr_err(&ll_tr, "cannot register domain %d", - ret); - -#if CONFIG_SOF_USERSPACE_LL - k_thread_access_grant(zephyr_domain_thread_tid(sch->ll_domain), sch->lock); - - tr_dbg(&ll_tr, "granting access to lock %p for thread %p", sch->lock, - zephyr_domain_thread_tid(sch->ll_domain)); - tr_dbg(&ll_tr, "granting access to domain lock %p for thread %p", &sch->ll_domain->lock, - zephyr_domain_thread_tid(sch->ll_domain)); -#endif + tr_err(&ll_tr, "cannot register domain %d", ret); return 0; } @@ -509,8 +499,41 @@ static void zephyr_ll_scheduler_free(void *data, uint32_t flags) if (sch->n_tasks) tr_err(&ll_tr, "%u tasks are still active!", sch->n_tasks); + +#if CONFIG_SOF_USERSPACE_LL + domain_thread_free(sch->ll_domain, sch->n_tasks); +#endif } +#if CONFIG_SOF_USERSPACE_LL +struct k_thread *zephyr_ll_init_context(void *data, struct task *task) +{ + struct zephyr_ll *sch = data; + int ret; + + /* + * Use domain_thread_init() for privileged setup (thread creation, + * timer, access grants). domain_register() is now bookkeeping only + * and will be called later from user context when scheduling tasks. + */ + ret = domain_thread_init(sch->ll_domain, task); + if (ret < 0) { + tr_err(&ll_tr, "cannot init_context %d", ret); + return NULL; + } + + assert(!k_is_user_context()); + k_thread_access_grant(zephyr_domain_thread_tid(sch->ll_domain), sch->lock); + + tr_dbg(&ll_tr, "granting access to lock %p for thread %p", sch->lock, + zephyr_domain_thread_tid(sch->ll_domain)); + tr_dbg(&ll_tr, "granting access to domain lock %p for thread %p", &sch->ll_domain->lock, + zephyr_domain_thread_tid(sch->ll_domain)); + + return zephyr_domain_thread_tid(sch->ll_domain); +} +#endif + static const struct scheduler_ops zephyr_ll_ops = { .schedule_task = zephyr_ll_task_schedule, .schedule_task_before = zephyr_ll_task_schedule_before, @@ -518,6 +541,9 @@ static const struct scheduler_ops zephyr_ll_ops = { .schedule_task_free = zephyr_ll_task_free, .schedule_task_cancel = zephyr_ll_task_cancel, .scheduler_free = zephyr_ll_scheduler_free, +#if CONFIG_SOF_USERSPACE_LL + .scheduler_init_context = zephyr_ll_init_context, +#endif }; #if CONFIG_SOF_USERSPACE_LL From 2fc6d0f605a8bf0fa1c545c518873b2245d33c26 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 31 Mar 2026 18:29:20 +0300 Subject: [PATCH 13/94] schedule: zephyr_ll: ISR check is not needed when LL in user-space No need to check whether we are running in ISR as this can never happen when LL scheduler is run in user-space. Signed-off-by: Kai Vehmanen --- src/schedule/zephyr_ll.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/schedule/zephyr_ll.c b/src/schedule/zephyr_ll.c index 1c1717d30ac6..0a6fd3f14221 100644 --- a/src/schedule/zephyr_ll.c +++ b/src/schedule/zephyr_ll.c @@ -402,10 +402,12 @@ static int zephyr_ll_task_free(void *data, struct task *task) zephyr_ll_assert_core(sch); +#ifndef CONFIG_SOF_USERSPACE_LL if (k_is_in_isr()) { tr_err(&ll_tr, "cannot free tasks from interrupt context!"); return -EDEADLK; } +#endif zephyr_ll_lock(sch, &flags); From 662b981fe39ddbce0443ecc705091961d07d7f87 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Mon, 23 Mar 2026 20:38:09 +0200 Subject: [PATCH 14/94] (---section submitted PRs STOP) From 21b021d7b5661a2eaec6cd6359dfc24e2d758cbc Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 19 Feb 2026 14:51:22 +0200 Subject: [PATCH 15/94] (---section dai-zephyr START) From d7ab8fa38a5321833241211ec5e2d3ca332427de Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 17 Feb 2026 21:41:58 +0200 Subject: [PATCH 16/94] audio: dai-zephyr: convert spinlock into mutex for properties The spinlock used to protect access to DAI properties can be converted to a mutex as this is only accessed from IPC and LL threads and both are normal Zephyr threads. As an additional benefit, use of mutex allows to run the dai-zephyr module in user-space. Signed-off-by: Kai Vehmanen --- src/audio/dai-zephyr.c | 53 +++++++++++++++++++------------- src/include/sof/lib/dai-zephyr.h | 5 ++- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/audio/dai-zephyr.c b/src/audio/dai-zephyr.c index e49d3e0f0122..659e710be05c 100644 --- a/src/audio/dai-zephyr.c +++ b/src/audio/dai-zephyr.c @@ -229,12 +229,13 @@ __cold int dai_set_config(struct dai *dai, struct ipc_config_dai *common_config, /* called from ipc/ipc3/dai.c */ int dai_get_handshake(struct dai *dai, int direction, int stream_id) { - k_spinlock_key_t key = k_spin_lock(&dai->lock); - const struct dai_properties *props = dai_get_properties(dai->dev, direction, - stream_id); - int hs_id = props->dma_hs_id; + const struct dai_properties *props; + int hs_id; - k_spin_unlock(&dai->lock, key); + k_mutex_lock(dai->lock, K_FOREVER); + props = dai_get_properties(dai->dev, direction, stream_id); + hs_id = props->dma_hs_id; + k_mutex_unlock(dai->lock); return hs_id; } @@ -243,39 +244,41 @@ int dai_get_handshake(struct dai *dai, int direction, int stream_id) int dai_get_fifo_depth(struct dai *dai, int direction) { const struct dai_properties *props; - k_spinlock_key_t key; int fifo_depth; if (!dai) return 0; - key = k_spin_lock(&dai->lock); + k_mutex_lock(dai->lock, K_FOREVER); props = dai_get_properties(dai->dev, direction, 0); fifo_depth = props->fifo_depth; - k_spin_unlock(&dai->lock, key); + k_mutex_unlock(dai->lock); return fifo_depth; } int dai_get_stream_id(struct dai *dai, int direction) { - k_spinlock_key_t key = k_spin_lock(&dai->lock); - const struct dai_properties *props = dai_get_properties(dai->dev, direction, 0); - int stream_id = props->stream_id; + const struct dai_properties *props; + int stream_id; - k_spin_unlock(&dai->lock, key); + k_mutex_lock(dai->lock, K_FOREVER); + props = dai_get_properties(dai->dev, direction, 0); + stream_id = props->stream_id; + k_mutex_unlock(dai->lock); return stream_id; } static int dai_get_fifo(struct dai *dai, int direction, int stream_id) { - k_spinlock_key_t key = k_spin_lock(&dai->lock); - const struct dai_properties *props = dai_get_properties(dai->dev, direction, - stream_id); - int fifo_address = props->fifo_address; + const struct dai_properties *props; + int fifo_address; - k_spin_unlock(&dai->lock, key); + k_mutex_lock(dai->lock, K_FOREVER); + props = dai_get_properties(dai->dev, direction, stream_id); + fifo_address = props->fifo_address; + k_mutex_unlock(dai->lock); return fifo_address; } @@ -532,7 +535,12 @@ __cold int dai_common_new(struct dai_data *dd, struct comp_dev *dev, return -ENODEV; } - k_spinlock_init(&dd->dai->lock); +#ifdef CONFIG_SOF_USERSPACE_LL + dd->dai->lock = k_object_alloc(K_OBJ_MUTEX); +#else + dd->dai->lock = &dd->dai->lock_obj; +#endif + k_mutex_init(dd->dai->lock); dma_sg_init(&dd->config.elem_array); dd->xrun = 0; @@ -658,6 +666,10 @@ __cold void dai_common_free(struct dai_data *dd) dai_release_llp_slot(dd); +#ifdef CONFIG_SOF_USERSPACE_LL + k_object_free(dd->dai->lock); +#endif + dai_put(dd->dai); sof_heap_free(dd->alloc_ctx.heap, dd->dai_spec_config); @@ -1974,16 +1986,15 @@ static int dai_ts_stop_op(struct comp_dev *dev) uint32_t dai_get_init_delay_ms(struct dai *dai) { const struct dai_properties *props; - k_spinlock_key_t key; uint32_t init_delay; if (!dai) return 0; - key = k_spin_lock(&dai->lock); + k_mutex_lock(dai->lock, K_FOREVER); props = dai_get_properties(dai->dev, 0, 0); init_delay = props->reg_init_delay; - k_spin_unlock(&dai->lock, key); + k_mutex_unlock(dai->lock); return init_delay; } diff --git a/src/include/sof/lib/dai-zephyr.h b/src/include/sof/lib/dai-zephyr.h index 595d11de9b47..16e52a9ea6ca 100644 --- a/src/include/sof/lib/dai-zephyr.h +++ b/src/include/sof/lib/dai-zephyr.h @@ -54,7 +54,10 @@ struct dai { uint32_t dma_dev; const struct device *dev; const struct dai_data *dd; - struct k_spinlock lock; /* protect properties */ + struct k_mutex *lock; /* protect properties */ +#ifndef CONFIG_SOF_USERSPACE_LL + struct k_mutex lock_obj; +#endif }; union hdalink_cfg { From 5828777b6e0fa3bd17f599faaced2c0b2b9712b4 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 18 Feb 2026 15:49:22 +0200 Subject: [PATCH 17/94] audio: dai-zephyr: migrate to use dai_get_properties_copy() Modify code to allocate DAI properties object on stack and use dai_get_properties_copy(). This is required when DAI code is run in user-space and a syscall is needed to talk to the DAI driver. It's not possible to return a pointer to kernel memory, so instead data needs to be copied to caller stack. Signed-off-by: Kai Vehmanen --- src/audio/dai-zephyr.c | 54 +++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/audio/dai-zephyr.c b/src/audio/dai-zephyr.c index 659e710be05c..20335dfcf4c4 100644 --- a/src/audio/dai-zephyr.c +++ b/src/audio/dai-zephyr.c @@ -229,58 +229,62 @@ __cold int dai_set_config(struct dai *dai, struct ipc_config_dai *common_config, /* called from ipc/ipc3/dai.c */ int dai_get_handshake(struct dai *dai, int direction, int stream_id) { - const struct dai_properties *props; - int hs_id; + struct dai_properties props; + int ret; k_mutex_lock(dai->lock, K_FOREVER); - props = dai_get_properties(dai->dev, direction, stream_id); - hs_id = props->dma_hs_id; + ret = dai_get_properties_copy(dai->dev, direction, stream_id, &props); k_mutex_unlock(dai->lock); + if (ret < 0) + return ret; - return hs_id; + return props.dma_hs_id; } /* called from ipc/ipc3/dai.c and ipc/ipc4/dai.c */ int dai_get_fifo_depth(struct dai *dai, int direction) { - const struct dai_properties *props; - int fifo_depth; + struct dai_properties props; + int ret; if (!dai) return 0; k_mutex_lock(dai->lock, K_FOREVER); - props = dai_get_properties(dai->dev, direction, 0); - fifo_depth = props->fifo_depth; + ret = dai_get_properties_copy(dai->dev, direction, 0, &props); k_mutex_unlock(dai->lock); + if (ret < 0) + return 0; - return fifo_depth; + return props.fifo_depth; } int dai_get_stream_id(struct dai *dai, int direction) { - const struct dai_properties *props; - int stream_id; + struct dai_properties props; + int ret; k_mutex_lock(dai->lock, K_FOREVER); - props = dai_get_properties(dai->dev, direction, 0); - stream_id = props->stream_id; + ret = dai_get_properties_copy(dai->dev, direction, 0, &props); k_mutex_unlock(dai->lock); + if (ret < 0) + return ret; - return stream_id; + return props.stream_id; } static int dai_get_fifo(struct dai *dai, int direction, int stream_id) { - const struct dai_properties *props; - int fifo_address; + struct dai_properties props; + int ret; k_mutex_lock(dai->lock, K_FOREVER); - props = dai_get_properties(dai->dev, direction, stream_id); - fifo_address = props->fifo_address; + ret = dai_get_properties_copy(dai->dev, direction, stream_id, &props); k_mutex_unlock(dai->lock); + if (ret < 0) + return ret; - return fifo_address; + return props.fifo_address; } /* this is called by DMA driver every time descriptor has completed */ @@ -1985,15 +1989,17 @@ static int dai_ts_stop_op(struct comp_dev *dev) uint32_t dai_get_init_delay_ms(struct dai *dai) { - const struct dai_properties *props; - uint32_t init_delay; + struct dai_properties props; + uint32_t init_delay = 0; + int ret; if (!dai) return 0; k_mutex_lock(dai->lock, K_FOREVER); - props = dai_get_properties(dai->dev, 0, 0); - init_delay = props->reg_init_delay; + ret = dai_get_properties_copy(dai->dev, 0, 0, &props); + if (!ret) + init_delay = props.reg_init_delay; k_mutex_unlock(dai->lock); return init_delay; From 5cbecfacad77b9d15a4aca65eaa205d6f239af8e Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 22 Apr 2026 15:16:04 +0300 Subject: [PATCH 18/94] dai: zephyr: replace k_mutex with sof_umutex for DAI lock - replace dynamically-allocated k_mutex *lock with embedded sof_umutex - remove k_object_alloc/k_thread_access_grant boilerplate - remove k_object_free leak workaround in dai_common_free() Signed-off-by: Kai Vehmanen --- src/audio/dai-zephyr.c | 37 ++++++++++++++++---------------- src/include/sof/lib/dai-zephyr.h | 6 ++---- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/audio/dai-zephyr.c b/src/audio/dai-zephyr.c index 20335dfcf4c4..6150c862a543 100644 --- a/src/audio/dai-zephyr.c +++ b/src/audio/dai-zephyr.c @@ -232,9 +232,9 @@ int dai_get_handshake(struct dai *dai, int direction, int stream_id) struct dai_properties props; int ret; - k_mutex_lock(dai->lock, K_FOREVER); + sof_umutex_lock(&dai->lock, K_FOREVER); ret = dai_get_properties_copy(dai->dev, direction, stream_id, &props); - k_mutex_unlock(dai->lock); + sof_umutex_unlock(&dai->lock); if (ret < 0) return ret; @@ -250,9 +250,9 @@ int dai_get_fifo_depth(struct dai *dai, int direction) if (!dai) return 0; - k_mutex_lock(dai->lock, K_FOREVER); + sof_umutex_lock(&dai->lock, K_FOREVER); ret = dai_get_properties_copy(dai->dev, direction, 0, &props); - k_mutex_unlock(dai->lock); + sof_umutex_unlock(&dai->lock); if (ret < 0) return 0; @@ -264,9 +264,9 @@ int dai_get_stream_id(struct dai *dai, int direction) struct dai_properties props; int ret; - k_mutex_lock(dai->lock, K_FOREVER); + sof_umutex_lock(&dai->lock, K_FOREVER); ret = dai_get_properties_copy(dai->dev, direction, 0, &props); - k_mutex_unlock(dai->lock); + sof_umutex_unlock(&dai->lock); if (ret < 0) return ret; @@ -278,9 +278,9 @@ static int dai_get_fifo(struct dai *dai, int direction, int stream_id) struct dai_properties props; int ret; - k_mutex_lock(dai->lock, K_FOREVER); + sof_umutex_lock(&dai->lock, K_FOREVER); ret = dai_get_properties_copy(dai->dev, direction, stream_id, &props); - k_mutex_unlock(dai->lock); + sof_umutex_unlock(&dai->lock); if (ret < 0) return ret; @@ -516,6 +516,7 @@ __cold int dai_common_new(struct dai_data *dd, struct comp_dev *dev, const struct ipc_config_dai *dai_cfg) { uint32_t dir; + int ret; assert_can_be_cold(); @@ -539,12 +540,12 @@ __cold int dai_common_new(struct dai_data *dd, struct comp_dev *dev, return -ENODEV; } -#ifdef CONFIG_SOF_USERSPACE_LL - dd->dai->lock = k_object_alloc(K_OBJ_MUTEX); -#else - dd->dai->lock = &dd->dai->lock_obj; -#endif - k_mutex_init(dd->dai->lock); + ret = sof_umutex_init(&dd->dai->lock); + if (ret < 0) { + dai_put(dd->dai); + comp_err(dev, "sof_umutex_init() failed: %d", ret); + return ret; + } dma_sg_init(&dd->config.elem_array); dd->xrun = 0; @@ -670,9 +671,7 @@ __cold void dai_common_free(struct dai_data *dd) dai_release_llp_slot(dd); -#ifdef CONFIG_SOF_USERSPACE_LL - k_object_free(dd->dai->lock); -#endif + sof_umutex_free(&dd->dai->lock); dai_put(dd->dai); @@ -1996,11 +1995,11 @@ uint32_t dai_get_init_delay_ms(struct dai *dai) if (!dai) return 0; - k_mutex_lock(dai->lock, K_FOREVER); + sof_umutex_lock(&dai->lock, K_FOREVER); ret = dai_get_properties_copy(dai->dev, 0, 0, &props); if (!ret) init_delay = props.reg_init_delay; - k_mutex_unlock(dai->lock); + sof_umutex_unlock(&dai->lock); return init_delay; } diff --git a/src/include/sof/lib/dai-zephyr.h b/src/include/sof/lib/dai-zephyr.h index 16e52a9ea6ca..3f02397436dd 100644 --- a/src/include/sof/lib/dai-zephyr.h +++ b/src/include/sof/lib/dai-zephyr.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -54,10 +55,7 @@ struct dai { uint32_t dma_dev; const struct device *dev; const struct dai_data *dd; - struct k_mutex *lock; /* protect properties */ -#ifndef CONFIG_SOF_USERSPACE_LL - struct k_mutex lock_obj; -#endif + struct sof_umutex lock; /* protect properties */ }; union hdalink_cfg { From 8f0b98aad6690be829e292b512313da0fb4014a8 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 19 Feb 2026 15:05:22 +0200 Subject: [PATCH 19/94] (---section dai-zephyr STOP) From db035af2efee044e7636d515e860017cc045c96a Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 3 Mar 2026 15:54:12 +0200 Subject: [PATCH 20/94] (---section audio module infra START) From 48783a9794af5af41bb56582c728cb33902dcc33 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 5 May 2026 12:41:59 +0300 Subject: [PATCH 21/94] audio: module_adapter: alloc from LL user heap if LL run in user When SOF is built with LL pipes in user-space, module adapter should allocate all resources from the LL user heap. Signed-off-by: Kai Vehmanen --- src/audio/module_adapter/module_adapter.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/audio/module_adapter/module_adapter.c b/src/audio/module_adapter/module_adapter.c index 01126446860b..6bf284d0e5f1 100644 --- a/src/audio/module_adapter/module_adapter.c +++ b/src/audio/module_adapter/module_adapter.c @@ -101,7 +101,13 @@ static struct processing_module *module_adapter_mem_alloc(const struct comp_driv } mod_heap = NULL; } else { +#ifdef CONFIG_SOF_USERSPACE_LL + mod_heap = zephyr_ll_user_heap(); + comp_cl_dbg(drv, "using ll user heap for module"); +#else mod_heap = drv->user_heap; +#endif + mod_heap_user = NULL; heap_size = 0; mod_vreg = NULL; } From f9ad3caaf1e2cc97096a12c765984c6dc232c574 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 3 Mar 2026 15:54:37 +0200 Subject: [PATCH 22/94] (---section schduler changes START) From c3b1bc72fbb60bd396d214da96ea278526083b22 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 13 Feb 2026 18:48:43 +0200 Subject: [PATCH 23/94] schedule: zephyr_ll: convert pdata->sem into a dynamic object Turn the pdata->sem into a dynamic object in userspace LL builds, implemented with Zephyr k_sem. Add POSIX no-op stubs for sys_sem to maintain testbench build compatibility. Keep statically allocated semaphore for kernel LL builds. Signed-off-by: Kai Vehmanen --- posix/include/rtos/mutex.h | 21 +++++++++++++++++++++ src/schedule/zephyr_ll.c | 9 +++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/posix/include/rtos/mutex.h b/posix/include/rtos/mutex.h index 19b360bdaea5..3bd01342ced5 100644 --- a/posix/include/rtos/mutex.h +++ b/posix/include/rtos/mutex.h @@ -62,4 +62,25 @@ static inline int sys_mutex_unlock(struct sys_mutex *mutex) return 0; } +/* provide a no-op implementation for zephyr/sys/sem.h */ + +struct sys_sem { +}; + +static inline int sys_sem_init(struct sys_sem *sem, unsigned int initial_count, + unsigned int limit) +{ + return 0; +} + +static inline int sys_sem_give(struct sys_sem *sem) +{ + return 0; +} + +static inline int sys_sem_take(struct sys_sem *sem, k_timeout_t timeout) +{ + return 0; +} + #endif diff --git a/src/schedule/zephyr_ll.c b/src/schedule/zephyr_ll.c index 0a6fd3f14221..80b03673ba2b 100644 --- a/src/schedule/zephyr_ll.c +++ b/src/schedule/zephyr_ll.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -40,7 +41,7 @@ struct zephyr_ll { struct zephyr_ll_pdata { bool run; bool freeing; - struct k_sem sem; + struct sys_sem sem; }; static void zephyr_ll_lock(struct zephyr_ll *sch, uint32_t *flags) @@ -87,7 +88,7 @@ static void zephyr_ll_task_done(struct zephyr_ll *sch, * zephyr_ll_task_free() is trying to free this task. Complete * it and signal the semaphore to let the function proceed */ - k_sem_give(&pdata->sem); + sys_sem_give(&pdata->sem); tr_info(&ll_tr, "task complete %p %pU", task, task->uid); tr_info(&ll_tr, "num_tasks %d total_num_tasks %ld", @@ -446,7 +447,7 @@ static int zephyr_ll_task_free(void *data, struct task *task) if (must_wait) /* Wait for up to 100 periods */ - k_sem_take(&pdata->sem, K_USEC(LL_TIMER_PERIOD_US * 100)); + sys_sem_take(&pdata->sem, K_USEC(LL_TIMER_PERIOD_US * 100)); /* Protect against racing with schedule_task() */ zephyr_ll_lock(sch, &flags); @@ -588,7 +589,7 @@ int zephyr_ll_task_init(struct task *task, memset(pdata, 0, sizeof(*pdata)); - k_sem_init(&pdata->sem, 0, 1); + sys_sem_init(&pdata->sem, 0, 1); task->priv_data = pdata; From 44e87443d4401b4e26cc8990b17036d623a8746d Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 26 Feb 2026 17:42:13 +0200 Subject: [PATCH 24/94] schedule: zephyr_ll: add zephyr_ll_task_free() Add counterpart to zephyr_ll_task_alloc() to allow freeing the task with correct heap. Signed-off-by: Kai Vehmanen --- src/include/sof/schedule/ll_schedule_domain.h | 1 + src/schedule/zephyr_ll.c | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/include/sof/schedule/ll_schedule_domain.h b/src/include/sof/schedule/ll_schedule_domain.h index 1367c408899f..68b28a77fdc3 100644 --- a/src/include/sof/schedule/ll_schedule_domain.h +++ b/src/include/sof/schedule/ll_schedule_domain.h @@ -117,6 +117,7 @@ static inline struct ll_schedule_domain *dma_domain_get(void) #ifdef CONFIG_SOF_USERSPACE_LL struct task *zephyr_ll_task_alloc(void); +void zephyr_ll_task_free(struct task *task); struct k_heap *zephyr_ll_user_heap(void); bool zephyr_ll_user_heap_verify(struct k_heap *heap); void zephyr_ll_user_resources_init(void); diff --git a/src/schedule/zephyr_ll.c b/src/schedule/zephyr_ll.c index 80b03673ba2b..f8275e637969 100644 --- a/src/schedule/zephyr_ll.c +++ b/src/schedule/zephyr_ll.c @@ -394,7 +394,7 @@ static int zephyr_ll_task_schedule_after(void *data, struct task *task, uint64_t * This is synchronous - after this returns the object can be destroyed! * Assertion: under Zephyr this is always called from a thread context! */ -static int zephyr_ll_task_free(void *data, struct task *task) +static int zephyr_ll_task_sched_free(void *data, struct task *task) { struct zephyr_ll *sch = data; uint32_t flags; @@ -541,7 +541,7 @@ static const struct scheduler_ops zephyr_ll_ops = { .schedule_task = zephyr_ll_task_schedule, .schedule_task_before = zephyr_ll_task_schedule_before, .schedule_task_after = zephyr_ll_task_schedule_after, - .schedule_task_free = zephyr_ll_task_free, + .schedule_task_free = zephyr_ll_task_sched_free, .schedule_task_cancel = zephyr_ll_task_cancel, .scheduler_free = zephyr_ll_scheduler_free, #if CONFIG_SOF_USERSPACE_LL @@ -555,6 +555,12 @@ struct task *zephyr_ll_task_alloc(void) return sof_heap_alloc(zephyr_ll_user_heap(), SOF_MEM_FLAG_USER, sizeof(struct task), sizeof(void *)); } + +void zephyr_ll_task_free(struct task *task) +{ + sof_heap_free(zephyr_ll_user_heap(), task); +} + #endif /* CONFIG_SOF_USERSPACE_LL */ int zephyr_ll_task_init(struct task *task, From 26d04c19333269c67d8be78a3e8bd79f1f273ab1 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 26 Feb 2026 17:43:48 +0200 Subject: [PATCH 25/94] schedule: zephyr_ll: add zephyr_ll_grant_access() Add function zephyr_ll_grant_access() to allow other threads to access the scheduler mutex. This is needed if work is submitted from other threads to the scheduler. Signed-off-by: Kai Vehmanen --- src/include/sof/schedule/ll_schedule_domain.h | 1 + src/schedule/zephyr_ll.c | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/include/sof/schedule/ll_schedule_domain.h b/src/include/sof/schedule/ll_schedule_domain.h index 68b28a77fdc3..5840550ad853 100644 --- a/src/include/sof/schedule/ll_schedule_domain.h +++ b/src/include/sof/schedule/ll_schedule_domain.h @@ -121,6 +121,7 @@ void zephyr_ll_task_free(struct task *task); struct k_heap *zephyr_ll_user_heap(void); bool zephyr_ll_user_heap_verify(struct k_heap *heap); void zephyr_ll_user_resources_init(void); +void zephyr_ll_grant_access(struct k_thread *thread); #endif /* CONFIG_SOF_USERSPACE_LL */ static inline struct ll_schedule_domain *domain_init diff --git a/src/schedule/zephyr_ll.c b/src/schedule/zephyr_ll.c index f8275e637969..9cc47cc66729 100644 --- a/src/schedule/zephyr_ll.c +++ b/src/schedule/zephyr_ll.c @@ -561,6 +561,13 @@ void zephyr_ll_task_free(struct task *task) sof_heap_free(zephyr_ll_user_heap(), task); } +void zephyr_ll_grant_access(struct k_thread *thread) +{ + struct zephyr_ll *ll_sch = (struct zephyr_ll *)scheduler_get_data(SOF_SCHEDULE_LL_TIMER); + + k_thread_access_grant(thread, ll_sch->lock); +} + #endif /* CONFIG_SOF_USERSPACE_LL */ int zephyr_ll_task_init(struct task *task, From 61b747757874277db1f073bfe37954e9ea869c3d Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 3 Mar 2026 15:54:51 +0200 Subject: [PATCH 26/94] (---section schduler changes END) From 4c365c02960267d67a5c96f71012364fc1289f92 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 25 Mar 2026 19:31:48 +0200 Subject: [PATCH 27/94] (---section audio-user PRs START) From e81393f02e41a45905541ccd9d7356116fb1f93a Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 24 Mar 2026 18:19:26 +0200 Subject: [PATCH 28/94] schedule: zephyr_domain: use a different thread name for user LL When LL scheduler is run in user-space, use a different Zephyr thread name. Signed-off-by: Kai Vehmanen --- src/schedule/zephyr_domain.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schedule/zephyr_domain.c b/src/schedule/zephyr_domain.c index daace8bac708..8d739c4ab888 100644 --- a/src/schedule/zephyr_domain.c +++ b/src/schedule/zephyr_domain.c @@ -301,7 +301,7 @@ static int zephyr_domain_thread_init(struct ll_schedule_domain *domain, { struct zephyr_domain *zephyr_domain = ll_sch_domain_get_pdata(domain); struct zephyr_domain_thread *dt; - char thread_name[] = "ll_thread0"; + char thread_name[] = "userll_thread0"; k_tid_t thread; int core = task->core; From 860cd9eeedc5fae9f75c5cdd7daef0839a1a1a23 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 5 May 2026 12:47:34 +0300 Subject: [PATCH 29/94] WIP: audio: module_adapter: use correct heap when freeing Signed-off-by: Kai Vehmanen --- src/audio/module_adapter/module_adapter.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/audio/module_adapter/module_adapter.c b/src/audio/module_adapter/module_adapter.c index 6bf284d0e5f1..f4d7b1531407 100644 --- a/src/audio/module_adapter/module_adapter.c +++ b/src/audio/module_adapter/module_adapter.c @@ -192,6 +192,12 @@ static void module_adapter_mem_free(struct processing_module *mod) if (!vregion_put(mod_vreg)) rfree(alloc); } else { + LOG_INF("mod"); +#ifdef CONFIG_SOF_USERSPACE_LL + mod_heap = zephyr_ll_user_heap(); + comp_cl_dbg(drv, "using ll user heap for module free"); +#endif + comp_cl_info(drv, "free mod %p with heap %p", mod, mod_heap); sof_heap_free(mod_heap, mod->dev); sof_heap_free(mod_heap, mod); rfree(alloc); From 5ea7cf20cce588a2eaf67d11f941bfc33456e719 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 20 Mar 2026 21:40:57 +0200 Subject: [PATCH 30/94] coherent: disable core debug checks for user-space builds The COHERENT_CHECK_NONSHARED_CORES debug macros call cpu_get_id() which invokes arch_proc_id() - a privileged hardware register read that faults in user-space context. Disable the entire debug block at compile time when CONFIG_SOF_USERSPACE_LL is enabled. This also fixes the same latent issue in CORE_CHECK_STRUCT and CORE_CHECK_STRUCT_INIT. Signed-off-by: Kai Vehmanen --- src/include/sof/coherent.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/include/sof/coherent.h b/src/include/sof/coherent.h index 172e45b4ed92..ba6b8d8c7e52 100644 --- a/src/include/sof/coherent.h +++ b/src/include/sof/coherent.h @@ -86,8 +86,8 @@ STATIC_ASSERT(sizeof(struct coherent) <= DCACHE_LINE_SIZE, DCACHE_LINE_SIZE_too #define ADDR_IS_COHERENT(_c) #endif -/* debug sharing amongst cores */ -#ifdef COHERENT_CHECK_NONSHARED_CORES +/* debug sharing amongst cores - not available in user-space builds */ +#if defined(COHERENT_CHECK_NONSHARED_CORES) && !defined(CONFIG_SOF_USERSPACE_LL) #define CORE_CHECK_STRUCT_FIELD uint32_t __core; bool __is_shared #define CORE_CHECK_STRUCT_INIT(_c, is_shared) { (_c)->__core = cpu_get_id(); \ From 991fb17802d350f7e17677bd7c7acb6dc63ab6a0 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 24 Mar 2026 18:17:19 +0200 Subject: [PATCH 31/94] audio: module_adapter: make adapter buffering user-space compatible Replace all calls to rzalloc() and rballoc() with calls to sof_heap_alloc() with heap set based on how SOF is built. If audio pipelines are run fully in user-space, the sof_sys_user_heap_get() should be used. Signed-off-by: Kai Vehmanen --- src/audio/module_adapter/module_adapter.c | 33 ++++++++++++++--------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/audio/module_adapter/module_adapter.c b/src/audio/module_adapter/module_adapter.c index f4d7b1531407..73d26e09ee3a 100644 --- a/src/audio/module_adapter/module_adapter.c +++ b/src/audio/module_adapter/module_adapter.c @@ -539,11 +539,13 @@ int module_adapter_prepare(struct comp_dev *dev) /* allocate memory for input buffers */ if (mod->max_sources) { mod->input_buffers = - rzalloc(memory_flags, sizeof(*mod->input_buffers) * mod->max_sources); + sof_heap_alloc(sof_sys_user_heap_get(), memory_flags, + sizeof(*mod->input_buffers) * mod->max_sources, 0); if (!mod->input_buffers) { comp_err(dev, "failed to allocate input buffers"); return -ENOMEM; } + memset(mod->input_buffers, 0, sizeof(*mod->input_buffers) * mod->max_sources); } else { mod->input_buffers = NULL; } @@ -551,12 +553,14 @@ int module_adapter_prepare(struct comp_dev *dev) /* allocate memory for output buffers */ if (mod->max_sinks) { mod->output_buffers = - rzalloc(memory_flags, sizeof(*mod->output_buffers) * mod->max_sinks); + sof_heap_alloc(sof_sys_user_heap_get(), memory_flags, + sizeof(*mod->output_buffers) * mod->max_sources, 0); if (!mod->output_buffers) { comp_err(dev, "failed to allocate output buffers"); ret = -ENOMEM; goto in_out_free; } + memset(mod->input_buffers, 0, sizeof(*mod->output_buffers) * mod->max_sources); } else { mod->output_buffers = NULL; } @@ -617,7 +621,8 @@ int module_adapter_prepare(struct comp_dev *dev) size_t size = MAX(mod->deep_buff_bytes, mod->period_bytes); list_for_item(blist, &dev->bsource_list) { - mod->input_buffers[i].data = rballoc(memory_flags, size); + mod->input_buffers[i].data = sof_heap_alloc(sof_sys_user_heap_get(), + memory_flags, size, 0); if (!mod->input_buffers[i].data) { comp_err(mod->dev, "Failed to alloc input buffer data"); ret = -ENOMEM; @@ -629,7 +634,9 @@ int module_adapter_prepare(struct comp_dev *dev) /* allocate memory for output buffer data */ i = 0; list_for_item(blist, &dev->bsink_list) { - mod->output_buffers[i].data = rballoc(memory_flags, md->mpd.out_buff_size); + mod->output_buffers[i].data = sof_heap_alloc(sof_sys_user_heap_get(), + memory_flags, + md->mpd.out_buff_size, 0); if (!mod->output_buffers[i].data) { comp_err(mod->dev, "Failed to alloc output buffer data"); ret = -ENOMEM; @@ -698,16 +705,16 @@ int module_adapter_prepare(struct comp_dev *dev) out_data_free: for (i = 0; i < mod->num_of_sinks; i++) - rfree(mod->output_buffers[i].data); + sof_heap_free(sof_sys_user_heap_get(), mod->output_buffers[i].data); in_data_free: for (i = 0; i < mod->num_of_sources; i++) - rfree(mod->input_buffers[i].data); + sof_heap_free(sof_sys_user_heap_get(), mod->input_buffers[i].data); in_out_free: - rfree(mod->output_buffers); + sof_heap_free(sof_sys_user_heap_get(), mod->output_buffers); mod->output_buffers = NULL; - rfree(mod->input_buffers); + sof_heap_free(sof_sys_user_heap_get(), mod->input_buffers); mod->input_buffers = NULL; return ret; } @@ -1419,14 +1426,16 @@ int module_adapter_reset(struct comp_dev *dev) if (IS_PROCESSING_MODE_RAW_DATA(mod)) { for (i = 0; i < mod->num_of_sinks; i++) - rfree((__sparse_force void *)mod->output_buffers[i].data); + sof_heap_free(sof_sys_user_heap_get(), + (__sparse_force void *)mod->output_buffers[i].data); for (i = 0; i < mod->num_of_sources; i++) - rfree((__sparse_force void *)mod->input_buffers[i].data); + sof_heap_free(sof_sys_user_heap_get(), + (__sparse_force void *)mod->input_buffers[i].data); } if (IS_PROCESSING_MODE_RAW_DATA(mod) || IS_PROCESSING_MODE_AUDIO_STREAM(mod)) { - rfree(mod->output_buffers); - rfree(mod->input_buffers); + sof_heap_free(sof_sys_user_heap_get(), mod->output_buffers); + sof_heap_free(sof_sys_user_heap_get(), mod->input_buffers); mod->num_of_sources = 0; mod->num_of_sinks = 0; From 09082b7862f56da1973cc5926f86a1ad08d72091 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 26 Feb 2026 17:39:22 +0200 Subject: [PATCH 32/94] audio: pipeline: use LL scheduler mutex for userspace pipeline triggers In CONFIG_SOF_USERSPACE_LL builds, irq_local_disable() is not available. Replace the no-op irq guard with the LL scheduler's own k_mutex to prevent the scheduler from processing tasks while pipeline state is being updated. The k_mutex is re-entrant so schedule_task() calls inside the critical section are safe. Add zephyr_ll_lock_sched() and zephyr_ll_unlock_sched() helpers that acquire and release the scheduler mutex, and call them from pipeline_schedule_triggered() under CONFIG_SOF_USERSPACE_LL. Signed-off-by: Kai Vehmanen --- src/audio/pipeline/pipeline-schedule.c | 17 ++++++++++++- src/include/sof/schedule/ll_schedule_domain.h | 2 ++ src/schedule/zephyr_ll.c | 24 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/audio/pipeline/pipeline-schedule.c b/src/audio/pipeline/pipeline-schedule.c index 45fd1eed639c..7e19b43963ff 100644 --- a/src/audio/pipeline/pipeline-schedule.c +++ b/src/audio/pipeline/pipeline-schedule.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -282,6 +283,16 @@ void pipeline_schedule_triggered(struct pipeline_walk_context *ctx, struct pipeline_data *ppl_data = ctx->comp_data; struct list_item *tlist; struct pipeline *p; + +#ifdef CONFIG_SOF_USERSPACE_LL + /* + * In user-space irq_local_disable() is not available. Use the LL + * scheduler mutex to prevent the scheduler from processing tasks + * while pipeline state is being updated. The k_mutex is re-entrant + * so schedule_task() calls inside the critical section are safe. + */ + zephyr_ll_lock_sched(); +#else uint32_t flags; /* @@ -290,6 +301,7 @@ void pipeline_schedule_triggered(struct pipeline_walk_context *ctx, * immediately before all pipelines achieved a consistent state. */ irq_local_disable(flags); +#endif switch (cmd) { case COMP_TRIGGER_PAUSE: @@ -345,8 +357,11 @@ void pipeline_schedule_triggered(struct pipeline_walk_context *ctx, p->xrun_bytes = 1; } } - +#ifdef CONFIG_SOF_USERSPACE_LL + zephyr_ll_unlock_sched(); +#else irq_local_enable(flags); +#endif } int pipeline_comp_ll_task_init(struct pipeline *p) diff --git a/src/include/sof/schedule/ll_schedule_domain.h b/src/include/sof/schedule/ll_schedule_domain.h index 5840550ad853..5a1451c310dd 100644 --- a/src/include/sof/schedule/ll_schedule_domain.h +++ b/src/include/sof/schedule/ll_schedule_domain.h @@ -122,6 +122,8 @@ struct k_heap *zephyr_ll_user_heap(void); bool zephyr_ll_user_heap_verify(struct k_heap *heap); void zephyr_ll_user_resources_init(void); void zephyr_ll_grant_access(struct k_thread *thread); +void zephyr_ll_lock_sched(void); +void zephyr_ll_unlock_sched(void); #endif /* CONFIG_SOF_USERSPACE_LL */ static inline struct ll_schedule_domain *domain_init diff --git a/src/schedule/zephyr_ll.c b/src/schedule/zephyr_ll.c index 9cc47cc66729..a4cf5052408a 100644 --- a/src/schedule/zephyr_ll.c +++ b/src/schedule/zephyr_ll.c @@ -568,6 +568,30 @@ void zephyr_ll_grant_access(struct k_thread *thread) k_thread_access_grant(thread, ll_sch->lock); } +/** + * Lock the LL scheduler to prevent it from processing tasks. + * + * Uses the LL scheduler's own k_mutex which is re-entrant, so + * schedule_task() calls within the locked section will not deadlock. + * Must be paired with zephyr_ll_unlock_sched(). + */ +void zephyr_ll_lock_sched(void) +{ + struct zephyr_ll *sch = (struct zephyr_ll *)scheduler_get_data(SOF_SCHEDULE_LL_TIMER); + + k_mutex_lock(sch->lock, K_FOREVER); +} + +/** + * Unlock the LL scheduler after a previous zephyr_ll_lock_sched() call. + */ +void zephyr_ll_unlock_sched(void) +{ + struct zephyr_ll *sch = (struct zephyr_ll *)scheduler_get_data(SOF_SCHEDULE_LL_TIMER); + + k_mutex_unlock(sch->lock); +} + #endif /* CONFIG_SOF_USERSPACE_LL */ int zephyr_ll_task_init(struct task *task, From 67ad76c37908fdf4c962fe28456c9ced9a65e42c Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 19 May 2026 22:21:05 +0300 Subject: [PATCH 33/94] rtos: umutex.h: add new locking interface The Zephyr locking interfaces do not have a variant that allows the lock object to be put in dynamically allocated user-space memory. Implement a variant on top of Zephyr k_mutex that provides this support for SOF. Provide a no-op wrapper for POSIX library builds (just like we do for other locking interfaces). Signed-off-by: Kai Vehmanen --- posix/include/rtos/mutex.h | 27 +++++++++ posix/include/rtos/umutex.h | 13 ++++ zephyr/CMakeLists.txt | 6 ++ zephyr/Kconfig | 8 +++ zephyr/include/rtos/mutex.h | 1 + zephyr/include/rtos/umutex.h | 83 +++++++++++++++++++++++++ zephyr/lib/umutex.c | 62 +++++++++++++++++++ zephyr/syscall/umutex.c | 43 +++++++++++++ zephyr/test/CMakeLists.txt | 4 ++ zephyr/test/userspace/test_umutex.c | 93 +++++++++++++++++++++++++++++ 10 files changed, 340 insertions(+) create mode 100644 posix/include/rtos/umutex.h create mode 100644 zephyr/include/rtos/umutex.h create mode 100644 zephyr/lib/umutex.c create mode 100644 zephyr/syscall/umutex.c create mode 100644 zephyr/test/userspace/test_umutex.c diff --git a/posix/include/rtos/mutex.h b/posix/include/rtos/mutex.h index 3bd01342ced5..657f9131d1d7 100644 --- a/posix/include/rtos/mutex.h +++ b/posix/include/rtos/mutex.h @@ -83,4 +83,31 @@ static inline int sys_sem_take(struct sys_sem *sem, k_timeout_t timeout) return 0; } +/** + * @brief User-space accessible mutex stub for host/testbench builds. + */ +struct sof_umutex { + struct k_mutex mutex; /**< Inline k_mutex for POSIX (no dynamic alloc needed) */ +}; + +static inline int sof_umutex_init(struct sof_umutex *umutex) +{ + return k_mutex_init(&umutex->mutex); +} + +static inline int sof_umutex_lock(struct sof_umutex *umutex, k_timeout_t timeout) +{ + return k_mutex_lock(&umutex->mutex, timeout); +} + +static inline int sof_umutex_unlock(struct sof_umutex *umutex) +{ + return k_mutex_unlock(&umutex->mutex); +} + +static inline void sof_umutex_free(struct sof_umutex *umutex) +{ + /* No-op on POSIX — no kernel objects to free */ +} + #endif diff --git a/posix/include/rtos/umutex.h b/posix/include/rtos/umutex.h new file mode 100644 index 000000000000..1b2007cf059a --- /dev/null +++ b/posix/include/rtos/umutex.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// + +#ifndef __POSIX_RTOS_UMUTEX_H__ +#define __POSIX_RTOS_UMUTEX_H__ + +#include + +/* sof_umutex type and operations are defined in rtos/mutex.h for POSIX */ + +#endif /* __POSIX_RTOS_UMUTEX_H__ */ diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index 9bdaa6453ad4..bce849e85b00 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -577,6 +577,12 @@ if(CONFIG_SOF_USERSPACE_INTERFACE_DMA) zephyr_syscall_header(include/sof/lib/sof_dma.h) endif() +if(CONFIG_SOF_USERSPACE_INTERFACE_MUTEX) + zephyr_library_sources(syscall/umutex.c) + zephyr_library_sources(lib/umutex.c) + zephyr_syscall_header(include/rtos/umutex.h) +endif() + # Mandatory Files used on all platforms. # Commented files will be added/removed as integration dictates. zephyr_library_sources( diff --git a/zephyr/Kconfig b/zephyr/Kconfig index 27f37d829b4e..10e00e4c7eaa 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -36,11 +36,19 @@ config SOF_USERSPACE_INTERFACE_ALLOC Allow user-space threads to use sof_heap_alloc/sof_heap_free as Zephyr system calls. +config SOF_USERSPACE_INTERFACE_MUTEX + bool "User-space mutex interface" + depends on USERSPACE + help + Enables the sof_umutex API for dynamically-allocated + user-space accessible mutexes backed by k_object_alloc. + config SOF_USERSPACE_LL bool "Run Low-Latency pipelines in userspace threads" depends on USERSPACE select SOF_USERSPACE_INTERFACE_ALLOC select SOF_USERSPACE_INTERFACE_DMA + select SOF_USERSPACE_INTERFACE_MUTEX help Run Low-Latency (LL) pipelines in userspace threads. This adds memory protection between operating system resources and diff --git a/zephyr/include/rtos/mutex.h b/zephyr/include/rtos/mutex.h index a8886f768d1e..bf1f25813888 100644 --- a/zephyr/include/rtos/mutex.h +++ b/zephyr/include/rtos/mutex.h @@ -8,5 +8,6 @@ #include /* k_mutex_*() */ #include /* for sys_mutex */ +#include /* for sof_umutex */ #endif /* __ZEPHYR_RTOS_MUTEX_H__ */ diff --git a/zephyr/include/rtos/umutex.h b/zephyr/include/rtos/umutex.h new file mode 100644 index 000000000000..2695bfe4b0f5 --- /dev/null +++ b/zephyr/include/rtos/umutex.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// + +#ifndef __ZEPHYR_RTOS_UMUTEX_H__ +#define __ZEPHYR_RTOS_UMUTEX_H__ + +#include + +/** + * @brief User-space accessible mutex with dynamic allocation. + * + * This mutex variant can be dynamically allocated at runtime. The state + * struct resides in user-accessible memory; access to the mutex is granted + * to any thread that can access this memory region (no per-thread + * k_thread_access_grant needed). + * + * The backing k_mutex is allocated via k_object_alloc(K_OBJ_MUTEX) during + * init and freed via k_object_free() during free. + */ +struct sof_umutex { + struct k_mutex *mutex; /**< Pointer to dynamically-allocated backing k_mutex */ +}; + +__syscall int z_sof_umutex_init(struct sof_umutex *umutex); +__syscall int z_sof_umutex_lock(struct sof_umutex *umutex, k_timeout_t timeout); +__syscall int z_sof_umutex_unlock(struct sof_umutex *umutex); +__syscall void z_sof_umutex_free(struct sof_umutex *umutex); + +/** + * @brief Initialize a dynamic user-space mutex. + * + * Allocates the backing k_mutex kernel object. The sof_umutex struct + * must reside in memory accessible to the calling thread. + * + * @param umutex Pointer to the sof_umutex state (in user-accessible memory) + * @return 0 on success, -ENOMEM if allocation fails + */ +static inline int sof_umutex_init(struct sof_umutex *umutex) +{ + return z_sof_umutex_init(umutex); +} + +/** + * @brief Lock a dynamic user-space mutex. + * + * @param umutex Pointer to the sof_umutex state + * @param timeout Timeout value (K_FOREVER for indefinite wait) + * @return 0 on success, -EAGAIN on timeout, -EINVAL if not initialized + */ +static inline int sof_umutex_lock(struct sof_umutex *umutex, k_timeout_t timeout) +{ + return z_sof_umutex_lock(umutex, timeout); +} + +/** + * @brief Unlock a dynamic user-space mutex. + * + * @param umutex Pointer to the sof_umutex state + * @return 0 on success, -EINVAL if not initialized, -EPERM if not owner + */ +static inline int sof_umutex_unlock(struct sof_umutex *umutex) +{ + return z_sof_umutex_unlock(umutex); +} + +/** + * @brief Free a dynamic user-space mutex. + * + * Releases the backing k_mutex kernel object via k_object_free(). + * The sof_umutex state must not be used after this call. + * + * @param umutex Pointer to the sof_umutex state + */ +static inline void sof_umutex_free(struct sof_umutex *umutex) +{ + z_sof_umutex_free(umutex); +} + +#include + +#endif /* __ZEPHYR_RTOS_UMUTEX_H__ */ diff --git a/zephyr/lib/umutex.c b/zephyr/lib/umutex.c new file mode 100644 index 000000000000..70cb7ca06426 --- /dev/null +++ b/zephyr/lib/umutex.c @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. + +/** + * @file + * @brief SOF dynamic user-space mutex implementation. + * + * Provides the kernel-side implementation of sof_umutex operations. + * The backing k_mutex is dynamically allocated via k_object_alloc + * and freed via k_object_free. + */ + +#include +#include +#include + +int z_impl_z_sof_umutex_init(struct sof_umutex *umutex) +{ + struct k_mutex *m; + int ret; + + m = k_object_alloc(K_OBJ_MUTEX); + if (m == NULL) { + return -ENOMEM; + } + + ret = k_mutex_init(m); + if (ret) { + k_object_free(m); + return ret; + } + + umutex->mutex = m; + return 0; +} + +int z_impl_z_sof_umutex_lock(struct sof_umutex *umutex, k_timeout_t timeout) +{ + if (umutex->mutex == NULL) { + return -EINVAL; + } + + return k_mutex_lock(umutex->mutex, timeout); +} + +int z_impl_z_sof_umutex_unlock(struct sof_umutex *umutex) +{ + if (umutex->mutex == NULL) { + return -EINVAL; + } + + return k_mutex_unlock(umutex->mutex); +} + +void z_impl_z_sof_umutex_free(struct sof_umutex *umutex) +{ + if (umutex->mutex != NULL) { + k_object_free(umutex->mutex); + umutex->mutex = NULL; + } +} diff --git a/zephyr/syscall/umutex.c b/zephyr/syscall/umutex.c new file mode 100644 index 000000000000..9e25da1bb7a8 --- /dev/null +++ b/zephyr/syscall/umutex.c @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. + +/** + * @file + * @brief SOF dynamic user-space mutex syscall verification. + * + * Verify handlers ensure the calling user thread has write access to + * the sof_umutex state struct before forwarding to the implementation. + */ + +#include +#include + +static inline int z_vrfy_z_sof_umutex_init(struct sof_umutex *umutex) +{ + K_OOPS(K_SYSCALL_MEMORY_WRITE(umutex, sizeof(struct sof_umutex))); + return z_impl_z_sof_umutex_init(umutex); +} +#include + +static inline int z_vrfy_z_sof_umutex_lock(struct sof_umutex *umutex, + k_timeout_t timeout) +{ + K_OOPS(K_SYSCALL_MEMORY_WRITE(umutex, sizeof(struct sof_umutex))); + return z_impl_z_sof_umutex_lock(umutex, timeout); +} +#include + +static inline int z_vrfy_z_sof_umutex_unlock(struct sof_umutex *umutex) +{ + K_OOPS(K_SYSCALL_MEMORY_WRITE(umutex, sizeof(struct sof_umutex))); + return z_impl_z_sof_umutex_unlock(umutex); +} +#include + +static inline void z_vrfy_z_sof_umutex_free(struct sof_umutex *umutex) +{ + K_OOPS(K_SYSCALL_MEMORY_WRITE(umutex, sizeof(struct sof_umutex))); + z_impl_z_sof_umutex_free(umutex); +} +#include diff --git a/zephyr/test/CMakeLists.txt b/zephyr/test/CMakeLists.txt index 397e9e31b938..5cae3c7f2f5f 100644 --- a/zephyr/test/CMakeLists.txt +++ b/zephyr/test/CMakeLists.txt @@ -14,6 +14,10 @@ if(CONFIG_SOF_BOOT_TEST) if(CONFIG_USERSPACE AND CONFIG_SOF_USERSPACE_INTERFACE_ALLOC) zephyr_library_sources(userspace/test_heap_alloc.c) endif() + + if(CONFIG_SOF_USERSPACE_INTERFACE_MUTEX) + zephyr_library_sources(userspace/test_umutex.c) + endif() endif() if(CONFIG_SOF_BOOT_TEST_STANDALONE AND CONFIG_SOF_USERSPACE_INTERFACE_DMA) diff --git a/zephyr/test/userspace/test_umutex.c b/zephyr/test/userspace/test_umutex.c new file mode 100644 index 000000000000..5f655f1063c2 --- /dev/null +++ b/zephyr/test/userspace/test_umutex.c @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright(c) 2026 Intel Corporation. + */ + +/** + * @file + * @brief Test case for sof_umutex API from a Zephyr user-space thread. + * + * Validates that sof_umutex_init/lock/unlock/free work correctly when + * called from user-space context. + */ + +#include +#include +#include + +#include +#include +#include +#include + +LOG_MODULE_DECLARE(sof_boot_test, LOG_LEVEL_DBG); + +#define USER_STACKSIZE 2048 + +static struct k_thread umutex_user_thread; +static K_THREAD_STACK_DEFINE(umutex_user_stack, USER_STACKSIZE); + +/* Memory partition for test data accessible from user-space */ +K_APPMEM_PARTITION_DEFINE(umutex_test_part); + +/* Place the sof_umutex state in the user-accessible partition */ +K_APP_BMEM(umutex_test_part) static struct sof_umutex test_umutex; + +static void umutex_user_function(void *p1, void *p2, void *p3) +{ + int ret; + + __ASSERT(k_is_user_context(), "isn't user"); + + LOG_INF("umutex test thread %s (%s)", + k_is_user_context() ? "UserSpace!" : "privileged mode.", + CONFIG_BOARD_TARGET); + + /* Initialize the umutex — allocates backing k_mutex */ + ret = sof_umutex_init(&test_umutex); + zassert_equal(ret, 0, "sof_umutex_init failed: %d", ret); + + LOG_INF("sof_umutex_init succeeded"); + + /* Lock the mutex */ + ret = sof_umutex_lock(&test_umutex, K_FOREVER); + zassert_equal(ret, 0, "sof_umutex_lock failed: %d", ret); + + LOG_INF("sof_umutex_lock succeeded"); + + /* Unlock the mutex */ + ret = sof_umutex_unlock(&test_umutex); + zassert_equal(ret, 0, "sof_umutex_unlock failed: %d", ret); + + LOG_INF("sof_umutex_unlock succeeded"); + + /* Free the mutex — releases backing k_mutex */ + sof_umutex_free(&test_umutex); + + LOG_INF("sof_umutex_free done"); +} + +static void test_user_thread_umutex(void) +{ + /* Add test partition to LL memory domain so user thread can access test_umutex */ + k_mem_domain_add_partition(zephyr_ll_mem_domain(), &umutex_test_part); + + k_thread_create(&umutex_user_thread, umutex_user_stack, USER_STACKSIZE, + umutex_user_function, NULL, NULL, NULL, + -1, K_USER, K_FOREVER); + + /* Add thread to LL memory domain so it can access the partition */ + k_mem_domain_add_thread(zephyr_ll_mem_domain(), &umutex_user_thread); + + k_thread_start(&umutex_user_thread); + k_thread_join(&umutex_user_thread, K_FOREVER); + + k_mem_domain_remove_partition(zephyr_ll_mem_domain(), &umutex_test_part); +} + +ZTEST(sof_boot, user_space_umutex) +{ + test_user_thread_umutex(); + + ztest_test_pass(); +} From e97292dbd3c99ac706071a711936f0abc3b2d1c6 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 28 May 2026 14:47:23 +0300 Subject: [PATCH 34/94] schedule: zephyr_ll: replace k_mutex with sof_umutex for scheduler lock - replace dynamically-allocated k_mutex *lock with embedded sof_umutex - remove k_object_alloc and error handling in init - remove k_thread_access_grant calls, gut zephyr_ll_grant_access() Signed-off-by: Kai Vehmanen --- src/schedule/zephyr_ll.c | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/schedule/zephyr_ll.c b/src/schedule/zephyr_ll.c index a4cf5052408a..bc76028c1a6d 100644 --- a/src/schedule/zephyr_ll.c +++ b/src/schedule/zephyr_ll.c @@ -5,6 +5,7 @@ // Author: Guennadi Liakhovetski #include +#include #include #include #include @@ -32,7 +33,7 @@ struct zephyr_ll { struct ll_schedule_domain *ll_domain; /* scheduling domain */ unsigned int core; /* core ID of this instance */ #if CONFIG_SOF_USERSPACE_LL - struct k_mutex *lock; /* mutex for userspace */ + struct sof_umutex lock; /* mutex for userspace */ #endif struct k_heap *heap; }; @@ -47,7 +48,7 @@ struct zephyr_ll_pdata { static void zephyr_ll_lock(struct zephyr_ll *sch, uint32_t *flags) { #if CONFIG_SOF_USERSPACE_LL - k_mutex_lock(sch->lock, K_FOREVER); + sof_umutex_lock(&sch->lock, K_FOREVER); #else irq_local_disable(*flags); #endif @@ -56,7 +57,7 @@ static void zephyr_ll_lock(struct zephyr_ll *sch, uint32_t *flags) static void zephyr_ll_unlock(struct zephyr_ll *sch, uint32_t *flags) { #if CONFIG_SOF_USERSPACE_LL - k_mutex_unlock(sch->lock); + sof_umutex_unlock(&sch->lock); #else irq_local_enable(*flags); #endif @@ -526,12 +527,6 @@ struct k_thread *zephyr_ll_init_context(void *data, struct task *task) } assert(!k_is_user_context()); - k_thread_access_grant(zephyr_domain_thread_tid(sch->ll_domain), sch->lock); - - tr_dbg(&ll_tr, "granting access to lock %p for thread %p", sch->lock, - zephyr_domain_thread_tid(sch->ll_domain)); - tr_dbg(&ll_tr, "granting access to domain lock %p for thread %p", &sch->ll_domain->lock, - zephyr_domain_thread_tid(sch->ll_domain)); return zephyr_domain_thread_tid(sch->ll_domain); } @@ -563,15 +558,13 @@ void zephyr_ll_task_free(struct task *task) void zephyr_ll_grant_access(struct k_thread *thread) { - struct zephyr_ll *ll_sch = (struct zephyr_ll *)scheduler_get_data(SOF_SCHEDULE_LL_TIMER); - - k_thread_access_grant(thread, ll_sch->lock); + /* sof_umutex does not require access grants */ } /** * Lock the LL scheduler to prevent it from processing tasks. * - * Uses the LL scheduler's own k_mutex which is re-entrant, so + * Uses the LL scheduler's own sof_umutex which is re-entrant, so * schedule_task() calls within the locked section will not deadlock. * Must be paired with zephyr_ll_unlock_sched(). */ @@ -579,7 +572,7 @@ void zephyr_ll_lock_sched(void) { struct zephyr_ll *sch = (struct zephyr_ll *)scheduler_get_data(SOF_SCHEDULE_LL_TIMER); - k_mutex_lock(sch->lock, K_FOREVER); + sof_umutex_lock(&sch->lock, K_FOREVER); } /** @@ -589,7 +582,7 @@ void zephyr_ll_unlock_sched(void) { struct zephyr_ll *sch = (struct zephyr_ll *)scheduler_get_data(SOF_SCHEDULE_LL_TIMER); - k_mutex_unlock(sch->lock); + sof_umutex_unlock(&sch->lock); } #endif /* CONFIG_SOF_USERSPACE_LL */ @@ -665,16 +658,12 @@ int zephyr_ll_scheduler_init(struct ll_schedule_domain *domain) sch->heap = heap; #if CONFIG_SOF_USERSPACE_LL - /* Allocate mutex dynamically for userspace access */ - sch->lock = k_object_alloc(K_OBJ_MUTEX); - if (!sch->lock) { + if (sof_umutex_init(&sch->lock)) { tr_err(&ll_tr, "mutex allocation failed"); sof_heap_free(sch->heap, sch); return -ENOMEM; } - k_mutex_init(sch->lock); - - tr_dbg(&ll_tr, "ll-scheduler init done, sch %p sch->lock %p", sch, sch->lock); + tr_dbg(&ll_tr, "ll-scheduler init done, sch %p", sch); #endif scheduler_init(domain->type, &zephyr_ll_ops, sch); From 34bc297285e6e1383ac5f8863bf52c22d243121a Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 13 Feb 2026 14:54:58 +0200 Subject: [PATCH 35/94] ipc: ipc-helper: use list_mutex to guard buffer list in userspace LL In user-space LL builds (CONFIG_SOF_USERSPACE_LL), irq_local_disable() is a privileged operation. In ipc_comp_free(), use sys_mutex_lock/unlock on the per-component list_mutex to protect buffer list iteration instead. This follows the same locking pattern as PPL_LOCK/PPL_UNLOCK used in pipeline_disconnect(). Signed-off-by: Kai Vehmanen --- src/ipc/ipc-helper.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ipc/ipc-helper.c b/src/ipc/ipc-helper.c index ad7b3771a16b..53a418ac4823 100644 --- a/src/ipc/ipc-helper.c +++ b/src/ipc/ipc-helper.c @@ -333,7 +333,16 @@ __cold int ipc_comp_free(struct ipc *ipc, uint32_t comp_id) return -EINVAL; } + /* Lock buffer lists to prevent racing with the LL scheduler. + * In user-space builds, irq_local_disable() is a privileged + * operation, so use the per-component list_mutex instead + * (same pattern as PPL_LOCK in pipeline_disconnect()). + */ +#ifdef CONFIG_SOF_USERSPACE_LL + sys_mutex_lock(&icd->cd->list_mutex, K_FOREVER); +#else irq_local_disable(flags); +#endif comp_dev_for_each_producer_safe(icd->cd, buffer, safe) { comp_buffer_set_sink_component(buffer, NULL); /* This breaks the list, but we anyway delete all buffers */ @@ -346,7 +355,11 @@ __cold int ipc_comp_free(struct ipc *ipc, uint32_t comp_id) comp_buffer_reset_source_list(buffer); } +#ifdef CONFIG_SOF_USERSPACE_LL + sys_mutex_unlock(&icd->cd->list_mutex); +#else irq_local_enable(flags); +#endif /* free component and remove from list */ comp_free(icd->cd); From b36103a0ea5d8a781f287ae758d7e381e5a62bce Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 29 May 2026 13:05:40 +0300 Subject: [PATCH 36/94] audio: pipeline: change locking strategy for user LL builds Modify the locking approach for CONFIG_SOF_USERSPACE_LL builds. Kernel LL implementation heavily relies on ability to disable interrupts when IPC handler is modifying the graph. This ensures a new LL tick and execution of a new graph cycle does not start before the graph modifications done by IPC handler are complete. In user-space, this approach is not available as user-space thread cannot disable interrupts. In commit 1e59ce23dfa4 ("pipeline: protect component connections with a mutex"), a sys_mutex based locking was implemented to protect the component list and modifications to it. This approach does not scale in the end as this would require taking the mutex for each component of each pipeline, and take the locks on every LL cycle tick. This results in significant system call overhead. Additionally Zephyr sys_mutex does not work correctly if the lock object is put into dynamically allocated user memory. In this commit, locking the LL graph is moved to a higher level. A single lock is used to protect the whole LL graph, and the lock is taken at start of LL tick. The same lock is taken by the IPC handlers when modifications to the graph are taken. The mutex interface supports priority inversion, so this usage is safe if LL timer tick happens while IPC processing is still in progress. The patch only changes behaviour for userspace LL SOF builds. If LL scheduling is kept in kernel, locking is done as before. Signed-off-by: Kai Vehmanen --- src/audio/pipeline/pipeline-graph.c | 19 +++++++++++-------- src/audio/pipeline/pipeline-stream.c | 18 ++++++++++++++++++ src/include/sof/audio/component.h | 7 ------- src/include/sof/audio/pipeline.h | 1 + src/ipc/ipc-helper.c | 10 +++++----- src/ipc/ipc4/helper.c | 21 ++++++++++++++++++++- 6 files changed, 55 insertions(+), 21 deletions(-) diff --git a/src/audio/pipeline/pipeline-graph.c b/src/audio/pipeline/pipeline-graph.c index 47d5d0127fd0..82436815f563 100644 --- a/src/audio/pipeline/pipeline-graph.c +++ b/src/audio/pipeline/pipeline-graph.c @@ -179,16 +179,19 @@ static void buffer_set_comp(struct comp_buffer *buffer, struct comp_dev *comp, } #ifdef CONFIG_SOF_USERSPACE_LL +/* + * User-space LL: IPC-level zephyr_ll_lock_sched() provides mutual + * exclusion with the LL thread. No per-pipeline lock needed. + */ #define PPL_LOCK_DECLARE -#define PPL_LOCK() do { \ - int ret = sys_mutex_lock(&comp->list_mutex, K_FOREVER); \ - assert(ret == 0); \ - } while (0) -#define PPL_UNLOCK() do { \ - int ret = sys_mutex_unlock(&comp->list_mutex); \ - assert(ret == 0); \ - } while (0) +#define PPL_LOCK() zephyr_ll_lock_sched(PLATFORM_PRIMARY_CORE_ID) +#define PPL_UNLOCK() zephyr_ll_unlock_sched(PLATFORM_PRIMARY_CORE_ID) #else +/* + * Kernel-space LL. When modifying pipeline connections, block IRQs + * and prevent LL from running. No locking needed when iterating + * the pipeline in the LL thread. + */ #define PPL_LOCK_DECLARE uint32_t flags #define PPL_LOCK() irq_local_disable(flags) #define PPL_UNLOCK() irq_local_enable(flags) diff --git a/src/audio/pipeline/pipeline-stream.c b/src/audio/pipeline/pipeline-stream.c index 8bfdfc182912..24922717cb10 100644 --- a/src/audio/pipeline/pipeline-stream.c +++ b/src/audio/pipeline/pipeline-stream.c @@ -145,6 +145,20 @@ static int pipeline_comp_copy(struct comp_dev *current, return err; } +#ifdef CONFIG_SOF_USERSPACE_LL +#include +/* + * User-space LL: The LL thread runs at cooperative priority (-16) + * and cannot be preempted by IPC (priority 1). No per-pipeline + * lock needed in pipeline_copy() hot path. + */ +#define PPL_LOCK() zephyr_ll_lock_sched(PLATFORM_PRIMARY_CORE_ID) +#define PPL_UNLOCK() zephyr_ll_unlock_sched(PLATFORM_PRIMARY_CORE_ID) +#else +#define PPL_LOCK() +#define PPL_UNLOCK() +#endif + /* Copy data across all pipeline components. * For capture pipelines it always starts from source component * and continues downstream and for playback pipelines it first @@ -162,6 +176,8 @@ int pipeline_copy(struct pipeline *p) uint32_t dir; int ret; + PPL_LOCK(); + if (p->source_comp->direction == SOF_IPC_STREAM_PLAYBACK) { dir = PPL_DIR_UPSTREAM; start = p->sink_comp; @@ -178,6 +194,8 @@ int pipeline_copy(struct pipeline *p) pipe_err(p, "ret = %d, start->comp.id = %u, dir = %u", ret, dev_comp_id(start), dir); + PPL_UNLOCK(); + return ret; } diff --git a/src/include/sof/audio/component.h b/src/include/sof/audio/component.h index ebb88c8f79a1..1d79b336b208 100644 --- a/src/include/sof/audio/component.h +++ b/src/include/sof/audio/component.h @@ -681,10 +681,6 @@ struct comp_dev { struct list_item bsource_list; /**< list of source buffers */ struct list_item bsink_list; /**< list of sink buffers */ -#ifdef CONFIG_SOF_USERSPACE_LL - struct sys_mutex list_mutex; /**< protect lists of source/sinks */ -#endif - /* performance data*/ struct comp_perf_data perf_data; /* Input Buffer Size for pin 0, add array for other pins if needed */ @@ -869,9 +865,6 @@ static inline void comp_init(const struct comp_driver *drv, dev->state = COMP_STATE_INIT; list_init(&dev->bsink_list); list_init(&dev->bsource_list); -#ifdef CONFIG_SOF_USERSPACE_LL - sys_mutex_init(&dev->list_mutex); -#endif #ifndef __ZEPHYR__ memcpy_s(&dev->tctx, sizeof(dev->tctx), trace_comp_drv_get_tr_ctx(dev->drv), sizeof(struct tr_ctx)); diff --git a/src/include/sof/audio/pipeline.h b/src/include/sof/audio/pipeline.h index 913a569c208c..065a32063843 100644 --- a/src/include/sof/audio/pipeline.h +++ b/src/include/sof/audio/pipeline.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include diff --git a/src/ipc/ipc-helper.c b/src/ipc/ipc-helper.c index 53a418ac4823..6da9bb2692fa 100644 --- a/src/ipc/ipc-helper.c +++ b/src/ipc/ipc-helper.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -334,12 +335,11 @@ __cold int ipc_comp_free(struct ipc *ipc, uint32_t comp_id) } /* Lock buffer lists to prevent racing with the LL scheduler. - * In user-space builds, irq_local_disable() is a privileged - * operation, so use the per-component list_mutex instead - * (same pattern as PPL_LOCK in pipeline_disconnect()). + * In user-space builds, use the LL scheduler's sys_mutex + * (re-entrant, so safe if caller already holds it). */ #ifdef CONFIG_SOF_USERSPACE_LL - sys_mutex_lock(&icd->cd->list_mutex, K_FOREVER); + zephyr_ll_lock_sched(icd->core); #else irq_local_disable(flags); #endif @@ -356,7 +356,7 @@ __cold int ipc_comp_free(struct ipc *ipc, uint32_t comp_id) } #ifdef CONFIG_SOF_USERSPACE_LL - sys_mutex_unlock(&icd->cd->list_mutex); + zephyr_ll_unlock_sched(icd->core); #else irq_local_enable(flags); #endif diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index 8e3073ab7797..397eea94c82e 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -449,10 +449,21 @@ __cold static int ipc_pipeline_module_free(uint32_t pipeline_id) struct ipc *ipc = ipc_get(); struct ipc_comp_dev *icd; int ret; +#ifdef CONFIG_SOF_USERSPACE_LL + int ppl_core; +#endif assert_can_be_cold(); icd = ipc_get_comp_by_ppl_id(ipc, COMP_TYPE_COMPONENT, pipeline_id, IPC_COMP_ALL); + if (!icd) + return IPC4_SUCCESS; + +#ifdef CONFIG_SOF_USERSPACE_LL + ppl_core = icd->core; + zephyr_ll_lock_sched(ppl_core); +#endif + while (icd) { struct comp_buffer *buffer; struct comp_buffer *safe; @@ -482,12 +493,20 @@ __cold static int ipc_pipeline_module_free(uint32_t pipeline_id) else ret = ipc_comp_free(ipc, icd->id); - if (ret) + if (ret) { +#ifdef CONFIG_SOF_USERSPACE_LL + zephyr_ll_unlock_sched(ppl_core); +#endif return IPC4_INVALID_RESOURCE_STATE; + } icd = ipc_get_comp_by_ppl_id(ipc, COMP_TYPE_COMPONENT, pipeline_id, IPC_COMP_ALL); } +#ifdef CONFIG_SOF_USERSPACE_LL + zephyr_ll_unlock_sched(ppl_core); +#endif + return IPC4_SUCCESS; } From 370b83141832f1b514177c5a73b0b09684b043fe Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 5 May 2026 12:53:17 +0300 Subject: [PATCH 37/94] audio: buffer: move dp_heap_user lifecycle to IPC and module adapter The mod_heap_user (dp_heap_user) reference count management in comp_buffer_free() does not belong there - a buffer's free method should not be responsible for freeing a module's private heap. Additionally, in CONFIG_SOF_USERSPACE_LL builds, comp_buffer_free() may run in user-space context where rfree() of kernel-allocated memory is not ok. - Add dp_heap_put() helper to dp_schedule.h for reference-counted DP heap release - Move buffer-side client_count decrements to IPC layer (ipc_comp_disconnect, ipc_pipeline_module_free, ipc_comp_connect error paths) - Move raw data buffer client_count management to module_adapter_free/prepare with DP domain guard - Fix module_adapter_mem_free() where CONFIG_SOF_USERSPACE_LL incorrectly reassigned mod_heap for DP modules - Fix client_count increment guard in ipc_comp_connect() to check dp pointer rather than dp_heap Signed-off-by: Kai Vehmanen --- src/audio/module_adapter/module_adapter.c | 6 +++ src/include/sof/schedule/dp_schedule.h | 1 + src/ipc/ipc4/helper.c | 50 ++++++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/audio/module_adapter/module_adapter.c b/src/audio/module_adapter/module_adapter.c index 73d26e09ee3a..5c0b8872fbc2 100644 --- a/src/audio/module_adapter/module_adapter.c +++ b/src/audio/module_adapter/module_adapter.c @@ -701,6 +701,9 @@ int module_adapter_prepare(struct comp_dev *dev) list_item_del(&buffer->buffers_list); irq_local_enable(flags); buffer_free(buffer); + if (dev->ipc_config.proc_domain == COMP_PROCESSING_DOMAIN_DP && + md->resources.heap) + dp_heap_put(md->resources.heap); } out_data_free: @@ -1493,6 +1496,9 @@ void module_adapter_free(struct comp_dev *dev) list_item_del(&buffer->buffers_list); irq_local_enable(flags); buffer_free(buffer); + if (dev->ipc_config.proc_domain == COMP_PROCESSING_DOMAIN_DP && + mod->priv.resources.heap) + dp_heap_put(mod->priv.resources.heap); } mod_free(mod, mod->stream_params); diff --git a/src/include/sof/schedule/dp_schedule.h b/src/include/sof/schedule/dp_schedule.h index 2267d676fb8a..35b2ff6f2c32 100644 --- a/src/include/sof/schedule/dp_schedule.h +++ b/src/include/sof/schedule/dp_schedule.h @@ -8,6 +8,7 @@ #ifndef __SOF_SCHEDULE_DP_SCHEDULE_H__ #define __SOF_SCHEDULE_DP_SCHEDULE_H__ +#include #include #include #include diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index 397eea94c82e..a78e546b8cfb 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -470,22 +470,51 @@ __cold static int ipc_pipeline_module_free(uint32_t pipeline_id) /* free sink buffer allocated by current component in bind function */ comp_dev_for_each_consumer_safe(icd->cd, buffer, safe) { +#if CONFIG_ZEPHYR_DP_SCHEDULER + struct k_heap *buf_heap = buffer->audio_buffer.heap; + struct comp_dev *orig_sink = comp_buffer_get_sink_component(buffer); + bool buf_is_dp = buf_heap && + (icd->cd->ipc_config.proc_domain == COMP_PROCESSING_DOMAIN_DP || + (orig_sink && orig_sink->ipc_config.proc_domain == + COMP_PROCESSING_DOMAIN_DP)); +#endif + pipeline_disconnect(icd->cd, buffer, PPL_CONN_DIR_COMP_TO_BUFFER); struct comp_dev *sink = comp_buffer_get_sink_component(buffer); /* free the buffer only when the sink module has also been disconnected */ - if (!sink) + if (!sink) { buffer_free(buffer); +#if CONFIG_ZEPHYR_DP_SCHEDULER + if (buf_is_dp) + dp_heap_put(buf_heap); +#endif + } } /* free source buffer allocated by current component in bind function */ comp_dev_for_each_producer_safe(icd->cd, buffer, safe) { +#if CONFIG_ZEPHYR_DP_SCHEDULER + struct k_heap *buf_heap = buffer->audio_buffer.heap; + struct comp_dev *orig_source = + comp_buffer_get_source_component(buffer); + bool buf_is_dp = buf_heap && + (icd->cd->ipc_config.proc_domain == COMP_PROCESSING_DOMAIN_DP || + (orig_source && orig_source->ipc_config.proc_domain == + COMP_PROCESSING_DOMAIN_DP)); +#endif + pipeline_disconnect(icd->cd, buffer, PPL_CONN_DIR_BUFFER_TO_COMP); struct comp_dev *source = comp_buffer_get_source_component(buffer); /* free the buffer only when the source module has also been disconnected */ - if (!source) + if (!source) { buffer_free(buffer); +#if CONFIG_ZEPHYR_DP_SCHEDULER + if (buf_is_dp) + dp_heap_put(buf_heap); +#endif + } } if (!cpu_is_me(icd->core)) @@ -800,6 +829,8 @@ __cold int ipc_comp_connect(struct ipc *ipc, ipc_pipe_comp_connect *_connect) buf_get_id(buffer)); if (!ring_buffer) { buffer_free(buffer); + if (dp) + dp_heap_put(dp_heap); return IPC4_OUT_OF_MEMORY; } @@ -887,6 +918,10 @@ __cold int ipc_comp_connect(struct ipc *ipc, ipc_pipe_comp_connect *_connect) free: ll_unblock(cross_core_bind, flags); buffer_free(buffer); +#if CONFIG_ZEPHYR_DP_SCHEDULER + if (dp) + dp_heap_put(dp_heap); +#endif return IPC4_INVALID_RESOURCE_STATE; } @@ -968,6 +1003,13 @@ __cold int ipc_comp_disconnect(struct ipc *ipc, ipc_pipe_comp_connect *_connect) #endif } +#if CONFIG_ZEPHYR_DP_SCHEDULER + struct k_heap *buf_heap = buffer->audio_buffer.heap; + bool buf_is_dp = buf_heap && + (src->ipc_config.proc_domain == COMP_PROCESSING_DOMAIN_DP || + sink->ipc_config.proc_domain == COMP_PROCESSING_DOMAIN_DP); +#endif + pipeline_disconnect(src, buffer, PPL_CONN_DIR_COMP_TO_BUFFER); pipeline_disconnect(sink, buffer, PPL_CONN_DIR_BUFFER_TO_COMP); /* these might call comp_ipc4_bind_remote() if necessary */ @@ -983,6 +1025,10 @@ __cold int ipc_comp_disconnect(struct ipc *ipc, ipc_pipe_comp_connect *_connect) ll_unblock(cross_core_unbind, flags); buffer_free(buffer); +#if CONFIG_ZEPHYR_DP_SCHEDULER + if (buf_is_dp) + dp_heap_put(buf_heap); +#endif if (ret || ret1) return IPC4_INVALID_RESOURCE_ID; From 93cd54431a15d86be932519ccb27686cf46e1fcd Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 5 May 2026 12:53:38 +0300 Subject: [PATCH 38/94] Revert "audio: buffer: move dp_heap_user lifecycle to IPC and module adapter" This no longer works with vregion. But is this needed at all anymore? This reverts commit da2e431439b740b06f7826661dac5221e774a9eb. --- src/audio/module_adapter/module_adapter.c | 6 --- src/include/sof/schedule/dp_schedule.h | 1 - src/ipc/ipc4/helper.c | 50 +---------------------- 3 files changed, 2 insertions(+), 55 deletions(-) diff --git a/src/audio/module_adapter/module_adapter.c b/src/audio/module_adapter/module_adapter.c index 5c0b8872fbc2..73d26e09ee3a 100644 --- a/src/audio/module_adapter/module_adapter.c +++ b/src/audio/module_adapter/module_adapter.c @@ -701,9 +701,6 @@ int module_adapter_prepare(struct comp_dev *dev) list_item_del(&buffer->buffers_list); irq_local_enable(flags); buffer_free(buffer); - if (dev->ipc_config.proc_domain == COMP_PROCESSING_DOMAIN_DP && - md->resources.heap) - dp_heap_put(md->resources.heap); } out_data_free: @@ -1496,9 +1493,6 @@ void module_adapter_free(struct comp_dev *dev) list_item_del(&buffer->buffers_list); irq_local_enable(flags); buffer_free(buffer); - if (dev->ipc_config.proc_domain == COMP_PROCESSING_DOMAIN_DP && - mod->priv.resources.heap) - dp_heap_put(mod->priv.resources.heap); } mod_free(mod, mod->stream_params); diff --git a/src/include/sof/schedule/dp_schedule.h b/src/include/sof/schedule/dp_schedule.h index 35b2ff6f2c32..2267d676fb8a 100644 --- a/src/include/sof/schedule/dp_schedule.h +++ b/src/include/sof/schedule/dp_schedule.h @@ -8,7 +8,6 @@ #ifndef __SOF_SCHEDULE_DP_SCHEDULE_H__ #define __SOF_SCHEDULE_DP_SCHEDULE_H__ -#include #include #include #include diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index a78e546b8cfb..397eea94c82e 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -470,51 +470,22 @@ __cold static int ipc_pipeline_module_free(uint32_t pipeline_id) /* free sink buffer allocated by current component in bind function */ comp_dev_for_each_consumer_safe(icd->cd, buffer, safe) { -#if CONFIG_ZEPHYR_DP_SCHEDULER - struct k_heap *buf_heap = buffer->audio_buffer.heap; - struct comp_dev *orig_sink = comp_buffer_get_sink_component(buffer); - bool buf_is_dp = buf_heap && - (icd->cd->ipc_config.proc_domain == COMP_PROCESSING_DOMAIN_DP || - (orig_sink && orig_sink->ipc_config.proc_domain == - COMP_PROCESSING_DOMAIN_DP)); -#endif - pipeline_disconnect(icd->cd, buffer, PPL_CONN_DIR_COMP_TO_BUFFER); struct comp_dev *sink = comp_buffer_get_sink_component(buffer); /* free the buffer only when the sink module has also been disconnected */ - if (!sink) { + if (!sink) buffer_free(buffer); -#if CONFIG_ZEPHYR_DP_SCHEDULER - if (buf_is_dp) - dp_heap_put(buf_heap); -#endif - } } /* free source buffer allocated by current component in bind function */ comp_dev_for_each_producer_safe(icd->cd, buffer, safe) { -#if CONFIG_ZEPHYR_DP_SCHEDULER - struct k_heap *buf_heap = buffer->audio_buffer.heap; - struct comp_dev *orig_source = - comp_buffer_get_source_component(buffer); - bool buf_is_dp = buf_heap && - (icd->cd->ipc_config.proc_domain == COMP_PROCESSING_DOMAIN_DP || - (orig_source && orig_source->ipc_config.proc_domain == - COMP_PROCESSING_DOMAIN_DP)); -#endif - pipeline_disconnect(icd->cd, buffer, PPL_CONN_DIR_BUFFER_TO_COMP); struct comp_dev *source = comp_buffer_get_source_component(buffer); /* free the buffer only when the source module has also been disconnected */ - if (!source) { + if (!source) buffer_free(buffer); -#if CONFIG_ZEPHYR_DP_SCHEDULER - if (buf_is_dp) - dp_heap_put(buf_heap); -#endif - } } if (!cpu_is_me(icd->core)) @@ -829,8 +800,6 @@ __cold int ipc_comp_connect(struct ipc *ipc, ipc_pipe_comp_connect *_connect) buf_get_id(buffer)); if (!ring_buffer) { buffer_free(buffer); - if (dp) - dp_heap_put(dp_heap); return IPC4_OUT_OF_MEMORY; } @@ -918,10 +887,6 @@ __cold int ipc_comp_connect(struct ipc *ipc, ipc_pipe_comp_connect *_connect) free: ll_unblock(cross_core_bind, flags); buffer_free(buffer); -#if CONFIG_ZEPHYR_DP_SCHEDULER - if (dp) - dp_heap_put(dp_heap); -#endif return IPC4_INVALID_RESOURCE_STATE; } @@ -1003,13 +968,6 @@ __cold int ipc_comp_disconnect(struct ipc *ipc, ipc_pipe_comp_connect *_connect) #endif } -#if CONFIG_ZEPHYR_DP_SCHEDULER - struct k_heap *buf_heap = buffer->audio_buffer.heap; - bool buf_is_dp = buf_heap && - (src->ipc_config.proc_domain == COMP_PROCESSING_DOMAIN_DP || - sink->ipc_config.proc_domain == COMP_PROCESSING_DOMAIN_DP); -#endif - pipeline_disconnect(src, buffer, PPL_CONN_DIR_COMP_TO_BUFFER); pipeline_disconnect(sink, buffer, PPL_CONN_DIR_BUFFER_TO_COMP); /* these might call comp_ipc4_bind_remote() if necessary */ @@ -1025,10 +983,6 @@ __cold int ipc_comp_disconnect(struct ipc *ipc, ipc_pipe_comp_connect *_connect) ll_unblock(cross_core_unbind, flags); buffer_free(buffer); -#if CONFIG_ZEPHYR_DP_SCHEDULER - if (buf_is_dp) - dp_heap_put(buf_heap); -#endif if (ret || ret1) return IPC4_INVALID_RESOURCE_ID; From 5eff207ebd7b29c252d768190f1886830e95a27d Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 21 May 2026 12:36:06 +0300 Subject: [PATCH 39/94] audio: host-zepher: add HOST_DMA_IPC_POSITION_UPDATES Kconfig Add a built option HOST_DMA_IPC_POSITION_UPDATES to control whether functionality to send IPC stream position updates is enabled or not. Most platforms provide more efficient means for host to monitor DMA state, so this code is in most cases unncessary. The current IPC sending code (from audio context) also assume kernel context, so making this functionality user-space compatible will require extra work. Signed-off-by: Kai Vehmanen --- src/audio/Kconfig | 8 ++++++++ src/audio/copier/host_copier.h | 2 ++ src/audio/host-zephyr.c | 10 ++++++++++ src/ipc/ipc-helper.c | 2 ++ 4 files changed, 22 insertions(+) diff --git a/src/audio/Kconfig b/src/audio/Kconfig index 1f7d362ffdc2..3bb4ec58f6bb 100644 --- a/src/audio/Kconfig +++ b/src/audio/Kconfig @@ -42,6 +42,14 @@ config HOST_DMA_STREAM_SYNCHRONIZATION for each group, different than the default one determined by the system tick frequency. This feature will allow host lower power consumption in scenarios with deep buffering. +config HOST_DMA_IPC_POSITION_UPDATES + bool "Support for stream position updates via IPC messages" + default y if IPC_MAJOR_3 + help + Support firmware functionality to report stream position updates + by sending a IPC message whenever one period of audio is transfferred. + Most platforms provide more efficient ways to query the DMA status. + config COMP_CHAIN_DMA bool "Chain DMA component" depends on IPC_MAJOR_4 diff --git a/src/audio/copier/host_copier.h b/src/audio/copier/host_copier.h index 71ce89cf315b..9e59e4fe07f3 100644 --- a/src/audio/copier/host_copier.h +++ b/src/audio/copier/host_copier.h @@ -106,7 +106,9 @@ struct host_data { /* stream info */ struct sof_ipc_stream_posn posn; /* TODO: update this */ +#if CONFIG_HOST_DMA_IPC_POSITION_UPDATES struct ipc_msg *msg; /**< host notification */ +#endif #if CONFIG_XRUN_NOTIFICATIONS_ENABLE bool xrun_notification_sent; #endif diff --git a/src/audio/host-zephyr.c b/src/audio/host-zephyr.c index 90b0821caf5a..aefd4abb71c3 100644 --- a/src/audio/host-zephyr.c +++ b/src/audio/host-zephyr.c @@ -246,7 +246,9 @@ void host_common_update(struct host_data *hd, struct comp_dev *dev, uint32_t byt struct comp_buffer *sink; int ret; bool update_mailbox = false; +#if CONFIG_HOST_DMA_IPC_POSITION_UPDATES bool send_ipc = false; +#endif if (dev->direction == SOF_IPC_STREAM_PLAYBACK) { source = hd->dma_buffer; @@ -285,6 +287,7 @@ void host_common_update(struct host_data *hd, struct comp_dev *dev, uint32_t byt if (hd->cont_update_posn) update_mailbox = true; +#if CONFIG_HOST_DMA_IPC_POSITION_UPDATES /* Don't send stream position if no_stream_position == 1 */ if (!hd->no_stream_position) { hd->report_pos += bytes; @@ -304,13 +307,16 @@ void host_common_update(struct host_data *hd, struct comp_dev *dev, uint32_t byt send_ipc = true; } } +#endif if (update_mailbox) { pipeline_get_timestamp(dev->pipeline, dev, &hd->posn); mailbox_stream_write(dev->pipeline->posn_offset, &hd->posn, sizeof(hd->posn)); +#if CONFIG_HOST_DMA_IPC_POSITION_UPDATES if (send_ipc) ipc_msg_send(hd->msg, &hd->posn, false); +#endif } } @@ -721,6 +727,7 @@ __cold int host_common_new(struct host_data *hd, struct comp_dev *dev, ipc_build_stream_posn(&hd->posn, SOF_IPC_STREAM_POSITION, config_id); +#if CONFIG_HOST_DMA_IPC_POSITION_UPDATES hd->msg = ipc_msg_init(hd->posn.rhdr.hdr.cmd, sizeof(hd->posn)); if (!hd->msg) { comp_err(dev, "ipc_msg_init failed"); @@ -728,6 +735,7 @@ __cold int host_common_new(struct host_data *hd, struct comp_dev *dev, return -ENOMEM; } hd->chan_index = -EINVAL; +#endif hd->copy_type = COMP_COPY_NORMAL; #ifdef CONFIG_SOF_USERSPACE_LL @@ -808,7 +816,9 @@ __cold void host_common_free(struct host_data *hd) sof_dma_put(hd->dma); +#if CONFIG_HOST_DMA_IPC_POSITION_UPDATES ipc_msg_free(hd->msg); +#endif dma_sg_free(hd->alloc_ctx.heap, &hd->config.elem_array); } diff --git a/src/ipc/ipc-helper.c b/src/ipc/ipc-helper.c index 6da9bb2692fa..3a938e493cda 100644 --- a/src/ipc/ipc-helper.c +++ b/src/ipc/ipc-helper.c @@ -292,7 +292,9 @@ __cold int ipc_comp_free(struct ipc *ipc, uint32_t comp_id) struct ipc_comp_dev *icd; struct comp_buffer *buffer; struct comp_buffer *safe; +#ifndef CONFIG_SOF_USERSPACE_LL uint32_t flags; +#endif assert_can_be_cold(); From cb28d3d1956fe7bdc2fcea31ee7dd49876cc00fe Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 31 Mar 2026 14:31:24 +0300 Subject: [PATCH 40/94] audio: copier: avoid IRQ lock/unlock in chmap code Copier set_chmap() blocks IRQs to atomically update the converters. This code is not safe to be moved to user-space, so replace the locks with calls to block LL scheduler execution. Signed-off-by: Kai Vehmanen --- src/audio/copier/copier.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/audio/copier/copier.c b/src/audio/copier/copier.c index 93e018408bf3..5268f810a525 100644 --- a/src/audio/copier/copier.c +++ b/src/audio/copier/copier.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -824,7 +825,9 @@ __cold static int set_chmap(struct comp_dev *dev, const void *data, size_t data_ pcm_converter_func process; pcm_converter_func converters[IPC4_COPIER_MODULE_OUTPUT_PINS_COUNT]; int i; +#ifndef CONFIG_SOF_USERSPACE_LL uint32_t irq_flags; +#endif assert_can_be_cold(); @@ -878,15 +881,26 @@ __cold static int set_chmap(struct comp_dev *dev, const void *data, size_t data_ } } - /* Atomically update chmap, process and converters */ + /* Atomically update chmap, process and converters. + * In user-space builds irq_local_disable() is privileged, + * use the LL scheduler lock instead. + */ +#ifdef CONFIG_SOF_USERSPACE_LL + zephyr_ll_lock_sched(cpu_get_id()); +#else irq_local_disable(irq_flags); +#endif cd->dd[0]->chmap = chmap_cfg->channel_map; cd->dd[0]->process = process; for (i = 0; i < IPC4_COPIER_MODULE_OUTPUT_PINS_COUNT; i++) cd->converter[i] = converters[i]; +#ifdef CONFIG_SOF_USERSPACE_LL + zephyr_ll_unlock_sched(cpu_get_id()); +#else irq_local_enable(irq_flags); +#endif return 0; } From 7998e2803f7224e0f5744d7e85cfeb6224428918 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 31 Mar 2026 14:34:35 +0300 Subject: [PATCH 41/94] audio: module_adapter: avoid IRQ lock/unlock in prepare() The module_adapter prepare() blocks IRQs to atomically to connect the sink/source buffers. This code is not safe to be moved to user-space, so replace the locks with calls to block LL scheduler execution, when compiled for user-space. Signed-off-by: Kai Vehmanen --- src/audio/module_adapter/module_adapter.c | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/audio/module_adapter/module_adapter.c b/src/audio/module_adapter/module_adapter.c index 73d26e09ee3a..bd85bd7873b3 100644 --- a/src/audio/module_adapter/module_adapter.c +++ b/src/audio/module_adapter/module_adapter.c @@ -653,7 +653,9 @@ int module_adapter_prepare(struct comp_dev *dev) buff_size, memory_flags, PLATFORM_DCACHE_ALIGN, BUFFER_USAGE_NOT_SHARED); +#ifndef CONFIG_SOF_USERSPACE_LL uint32_t flags; +#endif if (!buffer) { comp_err(dev, "failed to allocate local buffer"); @@ -663,9 +665,17 @@ int module_adapter_prepare(struct comp_dev *dev) vregion_get(md->resources.alloc->vreg); +#ifdef CONFIG_SOF_USERSPACE_LL + zephyr_ll_lock_sched(cpu_get_id()); +#else irq_local_disable(flags); +#endif list_item_prepend(&buffer->buffers_list, &mod->raw_data_buffers_list); +#ifdef CONFIG_SOF_USERSPACE_LL + zephyr_ll_unlock_sched(cpu_get_id()); +#else irq_local_enable(flags); +#endif buffer_set_params(buffer, mod->stream_params, BUFFER_UPDATE_FORCE); audio_buffer_reset(&buffer->audio_buffer); @@ -695,11 +705,21 @@ int module_adapter_prepare(struct comp_dev *dev) list_for_item_safe(blist, _blist, &mod->raw_data_buffers_list) { struct comp_buffer *buffer = container_of(blist, struct comp_buffer, buffers_list); +#ifndef CONFIG_SOF_USERSPACE_LL uint32_t flags; +#endif +#ifdef CONFIG_SOF_USERSPACE_LL + zephyr_ll_lock_sched(cpu_get_id()); +#else irq_local_disable(flags); +#endif list_item_del(&buffer->buffers_list); +#ifdef CONFIG_SOF_USERSPACE_LL + zephyr_ll_unlock_sched(cpu_get_id()); +#else irq_local_enable(flags); +#endif buffer_free(buffer); } @@ -1487,11 +1507,21 @@ void module_adapter_free(struct comp_dev *dev) list_for_item_safe(blist, _blist, &mod->raw_data_buffers_list) { struct comp_buffer *buffer = container_of(blist, struct comp_buffer, buffers_list); +#ifndef CONFIG_SOF_USERSPACE_LL uint32_t flags; +#endif +#ifdef CONFIG_SOF_USERSPACE_LL + zephyr_ll_lock_sched(cpu_get_id()); +#else irq_local_disable(flags); +#endif list_item_del(&buffer->buffers_list); +#ifdef CONFIG_SOF_USERSPACE_LL + zephyr_ll_unlock_sched(cpu_get_id()); +#else irq_local_enable(flags); +#endif buffer_free(buffer); } From 43f403fa796d70284d926ee7d105b8cd1647cd6e Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 14 Apr 2026 20:26:28 +0300 Subject: [PATCH 42/94] audio: module_adapter: make data_blob compatible with user-space Use user-space friendy sof_heap_alloc() for dynamic allocations in data_blob handler. Signed-off-by: Kai Vehmanen --- src/audio/data_blob.c | 47 +++++++++++++++++++---- src/audio/module_adapter/module/generic.c | 4 +- src/include/sof/audio/data_blob.h | 7 +++- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/audio/data_blob.c b/src/audio/data_blob.c index 399244106f95..088d54ee89f5 100644 --- a/src/audio/data_blob.c +++ b/src/audio/data_blob.c @@ -28,8 +28,7 @@ struct comp_data_blob_handler { */ uint32_t single_blob:1; /**< Allocate only one blob. Module can not * be active while reconfguring. - */ - void *(*alloc)(size_t size); /**< alternate allocator, maybe null */ + */ struct k_heap *heap; /**< heap for user-safe alloc, or NULL */ void *(*alloc)(size_t size); /**< alternate allocator, maybe null */ void (*free)(void *buf); /**< alternate free(), maybe null */ /** validator for new data, maybe null */ @@ -632,23 +631,52 @@ static void default_free(void *buf) rfree(buf); } +static void *default_heap_alloc(size_t size) +{ + return sof_heap_alloc(sof_sys_user_heap_get(), + SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, + size, 0); +} + +static void default_heap_free(void *buf) +{ + sof_heap_free(sof_sys_user_heap_get(), buf); +} + struct comp_data_blob_handler * comp_data_blob_handler_new_ext(struct comp_dev *dev, bool single_blob, void *(*alloc)(size_t size), - void (*free)(void *buf)) + void (*free)(void *buf), + struct k_heap *heap) { struct comp_data_blob_handler *handler; comp_dbg(dev, "entry"); - handler = rzalloc(SOF_MEM_FLAG_USER, - sizeof(struct comp_data_blob_handler)); + if (heap) + handler = sof_heap_alloc(heap, + SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, + sizeof(struct comp_data_blob_handler), 0); + else + handler = rzalloc(SOF_MEM_FLAG_USER, + sizeof(struct comp_data_blob_handler)); if (handler) { + if (heap) + memset(handler, 0, sizeof(*handler)); handler->dev = dev; handler->single_blob = single_blob; - handler->alloc = alloc ? alloc : default_alloc; - handler->free = free ? free : default_free; + handler->heap = heap; + if (alloc) { + handler->alloc = alloc; + handler->free = free ? free : default_free; + } else if (heap) { + handler->alloc = default_heap_alloc; + handler->free = default_heap_free; + } else { + handler->alloc = default_alloc; + handler->free = free ? free : default_free; + } } return handler; @@ -662,6 +690,9 @@ void comp_data_blob_handler_free(struct comp_data_blob_handler *blob_handler) comp_free_data_blob(blob_handler); - rfree(blob_handler); + if (blob_handler->heap) + sof_heap_free(blob_handler->heap, blob_handler); + else + rfree(blob_handler); } EXPORT_SYMBOL(comp_data_blob_handler_free); diff --git a/src/audio/module_adapter/module/generic.c b/src/audio/module_adapter/module/generic.c index f9531033dadb..ca66aefd0782 100644 --- a/src/audio/module_adapter/module/generic.c +++ b/src/audio/module_adapter/module/generic.c @@ -284,7 +284,7 @@ EXPORT_SYMBOL(z_impl_mod_alloc_ext); #if CONFIG_COMP_BLOB struct comp_data_blob_handler *mod_data_blob_handler_new(struct processing_module *mod) { - struct module_resources * __maybe_unused res = &mod->priv.resources; + struct module_resources *res = &mod->priv.resources; struct comp_data_blob_handler *bhp; struct module_resource *container; @@ -294,7 +294,7 @@ struct comp_data_blob_handler *mod_data_blob_handler_new(struct processing_modul if (!container) return NULL; - bhp = comp_data_blob_handler_new_ext(mod->dev, false, NULL, NULL); + bhp = comp_data_blob_handler_new_ext(mod->dev, false, NULL, NULL, res->heap); if (!bhp) { container_put(mod, container); return NULL; diff --git a/src/include/sof/audio/data_blob.h b/src/include/sof/audio/data_blob.h index d2eb8a74d66d..58505b2e199b 100644 --- a/src/include/sof/audio/data_blob.h +++ b/src/include/sof/audio/data_blob.h @@ -12,6 +12,7 @@ #include struct comp_dev; +struct k_heap; struct comp_data_blob_handler; @@ -113,11 +114,13 @@ int comp_data_blob_get_cmd(struct comp_data_blob_handler *blob_handler, * @param single_blob Set true for single configuration blob operation * @param alloc Optional blob memory allocator function pointer * @param free Optional blob memory free function pointer + * @param heap Optional heap for user-safe allocation, or NULL for default */ struct comp_data_blob_handler * comp_data_blob_handler_new_ext(struct comp_dev *dev, bool single_blob, void *(*alloc)(size_t size), - void (*free)(void *buf)); + void (*free)(void *buf), + struct k_heap *heap); /** * Returns new data blob handler. @@ -130,7 +133,7 @@ comp_data_blob_handler_new_ext(struct comp_dev *dev, bool single_blob, static inline struct comp_data_blob_handler *comp_data_blob_handler_new(struct comp_dev *dev) { - return comp_data_blob_handler_new_ext(dev, false, NULL, NULL); + return comp_data_blob_handler_new_ext(dev, false, NULL, NULL, NULL); } /** From e6c1d5f29e6a70a605af9c727f40c8629b201a45 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 16 Apr 2026 14:28:54 +0300 Subject: [PATCH 43/94] audio: module-adapter: make generic.c user-space compatible Replace direct rballoc() and rfree() calls with sys_alloc_heap() and sys_alloc_free(), and use sof_sys_user_heap_get() as the heap. This makes the code ready to be used in user-space, including module prepare and free stages. Signed-off-by: Kai Vehmanen --- src/audio/module_adapter/module/generic.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/audio/module_adapter/module/generic.c b/src/audio/module_adapter/module/generic.c index ca66aefd0782..dd3e1eb439ed 100644 --- a/src/audio/module_adapter/module/generic.c +++ b/src/audio/module_adapter/module/generic.c @@ -58,13 +58,15 @@ int module_load_config(struct comp_dev *dev, const void *cfg, size_t size) if (!dst->data) { /* No space for config available yet, allocate now */ - dst->data = rballoc(SOF_MEM_FLAG_USER, size); + dst->data = sof_heap_alloc(sof_sys_user_heap_get(), + SOF_MEM_FLAG_USER, size, 0); } else if (dst->size != size) { /* The size allocated for previous config doesn't match the new one. * Free old container and allocate new one. */ - rfree(dst->data); - dst->data = rballoc(SOF_MEM_FLAG_USER, size); + sof_heap_free(sof_sys_user_heap_get(), dst->data); + dst->data = sof_heap_alloc(sof_sys_user_heap_get(), + SOF_MEM_FLAG_USER, size, 0); } if (!dst->data) { comp_err(dev, "failed to allocate space for setup config."); @@ -539,7 +541,7 @@ int module_prepare(struct processing_module *mod, * as it has been applied during the procedure - it is safe to * free it. */ - rfree(md->cfg.data); + sof_heap_free(sof_sys_user_heap_get(), md->cfg.data); md->cfg.avail = false; md->cfg.data = NULL; @@ -674,7 +676,7 @@ int module_reset(struct processing_module *mod) md->cfg.avail = false; md->cfg.size = 0; - rfree(md->cfg.data); + sof_heap_free(sof_sys_user_heap_get(), md->cfg.data); md->cfg.data = NULL; #if CONFIG_IPC_MAJOR_3 @@ -725,10 +727,10 @@ int module_free(struct processing_module *mod) /* Free all memory shared by module_adapter & module */ md->cfg.avail = false; md->cfg.size = 0; - rfree(md->cfg.data); + sof_heap_free(sof_sys_user_heap_get(), md->cfg.data); md->cfg.data = NULL; if (md->runtime_params) { - rfree(md->runtime_params); + sof_heap_free(sof_sys_user_heap_get(), md->runtime_params); md->runtime_params = NULL; } #if CONFIG_IPC_MAJOR_3 @@ -795,7 +797,9 @@ int module_set_configuration(struct processing_module *mod, } /* Allocate buffer for new params */ - md->runtime_params = rballoc(SOF_MEM_FLAG_USER, md->new_cfg_size); + md->runtime_params = sof_heap_alloc(sof_sys_user_heap_get(), + SOF_MEM_FLAG_USER, + md->new_cfg_size, 0); if (!md->runtime_params) { comp_err(dev, "space allocation for new params failed"); return -ENOMEM; @@ -836,7 +840,7 @@ int module_set_configuration(struct processing_module *mod, md->new_cfg_size = 0; if (md->runtime_params) - rfree(md->runtime_params); + sof_heap_free(sof_sys_user_heap_get(), md->runtime_params); md->runtime_params = NULL; return ret; From 980bb2061e867a764b0d6240e679b2d7c3c2c25b Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 8 May 2026 14:43:25 +0300 Subject: [PATCH 44/94] audio: module_adapter: use module context for allocations Use a module context instead of heap to allocate objects in module-adapter module. This is required to support vregion use. Signed-off-by: Kai Vehmanen --- src/audio/module_adapter/module_adapter.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/audio/module_adapter/module_adapter.c b/src/audio/module_adapter/module_adapter.c index bd85bd7873b3..3554d39420bd 100644 --- a/src/audio/module_adapter/module_adapter.c +++ b/src/audio/module_adapter/module_adapter.c @@ -107,7 +107,6 @@ static struct processing_module *module_adapter_mem_alloc(const struct comp_driv #else mod_heap = drv->user_heap; #endif - mod_heap_user = NULL; heap_size = 0; mod_vreg = NULL; } @@ -124,7 +123,9 @@ static struct processing_module *module_adapter_mem_alloc(const struct comp_driv goto emod; } - struct mod_alloc_ctx *alloc = rmalloc(flags, sizeof(*alloc)); + struct mod_alloc_ctx *alloc; + + alloc = sof_heap_alloc(mod_heap, flags, sizeof(*alloc), 0); if (!alloc) goto ealloc; @@ -160,7 +161,7 @@ static struct processing_module *module_adapter_mem_alloc(const struct comp_driv return mod; edev: - rfree(alloc); + sof_heap_free(mod_heap, alloc); ealloc: if (mod_vreg) vregion_free(mod_vreg, mod); @@ -189,8 +190,9 @@ static void module_adapter_mem_free(struct processing_module *mod) vregion_free(mod_vreg, mod->dev); vregion_free(mod_vreg, mod); - if (!vregion_put(mod_vreg)) - rfree(alloc); + if (!vregion_put(mod_vreg)) { + sof_heap_free(alloc->heap, alloc); + } } else { LOG_INF("mod"); #ifdef CONFIG_SOF_USERSPACE_LL @@ -200,7 +202,7 @@ static void module_adapter_mem_free(struct processing_module *mod) comp_cl_info(drv, "free mod %p with heap %p", mod, mod_heap); sof_heap_free(mod_heap, mod->dev); sof_heap_free(mod_heap, mod); - rfree(alloc); + sof_heap_free(mod_heap, alloc); } } From 4158c5652ad15c6498b745d29a6dbafb6c80929a Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 8 May 2026 14:42:44 +0300 Subject: [PATCH 45/94] audio: module: generic: use module context for blob allocations Use a module context instead of heap to allocate objects for module parameter blobs. This is required to support vregion use. Signed-off-by: Kai Vehmanen --- src/audio/module_adapter/module/generic.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/audio/module_adapter/module/generic.c b/src/audio/module_adapter/module/generic.c index dd3e1eb439ed..6fdf9f77da70 100644 --- a/src/audio/module_adapter/module/generic.c +++ b/src/audio/module_adapter/module/generic.c @@ -296,7 +296,7 @@ struct comp_data_blob_handler *mod_data_blob_handler_new(struct processing_modul if (!container) return NULL; - bhp = comp_data_blob_handler_new_ext(mod->dev, false, NULL, NULL, res->heap); + bhp = comp_data_blob_handler_new_ext(mod->dev, false, NULL, NULL, res->alloc->heap); if (!bhp) { container_put(mod, container); return NULL; From bf41778ed8dfbe51c68e223bb5bed9bae551ddc1 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 15 Apr 2026 13:52:05 +0300 Subject: [PATCH 46/94] audio: make comp_drivers_get() accessible from user-space Make comp_drivers_get() usable from user-space when SOF is built with CONFIG_SOF_USERSPACE_LL. Signed-off-by: Kai Vehmanen --- src/audio/component.c | 8 ++++++++ src/audio/data_blob.c | 4 +++- src/include/sof/audio/component_ext.h | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/audio/component.c b/src/audio/component.c index da597023cf73..a895dc642523 100644 --- a/src/audio/component.c +++ b/src/audio/component.c @@ -38,6 +38,14 @@ LOG_MODULE_REGISTER(component, CONFIG_SOF_LOG_LEVEL); static APP_SYSUSER_BSS SHARED_DATA struct comp_driver_list cd; +#ifdef CONFIG_SOF_USERSPACE_LL +struct comp_driver_list *comp_drivers_get(void) +{ + return platform_shared_get(&cd, sizeof(cd)); +} +EXPORT_SYMBOL(comp_drivers_get); +#endif + SOF_DEFINE_REG_UUID(component); DECLARE_TR_CTX(comp_tr, SOF_UUID(component_uuid), LOG_LEVEL_INFO); diff --git a/src/audio/data_blob.c b/src/audio/data_blob.c index 088d54ee89f5..f62be15e2ed8 100644 --- a/src/audio/data_blob.c +++ b/src/audio/data_blob.c @@ -28,7 +28,9 @@ struct comp_data_blob_handler { */ uint32_t single_blob:1; /**< Allocate only one blob. Module can not * be active while reconfguring. - */ struct k_heap *heap; /**< heap for user-safe alloc, or NULL */ void *(*alloc)(size_t size); /**< alternate allocator, maybe null */ + */ + struct k_heap *heap; /**< heap for user-safe alloc, or NULL */ + void *(*alloc)(size_t size); /**< alternate allocator, maybe null */ void (*free)(void *buf); /**< alternate free(), maybe null */ /** validator for new data, maybe null */ diff --git a/src/include/sof/audio/component_ext.h b/src/include/sof/audio/component_ext.h index d2bbf87a7764..6d67b44014c2 100644 --- a/src/include/sof/audio/component_ext.h +++ b/src/include/sof/audio/component_ext.h @@ -425,10 +425,14 @@ static inline void comp_make_shared(struct comp_dev *dev) dev->is_shared = true; } +#ifdef CONFIG_SOF_USERSPACE_LL +struct comp_driver_list *comp_drivers_get(void); +#else static inline struct comp_driver_list *comp_drivers_get(void) { return sof_get()->comp_drivers; } +#endif #if CONFIG_IPC_MAJOR_4 static inline int comp_ipc4_bind_remote(struct comp_dev *dev, struct bind_info *bind_data) From 5186b00c7b783bee57a9378096f1fe8d8f0dba77 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 26 May 2026 19:29:29 +0300 Subject: [PATCH 47/94] dai: turn dai_get_device() into a syscall Make dai_get_device() a syscall, so it can be called from user-space threads. Signed-off-by: Kai Vehmanen --- src/include/sof/lib/dai-zephyr.h | 6 +++++- src/lib/dai.c | 2 +- zephyr/CMakeLists.txt | 2 ++ zephyr/syscall/dai.c | 14 ++++++++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 zephyr/syscall/dai.c diff --git a/src/include/sof/lib/dai-zephyr.h b/src/include/sof/lib/dai-zephyr.h index 3f02397436dd..09fa0aacad5c 100644 --- a/src/include/sof/lib/dai-zephyr.h +++ b/src/include/sof/lib/dai-zephyr.h @@ -311,7 +311,9 @@ void dai_release_llp_slot(struct dai_data *dd); /** * \brief Retrieve a pointer to the Zephyr device structure for a DAI of a given type and index. */ -const struct device *dai_get_device(enum sof_ipc_dai_type type, uint32_t index); +__syscall const struct device *dai_get_device(enum sof_ipc_dai_type type, uint32_t index); + +const struct device *z_impl_dai_get_device(enum sof_ipc_dai_type type, uint32_t index); /** * \brief Retrieve the list of all DAI devices. @@ -321,4 +323,6 @@ const struct device *dai_get_device(enum sof_ipc_dai_type type, uint32_t index); const struct device **dai_get_device_list(size_t *count); /** @}*/ +#include + #endif /* __SOF_LIB_DAI_ZEPHYR_H__ */ diff --git a/src/lib/dai.c b/src/lib/dai.c index 7a8e44087a65..d51266aa345e 100644 --- a/src/lib/dai.c +++ b/src/lib/dai.c @@ -243,7 +243,7 @@ static int sof_dai_type_to_zephyr(uint32_t type) } } -const struct device *dai_get_device(enum sof_ipc_dai_type type, uint32_t index) +const struct device *z_impl_dai_get_device(enum sof_ipc_dai_type type, uint32_t index) { struct dai_config cfg; int z_type; diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index bce849e85b00..ceb2888dca34 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -628,6 +628,8 @@ zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/audio/module_adapter/module/ge zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/fast-get.h) zephyr_syscall_header(include/rtos/alloc.h) zephyr_library_sources_ifdef(CONFIG_SOF_USERSPACE_INTERFACE_ALLOC syscall/alloc.c) +zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/dai-zephyr.h) +zephyr_library_sources(syscall/dai.c) zephyr_library_link_libraries(SOF) target_link_libraries(SOF INTERFACE zephyr_interface) diff --git a/zephyr/syscall/dai.c b/zephyr/syscall/dai.c new file mode 100644 index 000000000000..ea4f233fa40d --- /dev/null +++ b/zephyr/syscall/dai.c @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. + +#include +#include +#include + +static inline const struct device *z_vrfy_dai_get_device(enum sof_ipc_dai_type type, + uint32_t index) +{ + return z_impl_dai_get_device(type, index); +} +#include From 4cf9114d820e0edc78ed4ee4025138373623ee95 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 21 Apr 2026 16:09:15 +0300 Subject: [PATCH 48/94] audio: chain_dma: add user-space memory and scheduling support Add CONFIG_SOF_USERSPACE_LL support to chain DMA component: - Allocate comp_dev, private data, and DMA buffer from user heap - Use k_work_delayable for periodic task instead of LL timer scheduler to keep DMA operations in kernel context - Guard all changes with #ifdef CONFIG_SOF_USERSPACE_LL Signed-off-by: Kai Vehmanen --- src/audio/chain_dma.c | 91 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/audio/chain_dma.c b/src/audio/chain_dma.c index 15929dd6941c..6271cfb997a6 100644 --- a/src/audio/chain_dma.c +++ b/src/audio/chain_dma.c @@ -15,7 +15,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -59,6 +61,15 @@ struct chain_dma_data { bool xrun_notification_sent; #endif +#ifdef CONFIG_SOF_USERSPACE_LL + /** Kernel workqueue scheduling for chain DMA in userspace builds. + * Chain DMA needs kernel context for DMA operations, so it cannot + * run on the user-space LL timer thread. + */ + struct k_work_delayable dma_work; + bool stopped; +#endif + /* local host DMA config */ struct sof_dma *dma_host; struct dma_chan_data *chan_host; @@ -269,6 +280,25 @@ static enum task_state chain_task_run(void *data) return SOF_TASK_STATE_RESCHEDULE; } +#ifdef CONFIG_SOF_USERSPACE_LL +/** Kernel workqueue handler for chain DMA periodic task. + * Runs chain_task_run() in kernel context and reschedules if needed. + */ +static void chain_dma_work_handler(struct k_work *work) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct chain_dma_data *cd = CONTAINER_OF(dwork, struct chain_dma_data, dma_work); + enum task_state state; + + if (cd->stopped) + return; + + state = chain_task_run(cd); + if (state == SOF_TASK_STATE_RESCHEDULE && !cd->stopped) + k_work_reschedule(dwork, K_USEC(LL_TIMER_PERIOD_US)); +} +#endif + static int chain_task_start(struct comp_dev *dev) { struct chain_dma_data *cd = comp_get_drvdata(dev); @@ -310,6 +340,12 @@ static int chain_task_start(struct comp_dev *dev) } } +#ifdef CONFIG_SOF_USERSPACE_LL + cd->stopped = false; + k_work_init_delayable(&cd->dma_work, chain_dma_work_handler); + k_work_reschedule(&cd->dma_work, K_NO_WAIT); + cd->chain_task.state = SOF_TASK_STATE_QUEUED; +#else ret = schedule_task_init_ll(&cd->chain_task, SOF_UUID(chain_dma_uuid), SOF_SCHEDULE_LL_TIMER, SOF_TASK_PRI_HIGH, chain_task_run, cd, 0, 0); @@ -324,16 +360,19 @@ static int chain_task_start(struct comp_dev *dev) schedule_task_free(&cd->chain_task); goto error_task; } +#endif pm_policy_state_lock_get(PM_STATE_RUNTIME_IDLE, PM_ALL_SUBSTATES); return 0; +#ifndef CONFIG_SOF_USERSPACE_LL error_task: chain_host_stop(dev); chain_link_stop(dev); return ret; +#endif } static int chain_task_pause(struct comp_dev *dev) @@ -341,10 +380,18 @@ static int chain_task_pause(struct comp_dev *dev) struct chain_dma_data *cd = comp_get_drvdata(dev); int ret, ret2; +#ifdef CONFIG_SOF_USERSPACE_LL + if (cd->chain_task.state == SOF_TASK_STATE_FREE) + return 0; + + cd->stopped = true; + cd->first_data_received = false; +#else if (cd->chain_task.state == SOF_TASK_STATE_FREE) return 0; cd->first_data_received = false; +#endif if (cd->stream_direction == SOF_IPC_STREAM_PLAYBACK) { ret = chain_host_stop(dev); ret2 = chain_link_stop(dev); @@ -355,7 +402,12 @@ static int chain_task_pause(struct comp_dev *dev) if (!ret) ret = ret2; +#ifdef CONFIG_SOF_USERSPACE_LL + k_work_cancel_delayable_sync(&cd->dma_work, &(struct k_work_sync){}); + cd->chain_task.state = SOF_TASK_STATE_FREE; +#else schedule_task_free(&cd->chain_task); +#endif pm_policy_state_lock_put(PM_STATE_RUNTIME_IDLE, PM_ALL_SUBSTATES); return ret; @@ -583,8 +635,14 @@ __cold static int chain_task_init(struct comp_dev *dev, uint8_t host_dma_id, uin fifo_size = ALIGN_UP_INTERNAL(fifo_size, addr_align); /* allocate not shared buffer */ +#ifdef CONFIG_SOF_USERSPACE_LL + cd->dma_buffer = buffer_alloc(sof_sys_user_heap_get(), fifo_size, + SOF_MEM_FLAG_USER | SOF_MEM_FLAG_DMA, + addr_align, BUFFER_USAGE_NOT_SHARED); +#else cd->dma_buffer = buffer_alloc(NULL, fifo_size, SOF_MEM_FLAG_USER | SOF_MEM_FLAG_DMA, addr_align, BUFFER_USAGE_NOT_SHARED); +#endif if (!cd->dma_buffer) { comp_err(dev, "failed to alloc dma buffer"); @@ -643,14 +701,31 @@ __cold static struct comp_dev *chain_task_create(const struct comp_driver *drv, if (host_dma_id >= max_chain_number) return NULL; +#ifdef CONFIG_SOF_USERSPACE_LL + dev = sof_heap_alloc(sof_sys_user_heap_get(), + SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, + sizeof(*dev), 0); + if (!dev) + return NULL; + + memset(dev, 0, sizeof(*dev)); + comp_init(drv, dev, sizeof(*dev)); +#else dev = comp_alloc(drv, sizeof(*dev)); if (!dev) return NULL; +#endif +#ifdef CONFIG_SOF_USERSPACE_LL + cd = sof_heap_alloc(sof_sys_user_heap_get(), SOF_MEM_FLAG_USER, sizeof(*cd), 0); +#else cd = rzalloc(SOF_MEM_FLAG_USER, sizeof(*cd)); +#endif if (!cd) goto error; + memset(cd, 0, sizeof(*cd)); + cd->first_data_received = false; cd->cs = scs ? 2 : 4; cd->chain_task.state = SOF_TASK_STATE_INIT; @@ -661,9 +736,17 @@ __cold static struct comp_dev *chain_task_create(const struct comp_driver *drv, if (!ret) return dev; +#ifdef CONFIG_SOF_USERSPACE_LL + sof_heap_free(sof_sys_user_heap_get(), cd); +#else rfree(cd); +#endif error: +#ifdef CONFIG_SOF_USERSPACE_LL + sof_heap_free(sof_sys_user_heap_get(), dev); +#else comp_free_device(dev); +#endif return NULL; } @@ -674,8 +757,16 @@ __cold static void chain_task_free(struct comp_dev *dev) assert_can_be_cold(); chain_release(dev); +#ifdef CONFIG_SOF_USERSPACE_LL + sof_heap_free(sof_sys_user_heap_get(), cd); +#else rfree(cd); +#endif +#ifdef CONFIG_SOF_USERSPACE_LL + sof_heap_free(sof_sys_user_heap_get(), dev); +#else comp_free_device(dev); +#endif } static const struct comp_driver comp_chain_dma = { From 1e2c12af3507a4b348b5815024745882bf295127 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 8 May 2026 14:38:52 +0300 Subject: [PATCH 49/94] audio: chain-dma: use module context for allocations Use a module context instead of heap to allocate objects in chain-dma module. This is required to support vregion use. Signed-off-by: Kai Vehmanen --- src/audio/chain_dma.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/audio/chain_dma.c b/src/audio/chain_dma.c index 6271cfb997a6..64afd7fef1bf 100644 --- a/src/audio/chain_dma.c +++ b/src/audio/chain_dma.c @@ -556,6 +556,7 @@ __cold static int chain_task_init(struct comp_dev *dev, uint8_t host_dma_id, uin uint32_t fifo_size) { struct chain_dma_data *cd = comp_get_drvdata(dev); + struct mod_alloc_ctx *alloc_ctx = NULL; uint32_t addr_align; size_t buff_size; void *buff_addr; @@ -634,15 +635,15 @@ __cold static int chain_task_init(struct comp_dev *dev, uint8_t host_dma_id, uin } fifo_size = ALIGN_UP_INTERNAL(fifo_size, addr_align); - /* allocate not shared buffer */ + #ifdef CONFIG_SOF_USERSPACE_LL - cd->dma_buffer = buffer_alloc(sof_sys_user_heap_get(), fifo_size, + alloc_ctx = ipc_get()->ll_alloc; +#endif + + /* allocate not shared buffer */ + cd->dma_buffer = buffer_alloc(alloc_ctx, fifo_size, SOF_MEM_FLAG_USER | SOF_MEM_FLAG_DMA, addr_align, BUFFER_USAGE_NOT_SHARED); -#else - cd->dma_buffer = buffer_alloc(NULL, fifo_size, SOF_MEM_FLAG_USER | SOF_MEM_FLAG_DMA, - addr_align, BUFFER_USAGE_NOT_SHARED); -#endif if (!cd->dma_buffer) { comp_err(dev, "failed to alloc dma buffer"); From c80980c968cde383a4073f21884aeda64899bc5b Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 25 Mar 2026 19:31:49 +0200 Subject: [PATCH 50/94] (---section audio user PRs STOP) From 863ee726d38f94526c27d85a1fdeb82d9282c797 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 17 Mar 2026 20:21:06 +0200 Subject: [PATCH 51/94] (---section: IPC user support START) From d50037c5affc454bf60c3aad35f37e5ac99fcf18 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 10 Feb 2026 15:17:16 +0200 Subject: [PATCH 52/94] ipc: move standalone-test check later in ipc_init() If CONFIG_SOF_BOOT_TEST_STANDALONE is set, ipc_init() is terminated early. This ensures SOF will not start to generate or respond to IPC messages that could potentially interfere with standalone test cases (some of which send and receive IPCs). The current implementation leaves the component list uninitialized and this can cause trouble to standalone tests that want to utilzie common IPC code to build messages. Fix this problem by executing more of ipc_init() also in the standalone mode. Signed-off-by: Kai Vehmanen --- src/ipc/ipc-common.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index d0d248c9ec77..cde586f83c55 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -294,11 +294,6 @@ __cold int ipc_init(struct sof *sof) tr_dbg(&ipc_tr, "entry"); -#if CONFIG_SOF_BOOT_TEST_STANDALONE - LOG_INF("SOF_BOOT_TEST_STANDALONE, disabling IPC."); - return 0; -#endif - /* init ipc data */ sof->ipc = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, sizeof(*sof->ipc)); if (!sof->ipc) { @@ -329,6 +324,11 @@ __cold int ipc_init(struct sof *sof) io_perf_monitor_init_data(&sof->ipc->io_perf_out_msg_count, &init_data); #endif +#if CONFIG_SOF_BOOT_TEST_STANDALONE + LOG_INF("SOF_BOOT_TEST_STANDALONE, disabling IPC."); + return 0; +#endif + #ifdef __ZEPHYR__ struct k_thread *thread = &sof->ipc->ipc_send_wq.thread; From 5b3821236abacd0b07124006aa895ff3b0c23060 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 1 Apr 2026 14:08:26 +0300 Subject: [PATCH 53/94] userspace: split ipc files into user and kernel features. REVISIT memcpy This is a mostly mechanical split of initial ipc logic into kernel and user files. This is the 1st stage in supporting both privileged kernel and non privileged userspace IPC commands and security surfaces. At a high level library loading and PM will reside as kernel IPC and pipeline and module will become user IPCs. There will be no impact for devices without MMU. Signed-off-by: Liam Girdwood (cherry picked from commit e65c6d14a05c06e048202d7ee02996922265734a) --- src/include/sof/ipc/common.h | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/include/sof/ipc/common.h b/src/include/sof/ipc/common.h index e46fc10b9521..4260a9383ab5 100644 --- a/src/include/sof/ipc/common.h +++ b/src/include/sof/ipc/common.h @@ -24,6 +24,7 @@ struct dma_sg_elem_array; struct ipc_msg; +struct ipc4_message_request; /* validates internal non tail structures within IPC command structure */ #define IPC_IS_SIZE_INVALID(object) \ @@ -166,6 +167,49 @@ struct dai_data; */ int ipc_dai_data_config(struct dai_data *dd, struct comp_dev *dev); +/** + * \brief Processes IPC4 userspace module message. + * @param[in] ipc4 IPC4 message request. + * @param[in] reply IPC message reply structure. + * @return IPC4_SUCCESS on success, error code otherwise. + */ +int ipc4_user_process_module_message(struct ipc4_message_request *ipc4, struct ipc_msg *reply); + +/** + * \brief Processes IPC4 userspace global message. + * @param[in] ipc4 IPC4 message request. + * @param[in] reply IPC message reply structure. + * @return IPC4_SUCCESS on success, error code otherwise. + */ +int ipc4_user_process_glb_message(struct ipc4_message_request *ipc4, struct ipc_msg *reply); + +/** + * \brief Increment the IPC compound message pre-start counter. + * @param[in] msg_id IPC message ID. + */ +void ipc_compound_pre_start(int msg_id); + +/** + * \brief Decrement the IPC compound message pre-start counter on return value status. + * @param[in] msg_id IPC message ID. + * @param[in] ret Return value of the IPC command. + * @param[in] delayed True if the reply is delayed. + */ +void ipc_compound_post_start(uint32_t msg_id, int ret, bool delayed); + +/** + * \brief Complete the IPC compound message. + * @param[in] msg_id IPC message ID. + * @param[in] error Error code of the IPC command. + */ +void ipc_compound_msg_done(uint32_t msg_id, int error); + +/** + * \brief Wait for the IPC compound message to complete. + * @return 0 on success, error code otherwise on timeout. + */ +int ipc_wait_for_compound_msg(void); + /** * \brief create a IPC boot complete message. * @param[in] header header. From 164234c0fee2aae02db7ad3016ea0d21964c1f40 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 26 Mar 2026 15:32:33 +0200 Subject: [PATCH 54/94] ipc4: helper: use LL scheduler lock for userspace builds The ll_block()/ll_unblock() macros use irq_local_disable() and irq_local_enable() for the same-core binding path, or when CONFIG_CROSS_CORE_STREAM is not enabled. These are not available in user-space. Use zephyr_ll_lock_sched()/zephyr_ll_unlock_sched() when building with CONFIG_SOF_USERSPACE_LL to acquire the LL scheduler's own k_mutex instead. Signed-off-by: Kai Vehmanen --- src/ipc/ipc4/helper.c | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index 397eea94c82e..d8ee27752fb0 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -580,6 +580,23 @@ __cold static struct comp_buffer *ipc4_create_buffer(struct comp_dev *src, bool * disable any interrupts. */ +#if CONFIG_SOF_USERSPACE_LL +#define ll_block(cross_core_bind, flags) \ + do { \ + if (cross_core_bind) \ + domain_block(sof_get()->platform_timer_domain); \ + else \ + zephyr_ll_lock_sched(); \ + } while (0) + +#define ll_unblock(cross_core_bind, flags) \ + do { \ + if (cross_core_bind) \ + domain_unblock(sof_get()->platform_timer_domain); \ + else \ + zephyr_ll_unlock_sched(); \ + } while (0) +#else #define ll_block(cross_core_bind, flags) \ do { \ if (cross_core_bind) \ @@ -595,6 +612,7 @@ __cold static struct comp_buffer *ipc4_create_buffer(struct comp_dev *src, bool else \ irq_local_enable(flags); \ } while (0) +#endif /* CONFIG_SOF_USERSPACE_LL */ /* Calling both ll_block() and ll_wait_finished_on_core() makes sure LL will not start its * next cycle and its current cycle on specified core has finished. @@ -626,8 +644,13 @@ static int ll_wait_finished_on_core(struct comp_dev *dev) #else +#if CONFIG_SOF_USERSPACE_LL +#define ll_block(cross_core_bind, flags) zephyr_ll_lock_sched() +#define ll_unblock(cross_core_bind, flags) zephyr_ll_unlock_sched() +#else #define ll_block(cross_core_bind, flags) irq_local_disable(flags) #define ll_unblock(cross_core_bind, flags) irq_local_enable(flags) +#endif /* CONFIG_SOF_USERSPACE_LL */ #endif From a25e50375e6a0bab29d69dcd2b2546e7cecfd539 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 12 Mar 2026 16:33:15 +0200 Subject: [PATCH 55/94] WIP: ipc: implement user-space IPC handling for CREATE_PIPELINE Add a dedicated user-space thread for handling IPC commands that operate on audio pipelines when CONFIG_SOF_USERSPACE_LL is enabled. The kernel IPC handler forwards CREATE_PIPELINE messages to the user thread via k_event signaling and collects the result through a k_sem handshake. The IPC task_mask IPC_TASK_IN_THREAD bit prevents host completion until the user thread finishes. Key changes: - add ipc_user struct and user-space IPC thread with event loop - allocate IPC context statically in a dedicated memory partition - replace sof_get()->ipc with ipc_get() returning static context - forward CREATE_PIPELINE to user thread via ipc_user_forward_cmd() - allocate pipeline and IPC container from user-space heap This is a in-progress implementation that only handles one IPC message type and is thus not a full implementation. This does allow to proceed to test IPC user thread creation and the basic mechanism to handle messages. Signed-off-by: Kai Vehmanen --- src/include/sof/ipc/common.h | 36 +++++ src/ipc/ipc-common.c | 249 ++++++++++++++++++++++++++++++++--- src/ipc/ipc4/handler-user.c | 7 + src/ipc/ipc4/helper.c | 25 +++- zephyr/include/rtos/sof.h | 2 + 5 files changed, 297 insertions(+), 22 deletions(-) diff --git a/src/include/sof/ipc/common.h b/src/include/sof/ipc/common.h index 4260a9383ab5..6727175aa4b4 100644 --- a/src/include/sof/ipc/common.h +++ b/src/include/sof/ipc/common.h @@ -54,6 +54,20 @@ extern struct tr_ctx ipc_tr; #define IPC_TASK_SECONDARY_CORE BIT(2) #define IPC_TASK_POWERDOWN BIT(3) +struct ipc_user { + struct k_thread *thread; + struct k_sem *sem; + struct k_event *event; + /** @brief Copy of IPC4 message primary word forwarded to user thread */ + uint32_t ipc_msg_pri; + /** @brief Copy of IPC4 message extension word forwarded to user thread */ + uint32_t ipc_msg_ext; + /** @brief Result code from user thread processing */ + int result; + struct ipc *ipc; + struct k_thread *audio_thread; +}; + struct ipc { struct k_spinlock lock; /* locking mechanism */ void *comp_data; @@ -75,6 +89,10 @@ struct ipc { struct task ipc_task; #endif +#ifdef CONFIG_SOF_USERSPACE_LL + struct ipc_user *ipc_user_pdata; +#endif + #ifdef CONFIG_SOF_TELEMETRY_IO_PERFORMANCE_MEASUREMENTS /* io performance measurement */ struct io_perf_data_item *io_perf_in_msg_count; @@ -96,6 +114,12 @@ struct ipc { extern struct task_ops ipc_task_ops; +#ifdef CONFIG_SOF_USERSPACE_LL + +struct ipc *ipc_get(void); + +#else + /** * \brief Get the IPC global context. * @return The global IPC context. @@ -105,6 +129,8 @@ static inline struct ipc *ipc_get(void) return sof_get()->ipc; } +#endif /* CONFIG_SOF_USERSPACE_LL */ + /** * \brief Initialise global IPC context. * @param[in,out] sof Global SOF context. @@ -294,4 +320,14 @@ void ipc_complete_cmd(struct ipc *ipc); /* GDB stub: should enter GDB after completing the IPC processing */ extern bool ipc_enter_gdb; +#ifdef CONFIG_SOF_USERSPACE_LL +struct ipc4_message_request; +/** + * @brief Forward an IPC4 command to the user-space thread. + * @param ipc4 Pointer to the IPC4 message request + * @return Result from user thread processing + */ +int ipc_user_forward_cmd(struct ipc4_message_request *ipc4); +#endif + #endif /* __SOF_DRIVERS_IPC_H__ */ diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index cde586f83c55..4d6dc5c75971 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include #include @@ -35,6 +37,16 @@ #include #include +#ifdef __ZEPHYR__ +#include +#endif + +#ifdef CONFIG_SOF_USERSPACE_LL +#include +#include +#include +#endif + #include LOG_MODULE_REGISTER(ipc, CONFIG_SOF_LOG_LEVEL); @@ -43,6 +55,18 @@ SOF_DEFINE_REG_UUID(ipc); DECLARE_TR_CTX(ipc_tr, SOF_UUID(ipc_uuid), LOG_LEVEL_INFO); +#ifdef CONFIG_SOF_USERSPACE_LL +K_APPMEM_PARTITION_DEFINE(ipc_context_part); + +K_APP_BMEM(ipc_context_part) static struct ipc ipc_context; + +struct ipc *ipc_get(void) +{ + return &ipc_context; +} +EXPORT_SYMBOL(ipc_get); +#endif + int ipc_process_on_core(uint32_t core, bool blocking) { struct ipc *ipc = ipc_get(); @@ -256,7 +280,11 @@ void ipc_msg_send(struct ipc_msg *msg, void *data, bool high_priority) list_item_append(&msg->list, &ipc->msg_list); } +#if 0 /*def CONFIG_SOF_USERSPACE_LL */ + LOG_WRN("Skipping IPC worker schedule. TODO to fix\n"); +#else schedule_ipc_worker(); +#endif k_spin_unlock(&ipc->lock, key); } @@ -288,29 +316,208 @@ void ipc_schedule_process(struct ipc *ipc) #endif } +#ifdef CONFIG_SOF_USERSPACE_LL +/* User-space thread for pipeline_two_components test */ +#define IPC_USER_STACKSIZE 8192 + +#define IPC_USER_EVENT_CMD BIT(0) +#define IPC_USER_EVENT_STOP BIT(1) + +static struct k_thread ipc_user_thread; +static K_THREAD_STACK_DEFINE(ipc_user_stack, IPC_USER_STACKSIZE); + +/** + * @brief Forward an IPC4 command to the user-space thread. + * + * Called from kernel context (IPC EDF task) to forward the IPC4 + * message to the user-space thread for processing. Sets + * IPC_TASK_IN_THREAD in task_mask so the host is not signaled + * until the user thread completes. Blocks until the user thread + * finishes processing and returns the result. + * + * @param ipc4 Pointer to the IPC4 message request + * @return Result from user thread processing + */ +int ipc_user_forward_cmd(struct ipc4_message_request *ipc4) +{ + struct ipc *ipc = ipc_get(); + struct ipc_user *pdata = ipc->ipc_user_pdata; + k_spinlock_key_t key; + int ret; + + LOG_DBG("IPC: forward cmd %08x", ipc4->primary.dat); + + /* Copy message words — original buffer may be reused */ + pdata->ipc_msg_pri = ipc4->primary.dat; + pdata->ipc_msg_ext = ipc4->extension.dat; + pdata->ipc = ipc; + + /* Prevent host completion until user thread finishes */ + key = k_spin_lock(&ipc->lock); + ipc->task_mask |= IPC_TASK_IN_THREAD; + k_spin_unlock(&ipc->lock, key); + + /* Wake the user thread */ + k_event_set(pdata->event, IPC_USER_EVENT_CMD); + + /* Wait for user thread to complete */ + ret = k_sem_take(pdata->sem, K_MSEC(10)); + if (ret) { + LOG_ERR("IPC user: sem error %d\n", ret); + return ret; + } + + /* Clear the task mask bit and check for completion */ + key = k_spin_lock(&ipc->lock); + ipc->task_mask &= ~IPC_TASK_IN_THREAD; + ipc_complete_cmd(ipc); + k_spin_unlock(&ipc->lock, key); + + return pdata->result; +} + +/** + * User-space thread entry point for pipeline_two_components test. + * p1 points to the ppl_test_ctx shared with the kernel launcher. + */ +static void ipc_user_thread_fn(void *p1, void *p2, void *p3) +{ + struct ipc_user *ipc_user = p1; + + ARG_UNUSED(p2); + ARG_UNUSED(p3); + + __ASSERT(k_is_user_context(), "expected user context"); + + /* Signal startup complete — unblocks init waiting on semaphore */ + k_sem_give(ipc_user->sem); + LOG_INF("IPC user-space thread started"); + + for (;;) { + uint32_t mask = k_event_wait_safe(ipc_user->event, + IPC_USER_EVENT_CMD | IPC_USER_EVENT_STOP, + false, K_MSEC(5000)); + + LOG_DBG("IPC user wake, mask %u", mask); + + if (mask & IPC_USER_EVENT_CMD) { + struct ipc4_pipeline_create pipe_msg; + + /* Reconstruct the IPC4 message from copied words */ + pipe_msg.primary.dat = ipc_user->ipc_msg_pri; + pipe_msg.extension.dat = ipc_user->ipc_msg_ext; + + /* Execute pipeline creation in user context */ + ipc_user->result = ipc_pipeline_new(ipc_user->ipc, (ipc_pipe_new *)&pipe_msg); + + /* Signal completion — kernel side will finish IPC */ + k_sem_give(ipc_user->sem); + } + + if (mask & IPC_USER_EVENT_STOP) + break; + } +} + +__cold int ipc_user_init(void) +{ + struct ipc *ipc = ipc_get(); + struct ipc_user *ipc_user = sof_heap_alloc(sof_sys_user_heap_get(), SOF_MEM_FLAG_USER, + sizeof(*ipc_user), 0); + int ret; + + ipc_user->sem = k_object_alloc(K_OBJ_SEM); + if (!ipc_user->sem) { + LOG_ERR("user IPC sem alloc failed"); + k_panic(); + } + + ret = k_mem_domain_add_partition(zephyr_ll_mem_domain(), &ipc_context_part); + + k_sem_init(ipc_user->sem, 0, 1); + + /* Allocate kernel objects for the user-space thread */ + ipc_user->event = k_object_alloc(K_OBJ_EVENT); + if (!ipc_user->event) { + LOG_ERR("user IPC event alloc failed"); + k_panic(); + } + k_event_init(ipc_user->event); + + k_thread_create(&ipc_user_thread, ipc_user_stack, IPC_USER_STACKSIZE, + ipc_user_thread_fn, ipc_user, NULL, NULL, + -1, K_USER, K_FOREVER); + + ipc_user->thread = &ipc_user_thread; + k_thread_access_grant(&ipc_user_thread, ipc_user->sem, ipc_user->event); + user_grant_dai_access_all(&ipc_user_thread); + user_grant_dma_access_all(&ipc_user_thread); + user_access_to_mailbox(zephyr_ll_mem_domain(), &ipc_user_thread); + zephyr_ll_grant_access(&ipc_user_thread); + k_mem_domain_add_thread(zephyr_ll_mem_domain(), &ipc_user_thread); + + k_thread_name_set(&ipc_user_thread, __func__); + + /* Store references in ipc struct so kernel handler can forward commands */ + ipc->ipc_user_pdata = ipc_user; + + k_thread_start(&ipc_user_thread); + + struct task *task = zephyr_ll_task_alloc(); + schedule_task_init_ll(task, SOF_UUID(ipc_uuid), SOF_SCHEDULE_LL_TIMER, + 0, NULL, NULL, cpu_get_id(), 0); + ipc_user->audio_thread = scheduler_init_context(task); + + /* Wait for user thread startup — consumes the initial k_sem_give from thread */ + k_sem_take(ipc->ipc_user_pdata->sem, K_FOREVER); + + return 0; +} +#else +static int ipc_user_init(void) +{ + return 0; +} +#endif /* CONFIG_SOF_USERSPACE_LL */ + __cold int ipc_init(struct sof *sof) { + struct k_heap *heap; + struct ipc *ipc; + assert_can_be_cold(); tr_dbg(&ipc_tr, "entry"); +#ifdef CONFIG_SOF_USERSPACE_LL + heap = zephyr_ll_user_heap(); + + ipc = ipc_get(); + memset(ipc, 0, sizeof(*ipc)); +#else + heap = NULL; + /* init ipc data */ - sof->ipc = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, sizeof(*sof->ipc)); - if (!sof->ipc) { + ipc = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, sizeof(*ipc)); + if (!ipc) { tr_err(&ipc_tr, "Unable to allocate IPC data"); return -ENOMEM; } - sof->ipc->comp_data = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, - SOF_IPC_MSG_MAX_SIZE); - if (!sof->ipc->comp_data) { + sof->ipc = ipc; +#endif + + ipc->comp_data = sof_heap_alloc(heap, SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, + SOF_IPC_MSG_MAX_SIZE, 0); + if (!ipc->comp_data) { tr_err(&ipc_tr, "Unable to allocate IPC component data"); - rfree(sof->ipc); + sof_heap_free(heap, ipc); return -ENOMEM; } + memset(ipc->comp_data, 0, SOF_IPC_MSG_MAX_SIZE); - k_spinlock_init(&sof->ipc->lock); - list_init(&sof->ipc->msg_list); - list_init(&sof->ipc->comp_list); + k_spinlock_init(&ipc->lock); + list_init(&ipc->msg_list); + list_init(&ipc->comp_list); #ifdef CONFIG_SOF_TELEMETRY_IO_PERFORMANCE_MEASUREMENTS struct io_perf_data_item init_data = {IO_PERF_IPC_ID, @@ -319,20 +526,17 @@ __cold int ipc_init(struct sof *sof) IO_PERF_POWERED_UP_ENABLED, IO_PERF_D0IX_POWER_MODE, 0, 0, 0 }; - io_perf_monitor_init_data(&sof->ipc->io_perf_in_msg_count, &init_data); + io_perf_monitor_init_data(&ipc->io_perf_in_msg_count, &init_data); init_data.direction = IO_PERF_OUTPUT_DIRECTION; - io_perf_monitor_init_data(&sof->ipc->io_perf_out_msg_count, &init_data); + io_perf_monitor_init_data(&ipc->io_perf_out_msg_count, &init_data); #endif -#if CONFIG_SOF_BOOT_TEST_STANDALONE - LOG_INF("SOF_BOOT_TEST_STANDALONE, disabling IPC."); - return 0; -#endif #ifdef __ZEPHYR__ - struct k_thread *thread = &sof->ipc->ipc_send_wq.thread; + struct k_thread *thread = &ipc->ipc_send_wq.thread; - k_work_queue_start(&sof->ipc->ipc_send_wq, ipc_send_wq_stack, + k_work_queue_init(&ipc->ipc_send_wq); + k_work_queue_start(&ipc->ipc_send_wq, ipc_send_wq_stack, K_THREAD_STACK_SIZEOF(ipc_send_wq_stack), 1, NULL); k_thread_suspend(thread); @@ -344,10 +548,17 @@ __cold int ipc_init(struct sof *sof) k_thread_resume(thread); - k_work_init_delayable(&sof->ipc->z_delayed_work, ipc_work_handler); + k_work_init_delayable(&ipc->z_delayed_work, ipc_work_handler); +#endif + + ipc_user_init(); + +#if CONFIG_SOF_BOOT_TEST_STANDALONE + LOG_INF("SOF_BOOT_TEST_STANDALONE, skipping platform IPC init."); + return 0; #endif - return platform_ipc_init(sof->ipc); + return platform_ipc_init(ipc); } /* Locking: call with ipc->lock held and interrupts disabled */ diff --git a/src/ipc/ipc4/handler-user.c b/src/ipc/ipc4/handler-user.c index df243fa6ff58..38b8e5efc55d 100644 --- a/src/ipc/ipc4/handler-user.c +++ b/src/ipc/ipc4/handler-user.c @@ -90,6 +90,7 @@ static inline const struct ipc4_pipeline_set_state_data *ipc4_get_pipeline_data( /* * Global IPC Operations. */ +#ifndef CONFIG_SOF_USERSPACE_LL __cold static int ipc4_new_pipeline(struct ipc4_message_request *ipc4) { struct ipc *ipc = ipc_get(); @@ -98,6 +99,7 @@ __cold static int ipc4_new_pipeline(struct ipc4_message_request *ipc4) return ipc_pipeline_new(ipc, (ipc_pipe_new *)ipc4); } +#endif __cold static int ipc4_delete_pipeline(struct ipc4_message_request *ipc4) { @@ -681,7 +683,12 @@ int ipc4_user_process_glb_message(struct ipc4_message_request *ipc4, /* pipeline settings */ case SOF_IPC4_GLB_CREATE_PIPELINE: + /* Implementation in progress: forward only CREATE_PIPELINE for now */ +#ifdef CONFIG_SOF_USERSPACE_LL + ret = ipc_user_forward_cmd(ipc4); +#else ret = ipc4_new_pipeline(ipc4); +#endif break; case SOF_IPC4_GLB_DELETE_PIPELINE: ret = ipc4_delete_pipeline(ipc4); diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index d8ee27752fb0..c4b6f839f5ae 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -342,9 +342,16 @@ __cold static int ipc4_create_pipeline(struct ipc4_pipeline_create *pipe_desc, struct ipc_comp_dev *ipc_pipe; struct pipeline *pipe; struct ipc *ipc = ipc_get(); + struct k_heap *heap = NULL; assert_can_be_cold(); +#ifdef CONFIG_SOF_USERSPACE_LL + heap = zephyr_ll_user_heap(); +#endif + + LOG_INF("pipe_desc %x, instance %u", pipe_desc, pipe_desc->primary.r.instance_id); + /* check whether pipeline id is already taken or in use */ ipc_pipe = ipc_get_pipeline_by_id(ipc, pipe_desc->primary.r.instance_id); if (ipc_pipe) { @@ -354,8 +361,9 @@ __cold static int ipc4_create_pipeline(struct ipc4_pipeline_create *pipe_desc, } /* create the pipeline */ - pipe = pipeline_new(NULL, pipe_desc->primary.r.instance_id, + pipe = pipeline_new(heap, pipe_desc->primary.r.instance_id, pipe_desc->primary.r.ppl_priority, 0, pparams); + LOG_INF("pipeline_new() -> %p", pipe); if (!pipe) { tr_err(&ipc_tr, "ipc: pipeline_new() failed"); return IPC4_OUT_OF_MEMORY; @@ -370,12 +378,13 @@ __cold static int ipc4_create_pipeline(struct ipc4_pipeline_create *pipe_desc, pipe->core = pipe_desc->extension.r.core_id; /* allocate the IPC pipeline container */ - ipc_pipe = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, - sizeof(struct ipc_comp_dev)); + ipc_pipe = sof_heap_alloc(heap, SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, + sizeof(struct ipc_comp_dev), 0); if (!ipc_pipe) { pipeline_free(pipe); return IPC4_OUT_OF_MEMORY; } + memset(ipc_pipe, 0, sizeof(*ipc_pipe)); ipc_pipe->pipeline = pipe; ipc_pipe->type = COMP_TYPE_PIPELINE; @@ -386,6 +395,8 @@ __cold static int ipc4_create_pipeline(struct ipc4_pipeline_create *pipe_desc, /* add new pipeline to the list */ list_item_append(&ipc_pipe->list, &ipc->comp_list); + LOG_INF("success"); + return IPC4_SUCCESS; } @@ -707,6 +718,14 @@ __cold int ipc_comp_connect(struct ipc *ipc, ipc_pipe_comp_connect *_connect) #else alloc = NULL; #endif /* CONFIG_ZEPHYR_DP_SCHEDULER */ + +#ifdef CONFIG_SOF_USERSPACE_LL + if (!dp_heap) { + /* use system user heap for non-DP module buffers */ + dp_heap = sof_sys_user_heap_get(); + } +#endif + bool cross_core_bind = source->ipc_config.core != sink->ipc_config.core; /* If both components are on same core -- process IPC on that core, diff --git a/zephyr/include/rtos/sof.h b/zephyr/include/rtos/sof.h index 573e6ac63d77..efe0c1e6acab 100644 --- a/zephyr/include/rtos/sof.h +++ b/zephyr/include/rtos/sof.h @@ -46,8 +46,10 @@ struct sof { int argc; char **argv; +#ifndef CONFIG_SOF_USERSPACE_LL /* ipc */ struct ipc *ipc; +#endif /* system agent */ struct sa *sa; From 28ede09e37556898a223a3e53a3d7a681876af6f Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Mon, 30 Mar 2026 20:06:52 +0300 Subject: [PATCH 56/94] WIP: ipc: ipc4: route PIPELINE_DELETE to user-space handler Incremental change to also handle PIPELINE_DELETE IPC in the user-space IPC thread. Signed-off-by: Kai Vehmanen --- src/ipc/ipc-common.c | 29 +++++++++++++++++++++++------ src/ipc/ipc4/handler-user.c | 6 ++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index 4d6dc5c75971..6425cf1051ad 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -401,14 +401,31 @@ static void ipc_user_thread_fn(void *p1, void *p2, void *p3) LOG_DBG("IPC user wake, mask %u", mask); if (mask & IPC_USER_EVENT_CMD) { - struct ipc4_pipeline_create pipe_msg; + struct ipc4_message_request msg; /* Reconstruct the IPC4 message from copied words */ - pipe_msg.primary.dat = ipc_user->ipc_msg_pri; - pipe_msg.extension.dat = ipc_user->ipc_msg_ext; - - /* Execute pipeline creation in user context */ - ipc_user->result = ipc_pipeline_new(ipc_user->ipc, (ipc_pipe_new *)&pipe_msg); + msg.primary.dat = ipc_user->ipc_msg_pri; + msg.extension.dat = ipc_user->ipc_msg_ext; + + switch (msg.primary.r.type) { + case SOF_IPC4_GLB_CREATE_PIPELINE: + ipc_user->result = ipc_pipeline_new(ipc_user->ipc, + (ipc_pipe_new *)&msg); + break; + case SOF_IPC4_GLB_DELETE_PIPELINE: { + struct ipc4_pipeline_delete *pipe = + (struct ipc4_pipeline_delete *)&msg; + + ipc_user->result = ipc_pipeline_free( + ipc_user->ipc, pipe->primary.r.instance_id); + break; + } + default: + LOG_ERR("IPC user: unsupported cmd type %d", + msg.primary.r.type); + ipc_user->result = -EINVAL; + break; + } /* Signal completion — kernel side will finish IPC */ k_sem_give(ipc_user->sem); diff --git a/src/ipc/ipc4/handler-user.c b/src/ipc/ipc4/handler-user.c index 38b8e5efc55d..3b09b70804b0 100644 --- a/src/ipc/ipc4/handler-user.c +++ b/src/ipc/ipc4/handler-user.c @@ -101,6 +101,7 @@ __cold static int ipc4_new_pipeline(struct ipc4_message_request *ipc4) } #endif +#ifndef CONFIG_SOF_USERSPACE_LL __cold static int ipc4_delete_pipeline(struct ipc4_message_request *ipc4) { struct ipc4_pipeline_delete *pipe; @@ -113,6 +114,7 @@ __cold static int ipc4_delete_pipeline(struct ipc4_message_request *ipc4) return ipc_pipeline_free(ipc, pipe->primary.r.instance_id); } +#endif static int ipc4_pcm_params(struct ipc_comp_dev *pcm_dev) { @@ -691,7 +693,11 @@ int ipc4_user_process_glb_message(struct ipc4_message_request *ipc4, #endif break; case SOF_IPC4_GLB_DELETE_PIPELINE: +#ifdef CONFIG_SOF_USERSPACE_LL + ret = ipc_user_forward_cmd(ipc4); +#else ret = ipc4_delete_pipeline(ipc4); +#endif break; case SOF_IPC4_GLB_SET_PIPELINE_STATE: ret = ipc4_set_pipeline_state(ipc4); From 351202007d9156b8902df73a95a6f7d4759d9b1b Mon Sep 17 00:00:00 2001 From: Jyri Sarha Date: Wed, 1 Apr 2026 13:58:09 +0300 Subject: [PATCH 57/94] ipc: make IPC stack thread size configurable Make IPC user thread stack size configurable with SOF_IPC_USER_THREAD_STACK_SIZE (default 4096). Also pin the IPC user thread on primar CPU. Signed-off-by: Jyri Sarha Signed-off-by: Kai Vehmanen --- src/ipc/ipc-common.c | 9 +++++---- zephyr/Kconfig | 8 ++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index 6425cf1051ad..a12365c66a5d 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -318,13 +318,12 @@ void ipc_schedule_process(struct ipc *ipc) #ifdef CONFIG_SOF_USERSPACE_LL /* User-space thread for pipeline_two_components test */ -#define IPC_USER_STACKSIZE 8192 #define IPC_USER_EVENT_CMD BIT(0) #define IPC_USER_EVENT_STOP BIT(1) static struct k_thread ipc_user_thread; -static K_THREAD_STACK_DEFINE(ipc_user_stack, IPC_USER_STACKSIZE); +static K_THREAD_STACK_DEFINE(ipc_user_stack, CONFIG_SOF_IPC_USER_THREAD_STACK_SIZE); /** * @brief Forward an IPC4 command to the user-space thread. @@ -461,7 +460,8 @@ __cold int ipc_user_init(void) } k_event_init(ipc_user->event); - k_thread_create(&ipc_user_thread, ipc_user_stack, IPC_USER_STACKSIZE, + k_thread_create(&ipc_user_thread, ipc_user_stack, + CONFIG_SOF_IPC_USER_THREAD_STACK_SIZE, ipc_user_thread_fn, ipc_user, NULL, NULL, -1, K_USER, K_FOREVER); @@ -473,7 +473,8 @@ __cold int ipc_user_init(void) zephyr_ll_grant_access(&ipc_user_thread); k_mem_domain_add_thread(zephyr_ll_mem_domain(), &ipc_user_thread); - k_thread_name_set(&ipc_user_thread, __func__); + k_thread_cpu_pin(&ipc_user_thread, PLATFORM_PRIMARY_CORE_ID); + k_thread_name_set(&ipc_user_thread, "ipc_user"); /* Store references in ipc struct so kernel handler can forward commands */ ipc->ipc_user_pdata = ipc_user; diff --git a/zephyr/Kconfig b/zephyr/Kconfig index 10e00e4c7eaa..82851bdd0980 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -318,3 +318,11 @@ config STACK_SIZE_IPC_TX default 2048 help IPC sender work-queue thread stack size. Keep a power of 2. + +config SOF_IPC_USER_THREAD_STACK_SIZE + int "IPC user thread stack size" + default 8192 + depends on SOF_USERSPACE_LL + help + Stack size for the userspace IPC thread. + Keep a power of 2. From 71c1d8bb08b41429a8d3fa41ef36c1f26bd67227 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 20 May 2026 10:02:34 +0300 Subject: [PATCH 58/94] ipc: ipc4: use sof_heap_alloc in ipc4_add_comp_dev() Use sof_heap_alloc() and the new sof_sys_user_heap_get() to route the allocs to correct heap based on SOF build options. If the audio pipeline code is run completely in user-space, a compatible heap should be used. Signed-off-by: Kai Vehmanen --- src/ipc/ipc4/helper.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index c4b6f839f5ae..3574dcc5f4e4 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -1329,12 +1329,13 @@ __cold static int ipc4_add_comp_dev(struct comp_dev *dev) } /* allocate the IPC component container */ - icd = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, - sizeof(struct ipc_comp_dev)); + icd = sof_heap_alloc(sof_sys_user_heap_get(), SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, + sizeof(struct ipc_comp_dev), 0); if (!icd) { tr_err(&ipc_tr, "alloc failed"); return IPC4_OUT_OF_MEMORY; } + memset(icd, 0, sizeof(*icd)); icd->cd = dev; icd->type = COMP_TYPE_COMPONENT; From bdd46585c653c6fa1cf9d18fe87492b04a4cd794 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 29 May 2026 13:24:57 +0300 Subject: [PATCH 59/94] ipc: turn ipc_msg_reply() into a system call Make ipc_msg_reply() a system call, so it can be called from audio thread even when audio thread is running in a user-space thread. Signed-off-by: Kai Vehmanen --- src/include/sof/ipc/common.h | 2 +- src/include/sof/ipc/ipc_reply.h | 28 ++++++++++++++++++++++++++++ src/ipc/ipc3/helper.c | 2 +- src/ipc/ipc4/handler-kernel.c | 16 +++++++++++++++- zephyr/CMakeLists.txt | 1 + 5 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 src/include/sof/ipc/ipc_reply.h diff --git a/src/include/sof/ipc/common.h b/src/include/sof/ipc/common.h index 6727175aa4b4..7fa6e1977a26 100644 --- a/src/include/sof/ipc/common.h +++ b/src/include/sof/ipc/common.h @@ -310,7 +310,7 @@ int ipc_process_on_core(uint32_t core, bool blocking); * \brief reply to an IPC message. * @param[in] reply pointer to the reply structure. */ -void ipc_msg_reply(struct sof_ipc_reply *reply); +#include /** * \brief Call platform-specific IPC completion function. diff --git a/src/include/sof/ipc/ipc_reply.h b/src/include/sof/ipc/ipc_reply.h new file mode 100644 index 000000000000..756fd535ca30 --- /dev/null +++ b/src/include/sof/ipc/ipc_reply.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. All rights reserved. + */ + +#ifndef __SOF_IPC_IPC_REPLY_H__ +#define __SOF_IPC_IPC_REPLY_H__ + +#include + +struct sof_ipc_reply; + +/** + * \brief reply to an IPC message. + * @param[in] reply pointer to the reply structure. + */ +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_USERSPACE_LL) +__syscall void ipc_msg_reply(struct sof_ipc_reply *reply); +#else +void z_impl_ipc_msg_reply(struct sof_ipc_reply *reply); +#define ipc_msg_reply z_impl_ipc_msg_reply +#endif + +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_USERSPACE_LL) +#include +#endif + +#endif /* __SOF_IPC_IPC_REPLY_H__ */ diff --git a/src/ipc/ipc3/helper.c b/src/ipc/ipc3/helper.c index 4d87f042dd1d..fd2af4603b29 100644 --- a/src/ipc/ipc3/helper.c +++ b/src/ipc/ipc3/helper.c @@ -710,7 +710,7 @@ int ipc_comp_new(struct ipc *ipc, ipc_comp *_comp) return 0; } -void ipc_msg_reply(struct sof_ipc_reply *reply) +void z_impl_ipc_msg_reply(struct sof_ipc_reply *reply) { struct ipc *ipc = ipc_get(); k_spinlock_key_t key; diff --git a/src/ipc/ipc4/handler-kernel.c b/src/ipc/ipc4/handler-kernel.c index d7d9f417806a..b600e946ef0e 100644 --- a/src/ipc/ipc4/handler-kernel.c +++ b/src/ipc/ipc4/handler-kernel.c @@ -40,6 +40,11 @@ #include #include +#ifdef __ZEPHYR__ +#include +#include +#endif + #include #include #include @@ -509,7 +514,7 @@ void ipc_send_buffer_status_notify(void) } #endif -void ipc_msg_reply(struct sof_ipc_reply *reply) +void z_impl_ipc_msg_reply(struct sof_ipc_reply *reply) { struct ipc4_message_request in; @@ -517,6 +522,15 @@ void ipc_msg_reply(struct sof_ipc_reply *reply) ipc_compound_msg_done(in.primary.r.type, reply->error); } +#ifdef CONFIG_USERSPACE +void z_vrfy_ipc_msg_reply(struct sof_ipc_reply *reply) +{ + K_OOPS(K_SYSCALL_MEMORY_READ(reply, sizeof(*reply))); + z_impl_ipc_msg_reply(reply); +} +#include +#endif + void ipc_cmd(struct ipc_cmd_hdr *_hdr) { struct ipc4_message_request *in = ipc4_get_message_request(); diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index ceb2888dca34..7e9e6d81839c 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -626,6 +626,7 @@ zephyr_library_sources_ifdef(CONFIG_SHELL zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/audio/module_adapter/module/generic.h) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/fast-get.h) +zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/ipc/ipc_reply.h) zephyr_syscall_header(include/rtos/alloc.h) zephyr_library_sources_ifdef(CONFIG_SOF_USERSPACE_INTERFACE_ALLOC syscall/alloc.c) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/dai-zephyr.h) From 204fe5c5627065deeb293418dbf08f1b433f7f1a Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 19 Mar 2026 21:39:40 +0200 Subject: [PATCH 60/94] ipc: ipc4: use correct API to get DMA status Use sof_dma_get_status() call to allow the audio pipeline to be run in user-space. Signed-off-by: Kai Vehmanen --- src/ipc/ipc4/dai.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ipc/ipc4/dai.c b/src/ipc/ipc4/dai.c index 58c08d9ec04b..1a0135cb5d5d 100644 --- a/src/ipc/ipc4/dai.c +++ b/src/ipc/ipc4/dai.c @@ -450,7 +450,7 @@ void dai_dma_position_update(struct dai_data *dd, struct comp_dev *dev) if (!dd->slot_info.node_id) return; - ret = dma_get_status(dd->dma->z_dev, dd->chan_index, &status); + ret = sof_dma_get_status(dd->dma, dd->chan_index, &status); if (ret < 0) return; From 775cde47bf6aaf9ce2d2153a0b716bdc88cec0c1 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 20 May 2026 10:04:10 +0300 Subject: [PATCH 61/94] ipc: ipc4: dai: fix direct use of DMA driver calls Fix a few remaining uses of direct DMA driver calls. Use the sof_dma.h wrapper instead, allowing the code to be used also from user-space. Signed-off-by: Kai Vehmanen --- src/ipc/ipc4/dai.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ipc/ipc4/dai.c b/src/ipc/ipc4/dai.c index 1a0135cb5d5d..1540d95cf0f0 100644 --- a/src/ipc/ipc4/dai.c +++ b/src/ipc/ipc4/dai.c @@ -236,9 +236,9 @@ void dai_dma_release(struct dai_data *dd, struct comp_dev *dev) * TODO: refine power management when stream is paused */ /* if reset is after pause dma has already been stopped */ - dma_stop(dd->dma->z_dev, dd->chan_index); + sof_dma_stop(dd->dma, dd->chan_index); - dma_release_channel(dd->dma->z_dev, dd->chan_index); + sof_dma_release_channel(dd->dma, dd->chan_index); dd->chan_index = -EINVAL; } } From edb82c31ee55e38e8e8ddf1afb0a9f2a98364298 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 20 Mar 2026 21:44:59 +0200 Subject: [PATCH 62/94] ipc: use application heap for IPC pipeline and component allocations Replace rfree() calls with sof_heap_free() using the system user heap for IPC component and pipeline objects. In ipc4_create_pipeline(), replace the conditional zephyr_ll_user_heap() with the generic sof_sys_user_heap_get() to unify the allocation path across configurations. Signed-off-by: Kai Vehmanen --- src/ipc/ipc-helper.c | 2 +- src/ipc/ipc4/helper.c | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/ipc/ipc-helper.c b/src/ipc/ipc-helper.c index 3a938e493cda..90c472e7af1f 100644 --- a/src/ipc/ipc-helper.c +++ b/src/ipc/ipc-helper.c @@ -369,7 +369,7 @@ __cold int ipc_comp_free(struct ipc *ipc, uint32_t comp_id) icd->cd = NULL; list_item_del(&icd->list); - rfree(icd); + sof_heap_free(sof_sys_user_heap_get(), icd); return 0; } diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index 3574dcc5f4e4..a381f4feda1e 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -342,14 +342,10 @@ __cold static int ipc4_create_pipeline(struct ipc4_pipeline_create *pipe_desc, struct ipc_comp_dev *ipc_pipe; struct pipeline *pipe; struct ipc *ipc = ipc_get(); - struct k_heap *heap = NULL; + struct k_heap *heap = sof_sys_user_heap_get(); assert_can_be_cold(); -#ifdef CONFIG_SOF_USERSPACE_LL - heap = zephyr_ll_user_heap(); -#endif - LOG_INF("pipe_desc %x, instance %u", pipe_desc, pipe_desc->primary.r.instance_id); /* check whether pipeline id is already taken or in use */ @@ -553,7 +549,7 @@ __cold int ipc_pipeline_free(struct ipc *ipc, uint32_t comp_id) ipc_pipe->pipeline = NULL; list_item_del(&ipc_pipe->list); - rfree(ipc_pipe); + sof_heap_free(sof_sys_user_heap_get(), ipc_pipe); return IPC4_SUCCESS; } @@ -1094,7 +1090,7 @@ __cold int ipc4_chain_dma_state(struct comp_dev *dev, struct ipc4_chain_dma *cdm if (icd->cd != dev) continue; list_item_del(&icd->list); - rfree(icd); + sof_heap_free(sof_sys_user_heap_get(), icd); break; } comp_free(dev); From 2654335d1a26cec4888296d625411cffdde06816 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 31 Mar 2026 14:45:40 +0300 Subject: [PATCH 63/94] ipc: ipc4: helper: make ipc4_search_for_drv() userspace compatible Do not use IRQ disable/enable when built for user-space. The driver list is immutable by IPC processing time so no lock is needed. Signed-off-by: Kai Vehmanen --- src/ipc/ipc4/helper.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index a381f4feda1e..b97d41d65069 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -1188,11 +1188,19 @@ __cold static const struct comp_driver *ipc4_search_for_drv(const void *uuid) struct list_item *clist; const struct comp_driver *drv = NULL; struct comp_driver_info *info; +#ifndef CONFIG_SOF_USERSPACE_LL uint32_t flags; +#endif assert_can_be_cold(); + /* Driver list is populated at boot before IPC processing starts. + * In user-space builds irq_local_disable() is privileged, but the + * list is immutable by this point so no lock is needed. + */ +#ifndef CONFIG_SOF_USERSPACE_LL irq_local_disable(flags); +#endif /* search driver list with UUID */ list_for_item(clist, &drivers->list) { @@ -1208,7 +1216,9 @@ __cold static const struct comp_driver *ipc4_search_for_drv(const void *uuid) } } +#ifndef CONFIG_SOF_USERSPACE_LL irq_local_enable(flags); +#endif return drv; } From 47cfe4c3060cdd6028aac167717610adaf30ab5d Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 28 May 2026 14:48:04 +0300 Subject: [PATCH 64/94] WIP: schedule: add multi-core support for user-space LL scheduler - add arch_schedulers_get_for_core() for task-carried core routing - route 8 task-aware scheduler callers via task->core under CONFIG_SOF_USERSPACE_LL - fix zephyr_domain_thread_tid() to accept core parameter instead of hardcoding core 0 - create domain threads for secondary cores in secondary_core_init() - remove FIXME workaround from arch_schedulers_get() Signed-off-by: Kai Vehmanen --- src/audio/pipeline/pipeline-schedule.c | 6 +- src/include/sof/schedule/ll_schedule_domain.h | 6 +- src/include/sof/schedule/schedule.h | 61 +++++++++++++++++++ src/init/init.c | 16 +++++ src/ipc/ipc4/helper.c | 8 +-- src/schedule/zephyr_domain.c | 5 +- src/schedule/zephyr_ll.c | 23 ++++--- uuid-registry.txt | 1 + zephyr/schedule.c | 15 +++++ 9 files changed, 122 insertions(+), 19 deletions(-) diff --git a/src/audio/pipeline/pipeline-schedule.c b/src/audio/pipeline/pipeline-schedule.c index 7e19b43963ff..cddfdc7165db 100644 --- a/src/audio/pipeline/pipeline-schedule.c +++ b/src/audio/pipeline/pipeline-schedule.c @@ -291,7 +291,9 @@ void pipeline_schedule_triggered(struct pipeline_walk_context *ctx, * while pipeline state is being updated. The k_mutex is re-entrant * so schedule_task() calls inside the critical section are safe. */ - zephyr_ll_lock_sched(); + int sched_core = ppl_data->start->ipc_config.core; + + zephyr_ll_lock_sched(sched_core); #else uint32_t flags; @@ -358,7 +360,7 @@ void pipeline_schedule_triggered(struct pipeline_walk_context *ctx, } } #ifdef CONFIG_SOF_USERSPACE_LL - zephyr_ll_unlock_sched(); + zephyr_ll_unlock_sched(sched_core); #else irq_local_enable(flags); #endif diff --git a/src/include/sof/schedule/ll_schedule_domain.h b/src/include/sof/schedule/ll_schedule_domain.h index 5a1451c310dd..7424b62bc124 100644 --- a/src/include/sof/schedule/ll_schedule_domain.h +++ b/src/include/sof/schedule/ll_schedule_domain.h @@ -122,8 +122,8 @@ struct k_heap *zephyr_ll_user_heap(void); bool zephyr_ll_user_heap_verify(struct k_heap *heap); void zephyr_ll_user_resources_init(void); void zephyr_ll_grant_access(struct k_thread *thread); -void zephyr_ll_lock_sched(void); -void zephyr_ll_unlock_sched(void); +void zephyr_ll_lock_sched(int core); +void zephyr_ll_unlock_sched(int core); #endif /* CONFIG_SOF_USERSPACE_LL */ static inline struct ll_schedule_domain *domain_init @@ -322,7 +322,7 @@ struct ll_schedule_domain *zephyr_ll_domain(void); struct ll_schedule_domain *zephyr_domain_init(int clk); #define timer_domain_init(timer, clk) zephyr_domain_init(clk) #ifdef CONFIG_SOF_USERSPACE_LL -struct k_thread *zephyr_domain_thread_tid(struct ll_schedule_domain *domain); +struct k_thread *zephyr_domain_thread_tid(struct ll_schedule_domain *domain, int core); struct k_mem_domain *zephyr_ll_mem_domain(void); #endif /* CONFIG_SOF_USERSPACE_LL */ #endif /* __ZEPHYR__ */ diff --git a/src/include/sof/schedule/schedule.h b/src/include/sof/schedule/schedule.h index 9418064beb02..1ff6180b962e 100644 --- a/src/include/sof/schedule/schedule.h +++ b/src/include/sof/schedule/schedule.h @@ -189,6 +189,10 @@ struct schedulers { */ struct schedulers **arch_schedulers_get(void); +#if CONFIG_SOF_USERSPACE_LL +struct schedulers **arch_schedulers_get_for_core(int core); +#endif + /** * Retrieves scheduler's data. * @param type SOF_SCHEDULE_ type. @@ -209,10 +213,39 @@ static inline void *scheduler_get_data(uint16_t type) return NULL; } +#if CONFIG_SOF_USERSPACE_LL +/** + * Retrieves scheduler's data for a specific core. + * @param type SOF_SCHEDULE_ type. + * @param core Core ID to get scheduler data for. + * @return Pointer to scheduler's data. + * + * Safe to call from user-space context — does not use cpu_get_id(). + */ +static inline void *scheduler_get_data_for_core(uint16_t type, int core) +{ + struct schedulers *schedulers = *arch_schedulers_get_for_core(core); + struct schedule_data *sch; + struct list_item *slist; + + list_for_item(slist, &schedulers->list) { + sch = container_of(slist, struct schedule_data, list); + if (type == sch->type) + return sch->data; + } + + return NULL; +} +#endif + /** See scheduler_ops::schedule_task_running */ static inline int schedule_task_running(struct task *task) { +#if CONFIG_SOF_USERSPACE_LL + struct schedulers *schedulers = *arch_schedulers_get_for_core(task->core); +#else struct schedulers *schedulers = *arch_schedulers_get(); +#endif struct schedule_data *sch; struct list_item *slist; @@ -234,7 +267,11 @@ static inline int schedule_task_running(struct task *task) static inline int schedule_task(struct task *task, uint64_t start, uint64_t period) { +#if CONFIG_SOF_USERSPACE_LL + struct schedulers *schedulers = *arch_schedulers_get_for_core(task->core); +#else struct schedulers *schedulers = *arch_schedulers_get(); +#endif struct schedule_data *sch; struct list_item *slist; @@ -255,7 +292,11 @@ static inline int schedule_task(struct task *task, uint64_t start, static inline int schedule_task_before(struct task *task, uint64_t start, uint64_t period, struct task *before) { +#if CONFIG_SOF_USERSPACE_LL + struct schedulers *schedulers = *arch_schedulers_get_for_core(task->core); +#else struct schedulers *schedulers = *arch_schedulers_get(); +#endif struct schedule_data *sch; struct list_item *slist; @@ -281,7 +322,11 @@ static inline int schedule_task_before(struct task *task, uint64_t start, static inline int schedule_task_after(struct task *task, uint64_t start, uint64_t period, struct task *after) { +#if CONFIG_SOF_USERSPACE_LL + struct schedulers *schedulers = *arch_schedulers_get_for_core(task->core); +#else struct schedulers *schedulers = *arch_schedulers_get(); +#endif struct schedule_data *sch; struct list_item *slist; @@ -306,7 +351,11 @@ static inline int schedule_task_after(struct task *task, uint64_t start, /** See scheduler_ops::reschedule_task */ static inline int reschedule_task(struct task *task, uint64_t start) { +#if CONFIG_SOF_USERSPACE_LL + struct schedulers *schedulers = *arch_schedulers_get_for_core(task->core); +#else struct schedulers *schedulers = *arch_schedulers_get(); +#endif struct schedule_data *sch; struct list_item *slist; @@ -328,7 +377,11 @@ static inline int reschedule_task(struct task *task, uint64_t start) /** See scheduler_ops::schedule_task_cancel */ static inline int schedule_task_cancel(struct task *task) { +#if CONFIG_SOF_USERSPACE_LL + struct schedulers *schedulers = *arch_schedulers_get_for_core(task->core); +#else struct schedulers *schedulers = *arch_schedulers_get(); +#endif struct schedule_data *sch; struct list_item *slist; @@ -344,7 +397,11 @@ static inline int schedule_task_cancel(struct task *task) /** See scheduler_ops::schedule_task_free */ static inline int schedule_task_free(struct task *task) { +#if CONFIG_SOF_USERSPACE_LL + struct schedulers *schedulers = *arch_schedulers_get_for_core(task->core); +#else struct schedulers *schedulers = *arch_schedulers_get(); +#endif struct schedule_data *sch; struct list_item *slist; @@ -392,7 +449,11 @@ static inline int schedulers_restore(void) /** See scheduler_ops::scheduler_init_context */ static inline struct k_thread *scheduler_init_context(struct task *task) { +#if CONFIG_SOF_USERSPACE_LL + struct schedulers *schedulers = *arch_schedulers_get_for_core(task->core); +#else struct schedulers *schedulers = *arch_schedulers_get(); +#endif struct schedule_data *sch; struct list_item *slist; diff --git a/src/init/init.c b/src/init/init.c index e8d374c810ad..58607df1254a 100644 --- a/src/init/init.c +++ b/src/init/init.c @@ -47,6 +47,10 @@ LOG_MODULE_REGISTER(init, CONFIG_SOF_LOG_LEVEL); +#if CONFIG_SOF_USERSPACE_LL +SOF_DEFINE_REG_UUID(sec_core_init); +#endif + /* main firmware context */ static struct sof sof; @@ -135,6 +139,18 @@ __cold int secondary_core_init(struct sof *sof) return err; #endif /* CONFIG_ZEPHYR_DP_SCHEDULER */ +#if CONFIG_SOF_USERSPACE_LL + /* Create domain thread for this secondary core's LL scheduler */ + { + struct task *task = zephyr_ll_task_alloc(); + + schedule_task_init_ll(task, SOF_UUID(sec_core_init_uuid), + SOF_SCHEDULE_LL_TIMER, + 0, NULL, NULL, cpu_get_id(), 0); + scheduler_init_context(task); + } +#endif + /* initialize IDC mechanism */ trace_point(TRACE_BOOT_PLATFORM_IDC); err = idc_init(); diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index b97d41d65069..038487536aec 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -593,7 +593,7 @@ __cold static struct comp_buffer *ipc4_create_buffer(struct comp_dev *src, bool if (cross_core_bind) \ domain_block(sof_get()->platform_timer_domain); \ else \ - zephyr_ll_lock_sched(); \ + zephyr_ll_lock_sched(cpu_get_id()); \ } while (0) #define ll_unblock(cross_core_bind, flags) \ @@ -601,7 +601,7 @@ __cold static struct comp_buffer *ipc4_create_buffer(struct comp_dev *src, bool if (cross_core_bind) \ domain_unblock(sof_get()->platform_timer_domain); \ else \ - zephyr_ll_unlock_sched(); \ + zephyr_ll_unlock_sched(cpu_get_id()); \ } while (0) #else #define ll_block(cross_core_bind, flags) \ @@ -652,8 +652,8 @@ static int ll_wait_finished_on_core(struct comp_dev *dev) #else #if CONFIG_SOF_USERSPACE_LL -#define ll_block(cross_core_bind, flags) zephyr_ll_lock_sched() -#define ll_unblock(cross_core_bind, flags) zephyr_ll_unlock_sched() +#define ll_block(cross_core_bind, flags) zephyr_ll_lock_sched(cpu_get_id()) +#define ll_unblock(cross_core_bind, flags) zephyr_ll_unlock_sched(cpu_get_id()) #else #define ll_block(cross_core_bind, flags) irq_local_disable(flags) #define ll_unblock(cross_core_bind, flags) irq_local_enable(flags) diff --git a/src/schedule/zephyr_domain.c b/src/schedule/zephyr_domain.c index 8d739c4ab888..f90f70ae4436 100644 --- a/src/schedule/zephyr_domain.c +++ b/src/schedule/zephyr_domain.c @@ -476,13 +476,12 @@ static void zephyr_domain_thread_free(struct ll_schedule_domain *domain, tr_info(&ll_tr, "thread_free done, core %d", core); } -struct k_thread *zephyr_domain_thread_tid(struct ll_schedule_domain *domain) +struct k_thread *zephyr_domain_thread_tid(struct ll_schedule_domain *domain, int core) { struct zephyr_domain *zephyr_domain = ll_sch_domain_get_pdata(domain); - int core = cpu_get_id(); struct zephyr_domain_thread *dt = zephyr_domain->domain_thread + core; - tr_dbg(&ll_tr, "entry"); + tr_dbg(&ll_tr, "entry core %d", core); return dt->ll_thread; } diff --git a/src/schedule/zephyr_ll.c b/src/schedule/zephyr_ll.c index bc76028c1a6d..359053786a88 100644 --- a/src/schedule/zephyr_ll.c +++ b/src/schedule/zephyr_ll.c @@ -65,8 +65,15 @@ static void zephyr_ll_unlock(struct zephyr_ll *sch, uint32_t *flags) static void zephyr_ll_assert_core(const struct zephyr_ll *sch) { - assert(CONFIG_CORE_COUNT == 1 || IS_ENABLED(CONFIG_SOF_USERSPACE_LL) || - sch->core == cpu_get_id()); +#if CONFIG_SOF_USERSPACE_LL + /* In user-space mode, cpu_get_id() is not available. + * Core correctness is ensured by task->core routing in + * schedule.h and verified at task schedule time. + */ + (void)sch; +#else + assert(CONFIG_CORE_COUNT == 1 || sch->core == cpu_get_id()); +#endif } /* Locking: caller should hold the domain lock */ @@ -528,7 +535,7 @@ struct k_thread *zephyr_ll_init_context(void *data, struct task *task) assert(!k_is_user_context()); - return zephyr_domain_thread_tid(sch->ll_domain); + return zephyr_domain_thread_tid(sch->ll_domain, task->core); } #endif @@ -568,9 +575,10 @@ void zephyr_ll_grant_access(struct k_thread *thread) * schedule_task() calls within the locked section will not deadlock. * Must be paired with zephyr_ll_unlock_sched(). */ -void zephyr_ll_lock_sched(void) +void zephyr_ll_lock_sched(int core) { - struct zephyr_ll *sch = (struct zephyr_ll *)scheduler_get_data(SOF_SCHEDULE_LL_TIMER); + struct zephyr_ll *sch = (struct zephyr_ll *)scheduler_get_data_for_core(SOF_SCHEDULE_LL_TIMER, + core); sof_umutex_lock(&sch->lock, K_FOREVER); } @@ -578,9 +586,10 @@ void zephyr_ll_lock_sched(void) /** * Unlock the LL scheduler after a previous zephyr_ll_lock_sched() call. */ -void zephyr_ll_unlock_sched(void) +void zephyr_ll_unlock_sched(int core) { - struct zephyr_ll *sch = (struct zephyr_ll *)scheduler_get_data(SOF_SCHEDULE_LL_TIMER); + struct zephyr_ll *sch = (struct zephyr_ll *)scheduler_get_data_for_core(SOF_SCHEDULE_LL_TIMER, + core); sof_umutex_unlock(&sch->lock); } diff --git a/uuid-registry.txt b/uuid-registry.txt index dc5cf5e15297..b4c27f1c895d 100644 --- a/uuid-registry.txt +++ b/uuid-registry.txt @@ -146,6 +146,7 @@ d7f6712d-131c-45a7-82ed6aa9dc2291ea pm_runtime 9302adf5-88be-4234-a0a7dca538ef81f4 sai 3dee06de-f25a-4e10-ae1fabc9573873ea schedule 70d223ef-2b91-4aac-b444d89a0db2793a sdma +bdcb1461-34f5-4047-b9cc70fdf8dfb234 sec_core_init 55a88ed5-3d18-46ca-88f10ee6eae9930f selector 32fe92c1-1e17-4fc2-9758c7f3542e980a selector4 cf90d851-68a2-4987-a2de85aed0c8531c sgen_mt8186 diff --git a/zephyr/schedule.c b/zephyr/schedule.c index 1e3971a33682..90d50dd11f0a 100644 --- a/zephyr/schedule.c +++ b/zephyr/schedule.c @@ -25,3 +25,18 @@ struct schedulers **arch_schedulers_get(void) return _schedulers + cpu_get_id(); } EXPORT_SYMBOL(arch_schedulers_get); + +#if CONFIG_SOF_USERSPACE_LL +/** + * Retrieves registered schedulers for a specific core. + * @param core Core ID to get schedulers for. + * @return List of registered schedulers for the specified core. + * + * Safe to call from user-space context — does not use cpu_get_id(). + */ +struct schedulers **arch_schedulers_get_for_core(int core) +{ + return _schedulers + core; +} +EXPORT_SYMBOL(arch_schedulers_get_for_core); +#endif From 4a706ac19dc25d87f494c755dd5117dbe95ab288 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Mon, 13 Apr 2026 16:51:05 +0300 Subject: [PATCH 65/94] ipc: ipc-helper: trace context not used in user-space No need to copy the trace context object to buffer object, when audio pipelines are running in user-space. Signed-off-by: Kai Vehmanen --- src/ipc/ipc-helper.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ipc/ipc-helper.c b/src/ipc/ipc-helper.c index 90c472e7af1f..d1694a5b1930 100644 --- a/src/ipc/ipc-helper.c +++ b/src/ipc/ipc-helper.c @@ -87,8 +87,10 @@ __cold struct comp_buffer *buffer_new(struct mod_alloc_ctx *alloc, buffer->stream.runtime_stream_params.pipeline_id = desc->comp.pipeline_id; buffer->core = desc->comp.core; +#if !defined(CONFIG_SOF_USERSPACE_LL) memcpy_s(&buffer->tctx, sizeof(struct tr_ctx), &buffer_tr, sizeof(struct tr_ctx)); +#endif } return buffer; From 839788bc71b2548fd55f6f9c89332db0c375dc95 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 5 May 2026 12:55:40 +0300 Subject: [PATCH 66/94] ipc: ipc4: use the core number from IPC config To make ipc4_create_buffer() usable from user-space, do not call cpu_get_id() but instead rely on "src->ipc_config.core". If code is run in kernel space, check the core matching current active core, but skip the check if run in user-space (as check is privileged). Signed-off-by: Kai Vehmanen --- src/ipc/ipc4/helper.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index 038487536aec..1d8ab00b10d5 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -566,7 +566,10 @@ __cold static struct comp_buffer *ipc4_create_buffer(struct comp_dev *src, bool ipc_buf.size = buf_size; ipc_buf.comp.id = IPC4_COMP_ID(src_queue, dst_queue); ipc_buf.comp.pipeline_id = src->ipc_config.pipeline_id; - ipc_buf.comp.core = cpu_get_id(); + + assert(IS_ENABLED(CONFIG_SOF_USERSPACE_LL) || src->ipc_config.core == cpu_get_id()); + ipc_buf.comp.core = src->ipc_config.core; + return buffer_new(alloc, &ipc_buf, is_shared); } From 0e2ccd9a08ea081c690fb33c1cebb609d36f7c6b Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Mon, 13 Apr 2026 17:04:02 +0300 Subject: [PATCH 67/94] ipc: ipc4: helper: enable limited ipc_comp_connect() in user-space Make ipc_comp_connect() safe to run in user-space with limited functionality. The core id queries are privileged, so limit connections to cases where pipeline is running on 0. Signed-off-by: Kai Vehmanen --- src/ipc/ipc4/helper.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index 1d8ab00b10d5..6c8ba2c37290 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -655,8 +655,8 @@ static int ll_wait_finished_on_core(struct comp_dev *dev) #else #if CONFIG_SOF_USERSPACE_LL -#define ll_block(cross_core_bind, flags) zephyr_ll_lock_sched(cpu_get_id()) -#define ll_unblock(cross_core_bind, flags) zephyr_ll_unlock_sched(cpu_get_id()) +#define ll_block(cross_core_bind, flags) zephyr_ll_lock_sched(0) +#define ll_unblock(cross_core_bind, flags) zephyr_ll_unlock_sched(0) #else #define ll_block(cross_core_bind, flags) irq_local_disable(flags) #define ll_unblock(cross_core_bind, flags) irq_local_enable(flags) @@ -857,6 +857,12 @@ __cold int ipc_comp_connect(struct ipc *ipc, ipc_pipe_comp_connect *_connect) */ ll_block(cross_core_bind, flags); + /* + * TODO: for multicore user support, comp_bufffer_connect() + * needs to be converted to a syscall. for now, limit to core0 + */ + assert(IS_ENABLED(CONFIG_SOF_USERSPACE_LL) || source->ipc_config.core == cpu_get_id()); + if (cross_core_bind) { #if CONFIG_CROSS_CORE_STREAM /* Make sure LL has finished on both cores */ From 776c0cbd202fd8a45e6f4d5ee524af368ddd5b29 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 10 Apr 2026 18:30:50 +0300 Subject: [PATCH 68/94] WIP: ipc: ipc4: route MOD_CONFIG_GET/SET to user-space handler Move handling of SOF_IPC4_MOD_CONFIG_GET/GET to user-space IPC thread when SOF is built with CONFIG_SOF_USERSPACE_LL. Handling these IPC messages require calls to module interface get/set_attribute() methods, which must be run in user-space. Signed-off-by: Kai Vehmanen --- src/include/ipc4/handler.h | 10 ++++++ src/include/sof/ipc/common.h | 2 ++ src/ipc/ipc-common.c | 67 ++++++++++++++++++++++++++---------- src/ipc/ipc4/handler-user.c | 48 ++++++++++++++++++++++++-- 4 files changed, 107 insertions(+), 20 deletions(-) diff --git a/src/include/ipc4/handler.h b/src/include/ipc4/handler.h index b25cb98e9427..fa8259d02e8b 100644 --- a/src/include/ipc4/handler.h +++ b/src/include/ipc4/handler.h @@ -16,6 +16,16 @@ struct ipc4_message_request; */ int ipc4_user_process_module_message(struct ipc4_message_request *ipc4, struct ipc_msg *reply); +/** + * @brief Process MOD_CONFIG_GET or MOD_CONFIG_SET in any execution context. + * @param[in] ipc4 IPC4 message request. + * @param[in] set true for CONFIG_SET, false for CONFIG_GET. + * @param[out] reply_ext Receives extension value for CONFIG_GET (may be NULL). + * @return IPC4 status code (0 on success). + */ +int ipc4_process_module_config(struct ipc4_message_request *ipc4, + bool set, uint32_t *reply_ext); + /** * \brief Processes IPC4 userspace global message. * @param[in] ipc4 IPC4 message request. diff --git a/src/include/sof/ipc/common.h b/src/include/sof/ipc/common.h index 7fa6e1977a26..80b71f12e27a 100644 --- a/src/include/sof/ipc/common.h +++ b/src/include/sof/ipc/common.h @@ -64,6 +64,8 @@ struct ipc_user { uint32_t ipc_msg_ext; /** @brief Result code from user thread processing */ int result; + /** @brief Reply extension word from user thread (e.g. CONFIG_GET result) */ + uint32_t reply_ext; struct ipc *ipc; struct k_thread *audio_thread; }; diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index a12365c66a5d..0a470a0b14ac 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -45,6 +45,8 @@ #include #include #include +#include +#include #endif #include @@ -406,24 +408,53 @@ static void ipc_user_thread_fn(void *p1, void *p2, void *p3) msg.primary.dat = ipc_user->ipc_msg_pri; msg.extension.dat = ipc_user->ipc_msg_ext; - switch (msg.primary.r.type) { - case SOF_IPC4_GLB_CREATE_PIPELINE: - ipc_user->result = ipc_pipeline_new(ipc_user->ipc, - (ipc_pipe_new *)&msg); - break; - case SOF_IPC4_GLB_DELETE_PIPELINE: { - struct ipc4_pipeline_delete *pipe = - (struct ipc4_pipeline_delete *)&msg; - - ipc_user->result = ipc_pipeline_free( - ipc_user->ipc, pipe->primary.r.instance_id); - break; - } - default: - LOG_ERR("IPC user: unsupported cmd type %d", - msg.primary.r.type); - ipc_user->result = -EINVAL; - break; + ipc_user->reply_ext = 0; + + if (msg.primary.r.msg_tgt == + SOF_IPC4_MESSAGE_TARGET_MODULE_MSG) { + /* Module message dispatch */ + switch (msg.primary.r.type) { + case SOF_IPC4_MOD_CONFIG_GET: + ipc_user->result = + ipc4_process_module_config( + &msg, false, + &ipc_user->reply_ext); + break; + case SOF_IPC4_MOD_CONFIG_SET: + ipc_user->result = + ipc4_process_module_config( + &msg, true, NULL); + break; + default: + LOG_ERR("IPC user: unsupported module cmd type %d", + msg.primary.r.type); + ipc_user->result = -EINVAL; + break; + } + } else { + /* Global message dispatch */ + switch (msg.primary.r.type) { + case SOF_IPC4_GLB_CREATE_PIPELINE: + ipc_user->result = + ipc_pipeline_new(ipc_user->ipc, + (ipc_pipe_new *)&msg); + break; + case SOF_IPC4_GLB_DELETE_PIPELINE: { + struct ipc4_pipeline_delete *pipe = + (struct ipc4_pipeline_delete *)&msg; + + ipc_user->result = + ipc_pipeline_free( + ipc_user->ipc, + pipe->primary.r.instance_id); + break; + } + default: + LOG_ERR("IPC user: unsupported glb cmd type %d", + msg.primary.r.type); + ipc_user->result = -EINVAL; + break; + } } /* Signal completion — kernel side will finish IPC */ diff --git a/src/ipc/ipc4/handler-user.c b/src/ipc/ipc4/handler-user.c index 3b09b70804b0..f8d8d3ab2730 100644 --- a/src/ipc/ipc4/handler-user.c +++ b/src/ipc/ipc4/handler-user.c @@ -818,7 +818,22 @@ __cold static int ipc4_unbind_module_instance(struct ipc4_message_request *ipc4) return ipc_comp_disconnect(ipc, (ipc_pipe_comp_connect *)&bu); } -static int ipc4_set_get_config_module_instance(struct ipc4_message_request *ipc4, bool set) +/** + * @brief Process MOD_CONFIG_GET or MOD_CONFIG_SET in any execution context. + * + * Looks up the target component by module_id:instance_id, verifies the + * driver supports get_attribute/set_attribute, and dispatches the + * operation. For GET, the retrieved value is written to @p reply_ext. + * + * Callable from both the IPC kernel task and the IPC user thread. + * + * @param ipc4 Pointer to the IPC4 message request (primary + extension) + * @param set true for CONFIG_SET, false for CONFIG_GET + * @param reply_ext Output: receives the extension value for CONFIG_GET (may be NULL for SET) + * @return IPC4 status code (0 on success) + */ +__cold int ipc4_process_module_config(struct ipc4_message_request *ipc4, + bool set, uint32_t *reply_ext) { struct ipc4_module_config *config = (struct ipc4_module_config *)ipc4; int (*function)(struct comp_dev *dev, uint32_t type, void *value); @@ -868,8 +883,21 @@ static int ipc4_set_get_config_module_instance(struct ipc4_message_request *ipc4 ret = IPC4_INVALID_CONFIG_PARAM_ID; } + if (!set && reply_ext) + *reply_ext = config->extension.dat; + + return ret; +} + +static int ipc4_set_get_config_module_instance(struct ipc4_message_request *ipc4, bool set) +{ + uint32_t reply_ext; + int ret; + + ret = ipc4_process_module_config(ipc4, set, &reply_ext); + if (!set) - msg_reply->extension = config->extension.dat; + msg_reply->extension = reply_ext; return ret; } @@ -1296,10 +1324,26 @@ __cold int ipc4_user_process_module_message(struct ipc4_message_request *ipc4, ret = ipc4_init_module_instance(ipc4); break; case SOF_IPC4_MOD_CONFIG_GET: +#ifdef CONFIG_SOF_USERSPACE_LL + /* Forward to user thread for privilege-separated execution */ + ret = ipc_user_forward_cmd(ipc4); + if (!ret) { + struct ipc *ipc = ipc_get(); + struct ipc_user *pdata = ipc->ipc_user_pdata; + + msg_reply->extension = pdata->reply_ext; + } +#else ret = ipc4_set_get_config_module_instance(ipc4, false); +#endif break; case SOF_IPC4_MOD_CONFIG_SET: +#ifdef CONFIG_SOF_USERSPACE_LL + /* Forward to user thread for privilege-separated execution */ + ret = ipc_user_forward_cmd(ipc4); +#else ret = ipc4_set_get_config_module_instance(ipc4, true); +#endif break; case SOF_IPC4_MOD_LARGE_CONFIG_GET: ret = ipc4_get_large_config_module_instance(ipc4); From 912a9eb7f65db56a5680180abbe30d55854bc178 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 10 Apr 2026 18:28:53 +0300 Subject: [PATCH 69/94] WIP: ipc: ipc4: route MOD_BIND/UNBIND to user-space handler Move handling of SOF_IPC4_MOD_BIND/UNBIND to user-space IPC thread when SOF is built with CONFIG_SOF_USERSPACE_LL. Handling these IPC messages require calls to module interface bind/unbind() methods, which must be run in user-space. Signed-off-by: Kai Vehmanen --- src/ipc/ipc-common.c | 22 +++++++++++++++++++--- src/ipc/ipc4/handler-user.c | 8 ++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index 0a470a0b14ac..be176d71587d 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -410,8 +410,7 @@ static void ipc_user_thread_fn(void *p1, void *p2, void *p3) ipc_user->reply_ext = 0; - if (msg.primary.r.msg_tgt == - SOF_IPC4_MESSAGE_TARGET_MODULE_MSG) { + if (msg.primary.r.msg_tgt == SOF_IPC4_MESSAGE_TARGET_MODULE_MSG) { /* Module message dispatch */ switch (msg.primary.r.type) { case SOF_IPC4_MOD_CONFIG_GET: @@ -425,6 +424,24 @@ static void ipc_user_thread_fn(void *p1, void *p2, void *p3) ipc4_process_module_config( &msg, true, NULL); break; + case SOF_IPC4_MOD_BIND: { + struct ipc4_module_bind_unbind bu; + + memcpy_s(&bu, sizeof(bu), &msg, sizeof(msg)); + ipc_user->result = ipc_comp_connect( + ipc_user->ipc, + (ipc_pipe_comp_connect *)&bu); + break; + } + case SOF_IPC4_MOD_UNBIND: { + struct ipc4_module_bind_unbind bu; + + memcpy_s(&bu, sizeof(bu), &msg, sizeof(msg)); + ipc_user->result = ipc_comp_disconnect( + ipc_user->ipc, + (ipc_pipe_comp_connect *)&bu); + break; + } default: LOG_ERR("IPC user: unsupported module cmd type %d", msg.primary.r.type); @@ -442,7 +459,6 @@ static void ipc_user_thread_fn(void *p1, void *p2, void *p3) case SOF_IPC4_GLB_DELETE_PIPELINE: { struct ipc4_pipeline_delete *pipe = (struct ipc4_pipeline_delete *)&msg; - ipc_user->result = ipc_pipeline_free( ipc_user->ipc, diff --git a/src/ipc/ipc4/handler-user.c b/src/ipc/ipc4/handler-user.c index f8d8d3ab2730..5d8735b6ca25 100644 --- a/src/ipc/ipc4/handler-user.c +++ b/src/ipc/ipc4/handler-user.c @@ -1352,10 +1352,18 @@ __cold int ipc4_user_process_module_message(struct ipc4_message_request *ipc4, ret = ipc4_set_large_config_module_instance(ipc4); break; case SOF_IPC4_MOD_BIND: +#ifdef CONFIG_SOF_USERSPACE_LL + ret = ipc_user_forward_cmd(ipc4); +#else ret = ipc4_bind_module_instance(ipc4); +#endif break; case SOF_IPC4_MOD_UNBIND: +#ifdef CONFIG_SOF_USERSPACE_LL + ret = ipc_user_forward_cmd(ipc4); +#else ret = ipc4_unbind_module_instance(ipc4); +#endif break; case SOF_IPC4_MOD_DELETE_INSTANCE: ret = ipc4_delete_module_instance(ipc4); From d22a13218f5b559302526f5199799bd92fd1a38b Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Mon, 11 May 2026 15:36:28 +0300 Subject: [PATCH 70/94] WIP: ipc: ipc4: route MOD_INIT_INSTANCE to user-space handler Move handling of SOF_IPC4_IPC4_MOD_INIT_INSTANCE partially to user-space IPC thread when SOF is built with CONFIG_SOF_USERSPACE_LL. Module init involves multiple privileged actions, so part of module init handling is still done in kernel space, but the module specific initialization is run in user-space. Signed-off-by: Kai Vehmanen --- src/include/sof/audio/buffer.h | 1 + src/include/sof/ipc/common.h | 12 ++++ src/include/sof/ipc/topology.h | 7 +++ src/ipc/ipc-common.c | 67 ++++++++++++++++++++++ src/ipc/ipc4/handler-user.c | 59 +++++++++++++++++++ src/ipc/ipc4/helper.c | 102 ++++++++++++++++++++++++++++++++- zephyr/lib/userspace_helper.c | 37 ++++++++++++ 7 files changed, 283 insertions(+), 2 deletions(-) diff --git a/src/include/sof/audio/buffer.h b/src/include/sof/audio/buffer.h index 6e6b8a9caef8..02c62caf9809 100644 --- a/src/include/sof/audio/buffer.h +++ b/src/include/sof/audio/buffer.h @@ -34,6 +34,7 @@ struct comp_dev; struct buffer_cb_transact; + /** \name Trace macros * @{ */ diff --git a/src/include/sof/ipc/common.h b/src/include/sof/ipc/common.h index 80b71f12e27a..0218b72519a0 100644 --- a/src/include/sof/ipc/common.h +++ b/src/include/sof/ipc/common.h @@ -22,6 +22,7 @@ #include #include +struct comp_driver; struct dma_sg_elem_array; struct ipc_msg; struct ipc4_message_request; @@ -68,6 +69,17 @@ struct ipc_user { uint32_t reply_ext; struct ipc *ipc; struct k_thread *audio_thread; + /** @brief Original kernel driver pointer for restoring dev->drv after create */ + const struct comp_driver *init_drv; + /** + * @brief User-accessible copy of comp_driver + tr_ctx for create(). + * + * The comp_driver and tr_ctx structs reside in kernel memory + * (.rodata/.data) which is not user-readable. The kernel handler + * copies them here before forwarding to the user thread. + * Size verified by BUILD_ASSERT in handler-user.c. + */ + uint8_t init_drv_data[160] __aligned(4); }; struct ipc { diff --git a/src/include/sof/ipc/topology.h b/src/include/sof/ipc/topology.h index 3503c7a407d8..590424ee8be9 100644 --- a/src/include/sof/ipc/topology.h +++ b/src/include/sof/ipc/topology.h @@ -49,6 +49,13 @@ typedef uint32_t ipc_comp; struct ipc_comp_dev; const struct comp_driver *ipc4_get_comp_drv(uint32_t module_id); struct comp_dev *ipc4_get_comp_dev(uint32_t comp_id); +int ipc4_add_comp_dev(struct comp_dev *dev); +#ifdef CONFIG_SOF_USERSPACE_LL +struct ipc4_message_request; +struct comp_driver; +struct comp_dev *comp_new_ipc4_user(struct ipc4_message_request *ipc4, + const struct comp_driver *drv); +#endif int ipc4_chain_manager_create(struct ipc4_chain_dma *cdma); int ipc4_chain_dma_state(struct comp_dev *dev, struct ipc4_chain_dma *cdma); int ipc4_create_chain_dma(struct ipc *ipc, struct ipc4_chain_dma *cdma); diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index be176d71587d..3f8b0cdbc482 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -442,6 +442,67 @@ static void ipc_user_thread_fn(void *p1, void *p2, void *p3) (ipc_pipe_comp_connect *)&bu); break; } + case SOF_IPC4_MOD_INIT_INSTANCE: { + /* User thread creates the component — + * drv->ops.create() runs in user-space so + * untrusted module code does not execute + * with kernel privileges. + * + * init_drv = original kernel pointer + * init_drv_data = user-accessible copy + */ + const struct comp_driver *orig_drv = + ipc_user->init_drv; + const struct comp_driver *drv_copy = + (const struct comp_driver *) + ipc_user->init_drv_data; + + ipc_user->init_drv = NULL; + if (!orig_drv) { + ipc_user->result = + IPC4_MOD_NOT_INITIALIZED; + break; + } + + struct comp_dev *dev = + comp_new_ipc4_user(&msg, drv_copy); + + if (!dev) { + ipc_user->result = + IPC4_MOD_NOT_INITIALIZED; + break; + } + + /* Restore original kernel driver pointer. + * comp_init() set dev->drv to the copy; + * runtime code expects the canonical + * kernel address. + */ + dev->drv = orig_drv; + + ipc_user->result = + ipc4_add_comp_dev(dev); + if (ipc_user->result != IPC4_SUCCESS) + break; + + comp_update_ibs_obs_cpc(dev); + ipc_user->result = 0; + break; + } + case SOF_IPC4_MOD_DELETE_INSTANCE: { + struct ipc4_module_delete_instance module; + + memcpy_s(&module, sizeof(module), &msg, sizeof(msg)); + uint32_t comp_id = IPC4_COMP_ID( + module.primary.r.module_id, + module.primary.r.instance_id); + ipc_user->result = ipc_comp_free( + ipc_user->ipc, comp_id); + if (ipc_user->result < 0) + ipc_user->result = + IPC4_INVALID_RESOURCE_ID; + break; + } default: LOG_ERR("IPC user: unsupported module cmd type %d", msg.primary.r.type); @@ -533,6 +594,12 @@ __cold int ipc_user_init(void) 0, NULL, NULL, cpu_get_id(), 0); ipc_user->audio_thread = scheduler_init_context(task); + /* Grant ipc_user thread permission on the audio thread object. + * Needed so user-space dai_common_new() can call + * k_thread_access_grant(audio_thread, dai_mutex) from user context. + */ + k_thread_access_grant(&ipc_user_thread, ipc_user->audio_thread); + /* Wait for user thread startup — consumes the initial k_sem_give from thread */ k_sem_take(ipc->ipc_user_pdata->sem, K_FOREVER); diff --git a/src/ipc/ipc4/handler-user.c b/src/ipc/ipc4/handler-user.c index 5d8735b6ca25..1448f2db28cb 100644 --- a/src/ipc/ipc4/handler-user.c +++ b/src/ipc/ipc4/handler-user.c @@ -1321,7 +1321,62 @@ __cold int ipc4_user_process_module_message(struct ipc4_message_request *ipc4, switch (type) { case SOF_IPC4_MOD_INIT_INSTANCE: +#ifdef CONFIG_SOF_USERSPACE_LL + { + /* User-space init: kernel does driver lookup only (requires + * access to IMR manifest and driver list in kernel memory). + * Component creation (drv->ops.create) runs in user thread + * so untrusted module code does not execute in kernel context. + * Cross-core creation stays fully in kernel. + */ + struct ipc4_module_init_instance mi; + + BUILD_ASSERT(sizeof(struct comp_driver) + sizeof(struct tr_ctx) <= + sizeof(((struct ipc_user *)0)->init_drv_data), + "ipc_user.init_drv_data too small for driver copy"); + + memcpy_s(&mi, sizeof(mi), ipc4, sizeof(*ipc4)); + if (!cpu_is_me(mi.extension.r.core_id)) { + ret = ipc4_init_module_instance(ipc4); + } else { + struct ipc *ipc = ipc_get(); + uint32_t comp_id = IPC4_COMP_ID(mi.primary.r.module_id, + mi.primary.r.instance_id); + const struct comp_driver *drv = ipc4_get_comp_drv( + IPC4_MOD_ID(comp_id)); + + if (!drv) { + ret = IPC4_MOD_NOT_INITIALIZED; + } else { + struct ipc_user *pdata = ipc->ipc_user_pdata; + + /* Copy comp_driver and tr_ctx into + * user-accessible ipc_user buffer — + * originals are in kernel .rodata/.data + * and not readable from user mode. + */ + struct comp_driver *drv_copy = + (struct comp_driver *)pdata->init_drv_data; + struct tr_ctx *tctx_copy = + (struct tr_ctx *)(pdata->init_drv_data + + sizeof(struct comp_driver)); + + memcpy_s(drv_copy, sizeof(*drv_copy), + drv, sizeof(*drv)); + if (drv->tctx) { + memcpy_s(tctx_copy, sizeof(*tctx_copy), + drv->tctx, sizeof(*drv->tctx)); + drv_copy->tctx = tctx_copy; + } + + pdata->init_drv = drv; + ret = ipc_user_forward_cmd(ipc4); + } + } + } +#else ret = ipc4_init_module_instance(ipc4); +#endif break; case SOF_IPC4_MOD_CONFIG_GET: #ifdef CONFIG_SOF_USERSPACE_LL @@ -1366,7 +1421,11 @@ __cold int ipc4_user_process_module_message(struct ipc4_message_request *ipc4, #endif break; case SOF_IPC4_MOD_DELETE_INSTANCE: +#ifdef CONFIG_SOF_USERSPACE_LL + ret = ipc_user_forward_cmd(ipc4); +#else ret = ipc4_delete_module_instance(ipc4); +#endif break; case SOF_IPC4_MOD_ENTER_MODULE_RESTORE: case SOF_IPC4_MOD_EXIT_MODULE_RESTORE: diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index 6c8ba2c37290..e466711e8fe2 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -64,7 +64,6 @@ LOG_MODULE_DECLARE(ipc, CONFIG_SOF_LOG_LEVEL); extern struct tr_ctx comp_tr; static const struct comp_driver *ipc4_get_drv(const void *uuid); -static int ipc4_add_comp_dev(struct comp_dev *dev); void ipc_build_stream_posn(struct sof_ipc_stream_posn *posn, uint32_t type, uint32_t id) @@ -209,6 +208,105 @@ __cold struct comp_dev *comp_new_ipc4(struct ipc4_module_init_instance *module_i return dev; } +#ifdef CONFIG_SOF_USERSPACE_LL +/** + * comp_new_ipc4_user - Create component in user-space IPC thread context. + * + * Called from the user-space IPC thread. Receives a pre-resolved driver + * pointer from the kernel handler. Performs IPC4 message parsing, HOSTBOX + * data read, and calls drv->ops.create() in user-space context. + * + * @param ipc4 IPC4 message request (reconstructed from ipc_user pri/ext words) + * @param drv Component driver resolved by kernel via ipc4_get_comp_drv() + * @return Created component device, or NULL on failure + */ +__cold struct comp_dev *comp_new_ipc4_user(struct ipc4_message_request *ipc4, + const struct comp_driver *drv) +{ + struct ipc4_module_init_instance module_init; + struct comp_ipc_config ipc_config; + struct comp_dev *dev; + uint32_t comp_id; + char *data; + int ret; + + assert_can_be_cold(); + + ret = memcpy_s(&module_init, sizeof(module_init), ipc4, sizeof(*ipc4)); + if (ret < 0) + return NULL; + + comp_id = IPC4_COMP_ID(module_init.primary.r.module_id, + module_init.primary.r.instance_id); + + if (ipc4_get_comp_dev(comp_id)) { + tr_err(&ipc_tr, "comp 0x%x exists", comp_id); + return NULL; + } + + if (module_init.extension.r.core_id >= CONFIG_CORE_COUNT) { + tr_err(&ipc_tr, "ipc: comp->core = %u", + (uint32_t)module_init.extension.r.core_id); + return NULL; + } + + memset(&ipc_config, 0, sizeof(ipc_config)); + ipc_config.id = comp_id; + ipc_config.pipeline_id = module_init.extension.r.ppl_instance_id; + ipc_config.core = module_init.extension.r.core_id; + ipc_config.ipc_config_size = + module_init.extension.r.param_block_size * sizeof(uint32_t); + ipc_config.ipc_extended_init = module_init.extension.r.extended_init; + if (ipc_config.ipc_config_size > MAILBOX_HOSTBOX_SIZE) { + tr_err(&ipc_tr, + "IPC payload size %u too big for the message window", + ipc_config.ipc_config_size); + return NULL; + } +#ifdef CONFIG_DCACHE_LINE_SIZE + if (!IS_ENABLED(CONFIG_LIBRARY)) + sys_cache_data_invd_range( + (__sparse_force void __sparse_cache *)MAILBOX_HOSTBOX_BASE, + ALIGN_UP(ipc_config.ipc_config_size, + CONFIG_DCACHE_LINE_SIZE)); +#endif + data = ipc4_get_comp_new_data(); + +#if CONFIG_ZEPHYR_DP_SCHEDULER + if (module_init.extension.r.proc_domain) + ipc_config.proc_domain = COMP_PROCESSING_DOMAIN_DP; + else + ipc_config.proc_domain = COMP_PROCESSING_DOMAIN_LL; +#else + if (module_init.extension.r.proc_domain) { + tr_err(&ipc_tr, + "ipc: DP scheduling is disabled, cannot create comp 0x%x", + comp_id); + return NULL; + } + ipc_config.proc_domain = COMP_PROCESSING_DOMAIN_LL; +#endif + + if (drv->type == SOF_COMP_MODULE_ADAPTER) { + const struct ipc_config_process spec = { + .data = (const unsigned char *)data, + .size = ipc_config.ipc_config_size, + }; + + dev = drv->ops.create(drv, &ipc_config, (const void *)&spec); + } else { + dev = drv->ops.create(drv, &ipc_config, (const void *)data); + } + if (!dev) + return NULL; + + list_init(&dev->bsource_list); + list_init(&dev->bsink_list); + + return dev; +} +#endif /* CONFIG_SOF_USERSPACE_LL */ + /* Called from ipc4_set_pipeline_state(), so cannot be cold */ struct ipc_comp_dev *ipc_get_comp_by_ppl_id(struct ipc *ipc, uint16_t type, uint32_t ppl_id, @@ -1329,7 +1427,7 @@ struct comp_dev *ipc4_get_comp_dev(uint32_t comp_id) } EXPORT_SYMBOL(ipc4_get_comp_dev); -__cold static int ipc4_add_comp_dev(struct comp_dev *dev) +__cold int ipc4_add_comp_dev(struct comp_dev *dev) { struct ipc *ipc = ipc_get(); struct ipc_comp_dev *icd; diff --git a/zephyr/lib/userspace_helper.c b/zephyr/lib/userspace_helper.c index ce4368adc077..2abc2fa0e2f2 100644 --- a/zephyr/lib/userspace_helper.c +++ b/zephyr/lib/userspace_helper.c @@ -110,6 +110,43 @@ int user_access_to_mailbox(struct k_mem_domain *domain, k_tid_t thread_id) if (ret < 0) return ret; +#if defined(CONFIG_SOF_USERSPACE_LL) && defined(CONFIG_IPC_MAJOR_4) + /* HOSTBOX partitions for IPC4 module init parameter block reads. + * comp_new_ipc4() accesses MAILBOX_HOSTBOX_BASE directly to get + * the module configuration data sent by the host. + */ + { + struct k_mem_partition hostbox_partition; + + /* Uncached HOSTBOX partition */ + hostbox_partition.start = + (uintptr_t)sys_cache_uncached_ptr_get( + (void __sparse_cache *)MAILBOX_HOSTBOX_BASE); + hostbox_partition.size = ALIGN_UP(MAILBOX_HOSTBOX_SIZE, + CONFIG_MMU_PAGE_SIZE); + hostbox_partition.attr = K_MEM_PARTITION_P_RO_U_RO; + + ret = k_mem_domain_add_partition(domain, &hostbox_partition); + if (ret < 0) + return ret; + + /* Cached HOSTBOX partition for cache invalidation path. + * sys_cache_data_invd_range() syscall verification requires + * write access to the region, so use RW instead of RO. + */ + hostbox_partition.start = + (uintptr_t)sys_cache_cached_ptr_get( + (void *)MAILBOX_HOSTBOX_BASE); + hostbox_partition.size = ALIGN_UP(MAILBOX_HOSTBOX_SIZE, + CONFIG_MMU_PAGE_SIZE); + hostbox_partition.attr = K_MEM_PARTITION_P_RW_U_RW; + + ret = k_mem_domain_add_partition(domain, &hostbox_partition); + if (ret < 0) + return ret; + } +#endif /* CONFIG_IPC_MAJOR_4 */ + #ifndef CONFIG_IPC_MAJOR_4 /* * Next mailbox_stream (not available in IPC4). Stream access is cached, From 2014e4963245026a063669dcdbc90db9086a042b Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 16 Apr 2026 15:05:00 +0300 Subject: [PATCH 71/94] WIP: ipc: ipc4: route GLB_SET_PIPELINE_STATE to user-space handler Move handling of SOF_IPC4_GLB_SET_PIPELINE_STATE to user-space IPC thread when SOF is built with CONFIG_SOF_USERSPACE_LL. The IPC compound message helpers in IPC common.h are turned into system calls, allowing the user-space IPC thread to synchronize with kernel IPC thread that still does the low-level IPC messaging. Signed-off-by: Kai Vehmanen --- src/include/ipc4/handler.h | 40 ++++++++++++++++++------ src/include/sof/ipc/common.h | 19 ++++++++---- src/ipc/ipc-common.c | 4 +++ src/ipc/ipc4/handler-kernel.c | 58 ++++++++++++++++++++++++++++++++--- src/ipc/ipc4/handler-user.c | 12 ++++++-- zephyr/CMakeLists.txt | 1 + 6 files changed, 112 insertions(+), 22 deletions(-) diff --git a/src/include/ipc4/handler.h b/src/include/ipc4/handler.h index fa8259d02e8b..dcde958a2442 100644 --- a/src/include/ipc4/handler.h +++ b/src/include/ipc4/handler.h @@ -35,30 +35,50 @@ int ipc4_process_module_config(struct ipc4_message_request *ipc4, int ipc4_user_process_glb_message(struct ipc4_message_request *ipc4, struct ipc_msg *reply); /** - * \brief Increment the IPC compound message pre-start counter. + * \brief Process SET_PIPELINE_STATE IPC4 message (prepare + trigger phases). + * @param[in] ipc4 IPC4 message request. + * @return 0 on success, IPC4 error code otherwise. + */ +int ipc4_set_pipeline_state(struct ipc4_message_request *ipc4); + +/** + * \brief Complete the IPC compound message. * @param[in] msg_id IPC message ID. + * @param[in] error Error code of the IPC command. */ -void ipc_compound_pre_start(int msg_id); +void ipc_compound_msg_done(uint32_t msg_id, int error); +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_USERSPACE_LL) /** - * \brief Decrement the IPC compound message pre-start counter on return value status. + * \brief Increment the IPC compound message pre-start counter. * @param[in] msg_id IPC message ID. - * @param[in] ret Return value of the IPC command. - * @param[in] delayed True if the reply is delayed. */ -void ipc_compound_post_start(uint32_t msg_id, int ret, bool delayed); +__syscall void ipc_compound_pre_start(int msg_id); /** - * \brief Complete the IPC compound message. + * \brief Decrement the IPC compound message pre-start counter on return value status. * @param[in] msg_id IPC message ID. - * @param[in] error Error code of the IPC command. + * @param[in] ret Return value of the IPC command. + * @param[in] delayed True if the reply is delayed. */ -void ipc_compound_msg_done(uint32_t msg_id, int error); +__syscall void ipc_compound_post_start(uint32_t msg_id, int ret, bool delayed); /** * \brief Wait for the IPC compound message to complete. * @return 0 on success, error code otherwise on timeout. */ -int ipc_wait_for_compound_msg(void); +__syscall int ipc_wait_for_compound_msg(void); +#else +void z_impl_ipc_compound_pre_start(int msg_id); +#define ipc_compound_pre_start z_impl_ipc_compound_pre_start +void z_impl_ipc_compound_post_start(uint32_t msg_id, int ret, bool delayed); +#define ipc_compound_post_start z_impl_ipc_compound_post_start +int z_impl_ipc_wait_for_compound_msg(void); +#define ipc_wait_for_compound_msg z_impl_ipc_wait_for_compound_msg +#endif + +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_USERSPACE_LL) +#include +#endif #endif /* __SOF_IPC4_HANDLER_H__ */ diff --git a/src/include/sof/ipc/common.h b/src/include/sof/ipc/common.h index 0218b72519a0..8322da545aca 100644 --- a/src/include/sof/ipc/common.h +++ b/src/include/sof/ipc/common.h @@ -223,6 +223,12 @@ int ipc4_user_process_module_message(struct ipc4_message_request *ipc4, struct i */ int ipc4_user_process_glb_message(struct ipc4_message_request *ipc4, struct ipc_msg *reply); +/* + * When CONFIG_SOF_USERSPACE_LL is enabled, compound message functions are + * declared as syscalls in ipc4/handler.h — do not re-declare here with + * external linkage as that conflicts with the static inline syscall wrappers. + */ +#if !(defined(__ZEPHYR__) && defined(CONFIG_SOF_USERSPACE_LL)) /** * \brief Increment the IPC compound message pre-start counter. * @param[in] msg_id IPC message ID. @@ -237,6 +243,13 @@ void ipc_compound_pre_start(int msg_id); */ void ipc_compound_post_start(uint32_t msg_id, int ret, bool delayed); +/** + * \brief Wait for the IPC compound message to complete. + * @return 0 on success, error code otherwise on timeout. + */ +int ipc_wait_for_compound_msg(void); +#endif /* !CONFIG_SOF_USERSPACE_LL */ + /** * \brief Complete the IPC compound message. * @param[in] msg_id IPC message ID. @@ -244,12 +257,6 @@ void ipc_compound_post_start(uint32_t msg_id, int ret, bool delayed); */ void ipc_compound_msg_done(uint32_t msg_id, int error); -/** - * \brief Wait for the IPC compound message to complete. - * @return 0 on success, error code otherwise on timeout. - */ -int ipc_wait_for_compound_msg(void); - /** * \brief create a IPC boot complete message. * @param[in] header header. diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index 3f8b0cdbc482..5524292a766f 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -526,6 +526,10 @@ static void ipc_user_thread_fn(void *p1, void *p2, void *p3) pipe->primary.r.instance_id); break; } + case SOF_IPC4_GLB_SET_PIPELINE_STATE: + ipc_user->result = + ipc4_set_pipeline_state(&msg); + break; default: LOG_ERR("IPC user: unsupported glb cmd type %d", msg.primary.r.type); diff --git a/src/ipc/ipc4/handler-kernel.c b/src/ipc/ipc4/handler-kernel.c index b600e946ef0e..f58bf3787229 100644 --- a/src/ipc/ipc4/handler-kernel.c +++ b/src/ipc/ipc4/handler-kernel.c @@ -133,7 +133,7 @@ __cold static bool is_any_ppl_active(void) return false; } -void ipc_compound_pre_start(int msg_id) +void z_impl_ipc_compound_pre_start(int msg_id) { /* ipc thread will wait for all scheduled tasks to be complete * Use a reference count to check status of these tasks. @@ -141,7 +141,23 @@ void ipc_compound_pre_start(int msg_id) atomic_add(&msg_data.delayed_reply, 1); } -void ipc_compound_post_start(uint32_t msg_id, int ret, bool delayed) +#ifdef CONFIG_USERSPACE +/** + * \brief Userspace verification wrapper for ipc_compound_pre_start(). + * + * Forwards the call to z_impl_ipc_compound_pre_start(). No pointer + * validation is needed as only primitive types are passed. + * + * @param[in] msg_id IPC message ID. + */ +void z_vrfy_ipc_compound_pre_start(int msg_id) +{ + z_impl_ipc_compound_pre_start(msg_id); +} +#include +#endif + +void z_impl_ipc_compound_post_start(uint32_t msg_id, int ret, bool delayed) { if (ret) { ipc_cmd_err(&ipc_tr, "failed to process msg %d status %d", msg_id, ret); @@ -154,6 +170,24 @@ void ipc_compound_post_start(uint32_t msg_id, int ret, bool delayed) atomic_sub(&msg_data.delayed_reply, 1); } +#ifdef CONFIG_USERSPACE +/** + * \brief Userspace verification wrapper for ipc_compound_post_start(). + * + * Forwards the call to z_impl_ipc_compound_post_start(). No pointer + * validation is needed as only primitive types are passed. + * + * @param[in] msg_id IPC message ID. + * @param[in] ret Return value of the IPC command. + * @param[in] delayed True if the reply is delayed. + */ +void z_vrfy_ipc_compound_post_start(uint32_t msg_id, int ret, bool delayed) +{ + z_impl_ipc_compound_post_start(msg_id, ret, delayed); +} +#include +#endif + void ipc_compound_msg_done(uint32_t msg_id, int error) { if (!atomic_read(&msg_data.delayed_reply)) { @@ -175,13 +209,13 @@ void ipc_compound_msg_done(uint32_t msg_id, int error) * be always IPC4_FAILURE. Therefore the compound messages handling is simplified. The pipeline * triggers will require an explicit scheduler call to get the components to desired state. */ -int ipc_wait_for_compound_msg(void) +int z_impl_ipc_wait_for_compound_msg(void) { atomic_set(&msg_data.delayed_reply, 0); return IPC4_SUCCESS; } #else -int ipc_wait_for_compound_msg(void) +int z_impl_ipc_wait_for_compound_msg(void) { int try_count = 30; @@ -197,6 +231,22 @@ int ipc_wait_for_compound_msg(void) return IPC4_SUCCESS; } + +#ifdef CONFIG_USERSPACE +/** + * \brief Userspace verification wrapper for ipc_wait_for_compound_msg(). + * + * Forwards the call to z_impl_ipc_wait_for_compound_msg(). No pointer + * validation is needed as no pointers are passed. + * + * @return IPC4_SUCCESS on success, IPC4_FAILURE on timeout. + */ +int z_vrfy_ipc_wait_for_compound_msg(void) +{ + return z_impl_ipc_wait_for_compound_msg(); +} +#include +#endif #endif #if CONFIG_LIBRARY_MANAGER diff --git a/src/ipc/ipc4/handler-user.c b/src/ipc/ipc4/handler-user.c index 1448f2db28cb..728de2715ae2 100644 --- a/src/ipc/ipc4/handler-user.c +++ b/src/ipc/ipc4/handler-user.c @@ -423,8 +423,12 @@ __cold const struct ipc4_pipeline_set_state_data *ipc4_get_pipeline_data_wrapper return ipc4_get_pipeline_data(); } -/* Entry point for ipc4_pipeline_trigger(), therefore cannot be cold */ -static int ipc4_set_pipeline_state(struct ipc4_message_request *ipc4) +/** + * \brief Process SET_PIPELINE_STATE IPC4 message (prepare + trigger phases). + * @param[in] ipc4 IPC4 message request. + * @return 0 on success, IPC4 error code otherwise. + */ +int ipc4_set_pipeline_state(struct ipc4_message_request *ipc4) { const struct ipc4_pipeline_set_state_data *ppl_data; struct ipc4_pipeline_set_state state; @@ -700,7 +704,11 @@ int ipc4_user_process_glb_message(struct ipc4_message_request *ipc4, #endif break; case SOF_IPC4_GLB_SET_PIPELINE_STATE: +#ifdef CONFIG_SOF_USERSPACE_LL + ret = ipc_user_forward_cmd(ipc4); +#else ret = ipc4_set_pipeline_state(ipc4); +#endif break; case SOF_IPC4_GLB_GET_PIPELINE_STATE: diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index 7e9e6d81839c..74112c8a293e 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -627,6 +627,7 @@ zephyr_library_sources_ifdef(CONFIG_SHELL zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/audio/module_adapter/module/generic.h) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/fast-get.h) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/ipc/ipc_reply.h) +zephyr_syscall_header(${SOF_SRC_PATH}/include/ipc4/handler.h) zephyr_syscall_header(include/rtos/alloc.h) zephyr_library_sources_ifdef(CONFIG_SOF_USERSPACE_INTERFACE_ALLOC syscall/alloc.c) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/dai-zephyr.h) From 4a3eaa1a54510211455ac89396201a308f9f34dd Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 16 Apr 2026 19:18:23 +0300 Subject: [PATCH 72/94] WIP: ipc: ipc4: route MOD_LARGE_CONFIG_GET/SET user-space Move handling of SOF_IPC4_MOD_LARGE_CONFIG_GET/SET to user-space IPC thread when SOF is built with CONFIG_SOF_USERSPACE_LL. Handling of these IPCs requires calling to module specific code via the module get/set_large_config() method, so this code needs to run in user-space context if audio pipelines are running in user-space. Signed-off-by: Kai Vehmanen --- src/include/ipc4/handler.h | 20 ++++ src/include/sof/ipc/common.h | 4 + src/ipc/ipc-common.c | 13 +++ src/ipc/ipc4/handler-user.c | 213 +++++++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+) diff --git a/src/include/ipc4/handler.h b/src/include/ipc4/handler.h index dcde958a2442..6073e4dc83c0 100644 --- a/src/include/ipc4/handler.h +++ b/src/include/ipc4/handler.h @@ -26,6 +26,26 @@ int ipc4_user_process_module_message(struct ipc4_message_request *ipc4, struct i int ipc4_process_module_config(struct ipc4_message_request *ipc4, bool set, uint32_t *reply_ext); +/** + * @brief Process MOD_LARGE_CONFIG_GET in any execution context. + * @param[in] ipc4 IPC4 message request. + * @param[out] reply_ext Receives extension value for reply. + * @param[out] reply_tx_size Receives TX data size for reply. + * @param[out] reply_tx_data Receives TX data pointer for reply. + * @return IPC4 status code (0 on success). + */ +int ipc4_process_large_config_get(struct ipc4_message_request *ipc4, + uint32_t *reply_ext, + uint32_t *reply_tx_size, + void **reply_tx_data); + +/** + * @brief Process MOD_LARGE_CONFIG_SET in any execution context. + * @param[in] ipc4 IPC4 message request. + * @return IPC4 status code (0 on success). + */ +int ipc4_process_large_config_set(struct ipc4_message_request *ipc4); + /** * \brief Processes IPC4 userspace global message. * @param[in] ipc4 IPC4 message request. diff --git a/src/include/sof/ipc/common.h b/src/include/sof/ipc/common.h index 8322da545aca..d7e76d3955a3 100644 --- a/src/include/sof/ipc/common.h +++ b/src/include/sof/ipc/common.h @@ -67,6 +67,10 @@ struct ipc_user { int result; /** @brief Reply extension word from user thread (e.g. CONFIG_GET result) */ uint32_t reply_ext; + /** @brief Reply TX data size from user thread (e.g. LARGE_CONFIG_GET result) */ + uint32_t reply_tx_size; + /** @brief Reply TX data pointer from user thread (e.g. LARGE_CONFIG_GET result) */ + void *reply_tx_data; struct ipc *ipc; struct k_thread *audio_thread; /** @brief Original kernel driver pointer for restoring dev->drv after create */ diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index 5524292a766f..9af533e51a27 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -503,6 +503,19 @@ static void ipc_user_thread_fn(void *p1, void *p2, void *p3) IPC4_INVALID_RESOURCE_ID; break; } + case SOF_IPC4_MOD_LARGE_CONFIG_GET: + ipc_user->result = + ipc4_process_large_config_get( + &msg, + &ipc_user->reply_ext, + &ipc_user->reply_tx_size, + &ipc_user->reply_tx_data); + break; + case SOF_IPC4_MOD_LARGE_CONFIG_SET: + ipc_user->result = + ipc4_process_large_config_set( + &msg); + break; default: LOG_ERR("IPC user: unsupported module cmd type %d", msg.primary.r.type); diff --git a/src/ipc/ipc4/handler-user.c b/src/ipc/ipc4/handler-user.c index 728de2715ae2..05a3e8d304ac 100644 --- a/src/ipc/ipc4/handler-user.c +++ b/src/ipc/ipc4/handler-user.c @@ -1035,6 +1035,108 @@ __cold static int ipc4_get_vendor_config_module_instance(struct comp_dev *dev, return IPC4_SUCCESS; } +__cold int ipc4_process_large_config_get(struct ipc4_message_request *ipc4, + uint32_t *reply_ext, + uint32_t *reply_tx_size, + void **reply_tx_data) +{ + struct ipc4_module_large_config_reply reply; + struct ipc4_module_large_config config; + char *data = ipc_get()->comp_data; + const struct comp_driver *drv; + struct comp_dev *dev = NULL; + uint32_t data_offset; + + assert_can_be_cold(); + + int ret = memcpy_s(&config, sizeof(config), ipc4, sizeof(*ipc4)); + + if (ret < 0) + return IPC4_FAILURE; + + tr_dbg(&ipc_tr, "%x : %x", + (uint32_t)config.primary.r.module_id, (uint32_t)config.primary.r.instance_id); + + /* get component dev for non-basefw since there is no + * component dev for basefw + */ + if (config.primary.r.module_id) { + uint32_t comp_id; + + comp_id = IPC4_COMP_ID(config.primary.r.module_id, + config.primary.r.instance_id); + dev = ipc4_get_comp_dev(comp_id); + if (!dev) + return IPC4_MOD_INVALID_ID; + + drv = dev->drv; + + /* Multicore disabled for userspace forwarding; + * non-userspace path retains ipc4_process_on_core() + */ + } else { + drv = ipc4_get_comp_drv(config.primary.r.module_id); + } + + if (!drv) + return IPC4_MOD_INVALID_ID; + + if (!drv->ops.get_large_config) + return IPC4_INVALID_REQUEST; + + data_offset = config.extension.r.data_off_size; + + /* check for vendor param first */ + if (config.extension.r.large_param_id == VENDOR_CONFIG_PARAM) { + /* For now only vendor_config case uses payload from hostbox */ + dcache_invalidate_region((__sparse_force void __sparse_cache *)MAILBOX_HOSTBOX_BASE, + config.extension.r.data_off_size); + ret = ipc4_get_vendor_config_module_instance(dev, drv, + config.extension.r.init_block, + config.extension.r.final_block, + &data_offset, + data, + (const char *)MAILBOX_HOSTBOX_BASE); + } else { +#if CONFIG_LIBRARY + data += sizeof(reply); +#endif + ipc4_prepare_for_kcontrol_get(dev, config.extension.r.large_param_id, + data, data_offset); + + ret = drv->ops.get_large_config(dev, config.extension.r.large_param_id, + config.extension.r.init_block, + config.extension.r.final_block, + &data_offset, data); + } + + /* set up ipc4 error code for reply data */ + if (ret < 0) + ret = IPC4_MOD_INVALID_ID; + + /* Copy host config and overwrite */ + reply.extension.dat = config.extension.dat; + reply.extension.r.data_off_size = data_offset; + + /* The last block, no more data */ + if (!config.extension.r.final_block && data_offset < SOF_IPC_MSG_MAX_SIZE) + reply.extension.r.final_block = 1; + + /* Indicate last block if error occurs */ + if (ret) + reply.extension.r.final_block = 1; + + /* no need to allocate memory for reply msg */ + if (ret) + return ret; + + /* Output via parameters instead of msg_reply */ + *reply_ext = reply.extension.dat; + *reply_tx_size = data_offset; + *reply_tx_data = data; + return ret; +} + __cold static int ipc4_get_large_config_module_instance(struct ipc4_message_request *ipc4) { struct ipc4_module_large_config_reply reply; @@ -1213,6 +1315,77 @@ __cold static int ipc4_set_vendor_config_module_instance(struct comp_dev *dev, data_off_size, data); } +__cold int ipc4_process_large_config_set(struct ipc4_message_request *ipc4) +{ + struct ipc4_module_large_config config; + struct comp_dev *dev = NULL; + const struct comp_driver *drv; + + assert_can_be_cold(); + + int ret = memcpy_s(&config, sizeof(config), ipc4, sizeof(*ipc4)); + + if (ret < 0) + return IPC4_FAILURE; + + dcache_invalidate_region((__sparse_force void __sparse_cache *)MAILBOX_HOSTBOX_BASE, + config.extension.r.data_off_size); + tr_dbg(&ipc_tr, "%x : %x", + (uint32_t)config.primary.r.module_id, (uint32_t)config.primary.r.instance_id); + + if (config.primary.r.module_id) { + uint32_t comp_id; + + comp_id = IPC4_COMP_ID(config.primary.r.module_id, config.primary.r.instance_id); + dev = ipc4_get_comp_dev(comp_id); + if (!dev) + return IPC4_MOD_INVALID_ID; + + drv = dev->drv; + + /* Multicore disabled for userspace forwarding; + * non-userspace path retains ipc4_process_on_core() + */ + } else { + drv = ipc4_get_comp_drv(config.primary.r.module_id); + } + + if (!drv) + return IPC4_MOD_INVALID_ID; + + if (!drv->ops.set_large_config) + return IPC4_INVALID_REQUEST; + + /* check for vendor param first */ + if (config.extension.r.large_param_id == VENDOR_CONFIG_PARAM) { + ret = ipc4_set_vendor_config_module_instance(dev, drv, + (uint32_t)config.primary.r.module_id, + (uint32_t)config.primary.r.instance_id, + config.extension.r.init_block, + config.extension.r.final_block, + config.extension.r.data_off_size, + (const char *)MAILBOX_HOSTBOX_BASE); + } else { +#if CONFIG_LIBRARY + struct ipc *ipc = ipc_get(); + const char *data = (const char *)ipc->comp_data + sizeof(config); +#else + const char *data = (const char *)MAILBOX_HOSTBOX_BASE; +#endif + ret = drv->ops.set_large_config(dev, config.extension.r.large_param_id, + config.extension.r.init_block, config.extension.r.final_block, + config.extension.r.data_off_size, data); + if (ret < 0) { + ipc_cmd_err(&ipc_tr, "failed to set large_config_module_instance %x : %x", + (uint32_t)config.primary.r.module_id, + (uint32_t)config.primary.r.instance_id); + ret = IPC4_INVALID_RESOURCE_ID; + } + } + + return ret; +} + __cold static int ipc4_set_large_config_module_instance(struct ipc4_message_request *ipc4) { struct ipc4_module_large_config config; @@ -1409,10 +1582,50 @@ __cold int ipc4_user_process_module_message(struct ipc4_message_request *ipc4, #endif break; case SOF_IPC4_MOD_LARGE_CONFIG_GET: +#ifdef CONFIG_SOF_USERSPACE_LL + { + struct ipc4_module_large_config config; + + memcpy_s(&config, sizeof(config), ipc4, sizeof(*ipc4)); + if (config.primary.r.module_id) { + /* Module case: forward to user thread */ + ret = ipc_user_forward_cmd(ipc4); + if (!ret) { + struct ipc *ipc = ipc_get(); + struct ipc_user *pdata = ipc->ipc_user_pdata; + + msg_reply->extension = pdata->reply_ext; + msg_reply->tx_size = pdata->reply_tx_size; + msg_reply->tx_data = pdata->reply_tx_data; + } + } else { + /* Base firmware (module_id==0): keep in kernel — + * ipc4_get_comp_drv() accesses IMR manifest which + * has no user-space partition. + */ + ret = ipc4_get_large_config_module_instance(ipc4); + } + } +#else ret = ipc4_get_large_config_module_instance(ipc4); +#endif break; case SOF_IPC4_MOD_LARGE_CONFIG_SET: +#ifdef CONFIG_SOF_USERSPACE_LL + { + struct ipc4_module_large_config config; + + memcpy_s(&config, sizeof(config), ipc4, sizeof(*ipc4)); + if (config.primary.r.module_id) { + ret = ipc_user_forward_cmd(ipc4); + } else { + /* Base firmware: keep in kernel (IMR access) */ + ret = ipc4_set_large_config_module_instance(ipc4); + } + } +#else ret = ipc4_set_large_config_module_instance(ipc4); +#endif break; case SOF_IPC4_MOD_BIND: #ifdef CONFIG_SOF_USERSPACE_LL From 9f8fea92d56460e9928d96549181116a7955ae13 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 27 Mar 2026 12:45:25 +0200 Subject: [PATCH 73/94] (---section: IPC user support STOP) From ade94084df88cb48ccfb2265ba26b7e753b6da8f Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 8 May 2026 18:58:45 +0300 Subject: [PATCH 74/94] (---section: IPC notifications START) From be54c607d3dac50dbe3b3a0b765ae9f7a995bdaf Mon Sep 17 00:00:00 2001 From: Jyri Sarha Date: Tue, 21 Apr 2026 00:27:48 +0300 Subject: [PATCH 75/94] ipc: turn ipc_msg_send() into a system call if SOF_USERSPACE_LL=y Make ipc_msg_send() a Zephyr system call so audio processing modules running in user-space LL threads can queue IPC messages (e.g. position updates, notifications) back to the host. The change takes effect only if CONFIG_SOF_USERSPACE_LL=y. Follows the same pattern used for ipc_msg_reply(): a dedicated header with __syscall declaration, z_impl/z_vrfy split, and syscall header registration in CMakeLists.txt. The verifier validates that the msg struct is writable (the implementation touches the list linkage) and that the data buffer, when provided, is readable up to msg->tx_size bytes. Signed-off-by: Jyri Sarha (cherry picked from commit b25c15cd33674444bfc4e808592ed92e9828e809) --- src/include/sof/ipc/ipc_msg_send.h | 32 ++++++++++++++++++++++++++++++ src/include/sof/ipc/msg.h | 8 +------- src/ipc/ipc-common.c | 26 ++++++++++++++++++++++++ zephyr/CMakeLists.txt | 1 + 4 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 src/include/sof/ipc/ipc_msg_send.h diff --git a/src/include/sof/ipc/ipc_msg_send.h b/src/include/sof/ipc/ipc_msg_send.h new file mode 100644 index 000000000000..c5b9b4a4448a --- /dev/null +++ b/src/include/sof/ipc/ipc_msg_send.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. All rights reserved. + */ + +#ifndef __SOF_IPC_IPC_MSG_SEND_H__ +#define __SOF_IPC_IPC_MSG_SEND_H__ + +#include + +struct ipc_msg; + +/** + * \brief Queues an IPC message for transmission. + * @param msg The IPC message. + * @param data The message data. + * @param high_priority True if a high priority message. + */ +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_USERSPACE_LL) +__syscall void ipc_msg_send(struct ipc_msg *msg, void *data, + bool high_priority); +#else +void z_impl_ipc_msg_send(struct ipc_msg *msg, void *data, + bool high_priority); +#define ipc_msg_send z_impl_ipc_msg_send +#endif + +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_USERSPACE_LL) +#include +#endif + +#endif /* __SOF_IPC_IPC_MSG_SEND_H__ */ diff --git a/src/include/sof/ipc/msg.h b/src/include/sof/ipc/msg.h index 160a9be9ec7c..74c2b66fb916 100644 --- a/src/include/sof/ipc/msg.h +++ b/src/include/sof/ipc/msg.h @@ -108,13 +108,7 @@ static inline void ipc_msg_free(struct ipc_msg *msg) */ void ipc_send_queued_msg(void); -/** - * \brief Queues an IPC message for transmission. - * @param msg The IPC message to be freed. - * @param data The message data. - * @param high_priority True if a high priortity message. - */ -void ipc_msg_send(struct ipc_msg *msg, void *data, bool high_priority); +#include /** * \brief Send an IPC message directly for emergency. diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index 9af533e51a27..5ec530deea41 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -39,6 +39,7 @@ #ifdef __ZEPHYR__ #include +#include #endif #ifdef CONFIG_SOF_USERSPACE_LL @@ -242,7 +243,11 @@ __cold void ipc_msg_send_direct(struct ipc_msg *msg, void *data) k_spin_unlock(&ipc->lock, key); } +#ifdef CONFIG_SOF_USERSPACE_LL +void z_impl_ipc_msg_send(struct ipc_msg *msg, void *data, bool high_priority) +#else void ipc_msg_send(struct ipc_msg *msg, void *data, bool high_priority) +#endif { struct ipc *ipc = ipc_get(); k_spinlock_key_t key; @@ -292,6 +297,27 @@ void ipc_msg_send(struct ipc_msg *msg, void *data, bool high_priority) } EXPORT_SYMBOL(ipc_msg_send); +#ifdef CONFIG_SOF_USERSPACE_LL +static inline bool z_vrfy_ipc_msg_send_check_data(struct ipc_msg *msg, void *data) +{ + /* If data != NULL and tx_size > 0, verify the data buffer */ + if (data && msg->tx_size > 0) + K_OOPS(K_SYSCALL_MEMORY_READ(data, msg->tx_size)); + + return true; +} + +void z_vrfy_ipc_msg_send(struct ipc_msg *msg, void *data, bool high_priority) +{ + K_OOPS(K_SYSCALL_MEMORY_WRITE(msg, sizeof(*msg))); + + z_vrfy_ipc_msg_send_check_data(msg, data); + + z_impl_ipc_msg_send(msg, data, high_priority); +} +#include +#endif + #ifdef __ZEPHYR__ static void ipc_work_handler(struct k_work *work) { diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index 74112c8a293e..de4a9562b47a 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -628,6 +628,7 @@ zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/audio/module_adapter/module/ge zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/fast-get.h) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/ipc/ipc_reply.h) zephyr_syscall_header(${SOF_SRC_PATH}/include/ipc4/handler.h) +zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/ipc/ipc_msg_send.h) zephyr_syscall_header(include/rtos/alloc.h) zephyr_library_sources_ifdef(CONFIG_SOF_USERSPACE_INTERFACE_ALLOC syscall/alloc.c) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/dai-zephyr.h) From 2096d46cba049862d8556b19bd9eb3f4370a7bae Mon Sep 17 00:00:00 2001 From: Jyri Sarha Date: Tue, 21 Apr 2026 20:59:28 +0300 Subject: [PATCH 76/94] ipc: make IPC message allocation userspace-safe Add an optional heap parameter to ipc_msg_w_ext_init() and ipc_msg_init() so callers can direct allocations to a specific heap. When the heap argument is NULL the existing rzalloc() path is used; when non-NULL, sof_heap_alloc()/sof_heap_free() are used instead. This allows IPC messages to be allocated from userspace-accessible heaps. For audio module contexts, introduce mod_ipc_msg_w_ext_init() and mod_ipc_msg_init() in generic.h. These use mod_zalloc()/ mod_free() for allocations that are automatically tracked and freed with the module lifecycle. ipc_msg_w_ext_init() is moved from a static inline in msg.h to a non-inline function in ipc-common.c due to the additional sof_heap_alloc dependency. Update all existing callers: - Module context callers (cadence, tdfb, sound_dose) use the new mod_ipc_msg_*() variants. - host-zephyr.c uses hd->heap, pipeline-graph.c uses the heap parameter from pipeline_new(). - Remaining kernel-context callers pass NULL for the default heap. Signed-off-by: Jyri Sarha (cherry picked from commit 11a17d542f135524d4c2d348a5ea60756d1859ff) --- src/audio/google/google_hotword_detect.c | 2 +- src/audio/host-legacy.c | 2 +- src/audio/host-zephyr.c | 2 +- .../module_adapter/module/cadence_ipc4.c | 4 +- src/audio/pipeline/pipeline-graph.c | 2 +- src/audio/sound_dose/sound_dose-ipc4.c | 6 +-- src/audio/tdfb/tdfb_ipc3.c | 2 +- src/audio/tdfb/tdfb_ipc4.c | 4 +- .../sof/audio/module_adapter/module/generic.h | 42 ++++++++++++++++++ src/include/sof/ipc/msg.h | 38 +++++----------- src/ipc/ipc-common.c | 44 +++++++++++++++++++ src/library_manager/lib_notification.c | 2 +- src/samples/audio/detect_test.c | 4 +- src/trace/dma-trace.c | 2 +- 14 files changed, 112 insertions(+), 44 deletions(-) diff --git a/src/audio/google/google_hotword_detect.c b/src/audio/google/google_hotword_detect.c index f7cf44f026fd..2bb7953d06a7 100644 --- a/src/audio/google/google_hotword_detect.c +++ b/src/audio/google/google_hotword_detect.c @@ -112,7 +112,7 @@ static struct comp_dev *ghd_create(const struct comp_driver *drv, cd->event.event_type = SOF_CTRL_EVENT_KD; cd->event.num_elems = 0; - cd->msg = ipc_msg_init(cd->event.rhdr.hdr.cmd, cd->event.rhdr.hdr.size); + cd->msg = ipc_msg_init(NULL, cd->event.rhdr.hdr.cmd, cd->event.rhdr.hdr.size); if (!cd->msg) { comp_err(dev, "ipc_msg_init failed"); goto cd_fail; diff --git a/src/audio/host-legacy.c b/src/audio/host-legacy.c index 3d62e271f518..4523ab751276 100644 --- a/src/audio/host-legacy.c +++ b/src/audio/host-legacy.c @@ -553,7 +553,7 @@ int host_common_new(struct host_data *hd, struct comp_dev *dev, ipc_build_stream_posn(&hd->posn, SOF_IPC_STREAM_POSITION, config_id); - hd->msg = ipc_msg_init(hd->posn.rhdr.hdr.cmd, hd->posn.rhdr.hdr.size); + hd->msg = ipc_msg_init(NULL, hd->posn.rhdr.hdr.cmd, hd->posn.rhdr.hdr.size); if (!hd->msg) { comp_err(dev, "ipc_msg_init failed"); dma_put(hd->dma); diff --git a/src/audio/host-zephyr.c b/src/audio/host-zephyr.c index aefd4abb71c3..5ba944f772e4 100644 --- a/src/audio/host-zephyr.c +++ b/src/audio/host-zephyr.c @@ -728,7 +728,7 @@ __cold int host_common_new(struct host_data *hd, struct comp_dev *dev, ipc_build_stream_posn(&hd->posn, SOF_IPC_STREAM_POSITION, config_id); #if CONFIG_HOST_DMA_IPC_POSITION_UPDATES - hd->msg = ipc_msg_init(hd->posn.rhdr.hdr.cmd, sizeof(hd->posn)); + hd->msg = ipc_msg_init(hd->heap, hd->posn.rhdr.hdr.cmd, sizeof(hd->posn)); if (!hd->msg) { comp_err(dev, "ipc_msg_init failed"); sof_dma_put(hd->dma); diff --git a/src/audio/module_adapter/module/cadence_ipc4.c b/src/audio/module_adapter/module/cadence_ipc4.c index 9621d8ab0f1d..f1002a3d2e7b 100644 --- a/src/audio/module_adapter/module/cadence_ipc4.c +++ b/src/audio/module_adapter/module/cadence_ipc4.c @@ -484,8 +484,8 @@ static int cadence_codec_process(struct processing_module *mod, struct sof_sourc primary->r.type = SOF_IPC4_GLB_NOTIFICATION; primary->r.rsp = SOF_IPC4_MESSAGE_DIR_MSG_REQUEST; primary->r.msg_tgt = SOF_IPC4_MESSAGE_TARGET_FW_GEN_MSG; - msg = ipc_msg_w_ext_init(msg_proto.header, msg_proto.extension, - sizeof(*msg_module_data)); + msg = mod_ipc_msg_w_ext_init(mod, msg_proto.header, msg_proto.extension, + sizeof(*msg_module_data)); if (msg) { msg_module_data = (struct sof_ipc4_notify_module_data *)msg->tx_data; msg_module_data->instance_id = IPC4_INST_ID(ipc_config->id); diff --git a/src/audio/pipeline/pipeline-graph.c b/src/audio/pipeline/pipeline-graph.c index 82436815f563..8c2802abad6d 100644 --- a/src/audio/pipeline/pipeline-graph.c +++ b/src/audio/pipeline/pipeline-graph.c @@ -156,7 +156,7 @@ struct pipeline *pipeline_new(struct k_heap *heap, uint32_t pipeline_id, uint32_ ipc_build_stream_posn(&posn, SOF_IPC_STREAM_TRIG_XRUN, p->comp_id); if (posn.rhdr.hdr.size) { - p->msg = ipc_msg_init(posn.rhdr.hdr.cmd, posn.rhdr.hdr.size); + p->msg = ipc_msg_init(heap, posn.rhdr.hdr.cmd, posn.rhdr.hdr.size); if (!p->msg) { pipe_err(p, "ipc_msg_init failed"); goto free; diff --git a/src/audio/sound_dose/sound_dose-ipc4.c b/src/audio/sound_dose/sound_dose-ipc4.c index fd3a8151e984..3c3f537dc338 100644 --- a/src/audio/sound_dose/sound_dose-ipc4.c +++ b/src/audio/sound_dose/sound_dose-ipc4.c @@ -32,9 +32,9 @@ static struct ipc_msg *sound_dose_notification_init(struct processing_module *mo primary->r.type = SOF_IPC4_GLB_NOTIFICATION; primary->r.rsp = SOF_IPC4_MESSAGE_DIR_MSG_REQUEST; primary->r.msg_tgt = SOF_IPC4_MESSAGE_TARGET_FW_GEN_MSG; - msg = ipc_msg_w_ext_init(msg_proto.header, msg_proto.extension, - sizeof(struct sof_ipc4_notify_module_data) + - sizeof(struct sof_ipc4_control_msg_payload)); + msg = mod_ipc_msg_w_ext_init(mod, msg_proto.header, msg_proto.extension, + sizeof(struct sof_ipc4_notify_module_data) + + sizeof(struct sof_ipc4_control_msg_payload)); if (!msg) return NULL; diff --git a/src/audio/tdfb/tdfb_ipc3.c b/src/audio/tdfb/tdfb_ipc3.c index 544acbe9a41f..cfcfbfd10eda 100644 --- a/src/audio/tdfb/tdfb_ipc3.c +++ b/src/audio/tdfb/tdfb_ipc3.c @@ -36,7 +36,7 @@ static int init_get_ctl_ipc(struct processing_module *mod) cd->ctrl_data->rhdr.hdr.cmd = SOF_IPC_GLB_COMP_MSG | SOF_IPC_COMP_GET_VALUE | comp_id; cd->ctrl_data->rhdr.hdr.size = TDFB_GET_CTRL_DATA_SIZE; - cd->msg = ipc_msg_init(cd->ctrl_data->rhdr.hdr.cmd, cd->ctrl_data->rhdr.hdr.size); + cd->msg = mod_ipc_msg_init(mod, cd->ctrl_data->rhdr.hdr.cmd, cd->ctrl_data->rhdr.hdr.size); cd->ctrl_data->comp_id = comp_id; cd->ctrl_data->type = SOF_CTRL_TYPE_VALUE_CHAN_GET; diff --git a/src/audio/tdfb/tdfb_ipc4.c b/src/audio/tdfb/tdfb_ipc4.c index 2f7e7e865214..a7b4e34977c7 100644 --- a/src/audio/tdfb/tdfb_ipc4.c +++ b/src/audio/tdfb/tdfb_ipc4.c @@ -48,8 +48,8 @@ static struct ipc_msg *tdfb_notification_init(struct processing_module *mod, primary->r.type = SOF_IPC4_GLB_NOTIFICATION; primary->r.rsp = SOF_IPC4_MESSAGE_DIR_MSG_REQUEST; primary->r.msg_tgt = SOF_IPC4_MESSAGE_TARGET_FW_GEN_MSG; - msg = ipc_msg_w_ext_init(msg_proto.header, msg_proto.extension, - sizeof(struct sof_ipc4_notify_module_data) + + msg = mod_ipc_msg_w_ext_init(mod, msg_proto.header, msg_proto.extension, + sizeof(struct sof_ipc4_notify_module_data) + sizeof(struct sof_ipc4_control_msg_payload) + sizeof(struct sof_ipc4_ctrl_value_chan)); if (!msg) diff --git a/src/include/sof/audio/module_adapter/module/generic.h b/src/include/sof/audio/module_adapter/module/generic.h index 94827d86cd9f..937d29f3da90 100644 --- a/src/include/sof/audio/module_adapter/module/generic.h +++ b/src/include/sof/audio/module_adapter/module/generic.h @@ -18,6 +18,7 @@ #include #include #include +#include #include "module_interface.h" /* The __ZEPHYR__ condition is to keep cmocka tests working */ @@ -240,6 +241,47 @@ static inline void *mod_zalloc(struct processing_module *mod, size_t size) return ret; } +/** + * \brief Initialize a new IPC message using the module allocator. + * @param mod Module to allocate from + * @param header Message header metadata + * @param extension Message header extension metadata + * @param size Message data size in bytes. + * @return New IPC message. + */ +static inline struct ipc_msg *mod_ipc_msg_w_ext_init(struct processing_module *mod, + uint32_t header, + uint32_t extension, + uint32_t size) +{ + struct ipc_msg *msg; + + msg = mod_zalloc(mod, sizeof(*msg)); + if (!msg) + return NULL; + + if (size) { + msg->tx_data = mod_zalloc(mod, size); + if (!msg->tx_data) { + mod_free(mod, msg); + return NULL; + } + } + + msg->header = header; + msg->extension = extension; + msg->tx_size = size; + list_init(&msg->list); + + return msg; +} + +static inline struct ipc_msg *mod_ipc_msg_init(struct processing_module *mod, + uint32_t header, uint32_t size) +{ + return mod_ipc_msg_w_ext_init(mod, header, 0, size); +} + #if CONFIG_COMP_BLOB struct comp_data_blob_handler *mod_data_blob_handler_new(struct processing_module *mod); void mod_data_blob_handler_free(struct processing_module *mod, struct comp_data_blob_handler *dbh); diff --git a/src/include/sof/ipc/msg.h b/src/include/sof/ipc/msg.h index 74c2b66fb916..2471b8c2004b 100644 --- a/src/include/sof/ipc/msg.h +++ b/src/include/sof/ipc/msg.h @@ -29,6 +29,7 @@ struct dai_config; struct dma; struct dma_sg_elem_array; +struct k_heap; struct ipc_msg { uint32_t header; /* specific to platform */ @@ -40,46 +41,27 @@ struct ipc_msg { }; /** - * \brief Initialize a new IPC message. + * \brief Initialize a new IPC message using a specific heap. + * @param heap Heap to allocate from (NULL = default kernel heap) * @param header Message header metadata * @param extension Message header extension metadata * @param size Message data size in bytes. * @return New IPC message. */ -static inline struct ipc_msg *ipc_msg_w_ext_init(uint32_t header, uint32_t extension, - uint32_t size) -{ - struct ipc_msg *msg; - - msg = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, sizeof(*msg)); - if (!msg) - return NULL; - - if (size) { - msg->tx_data = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, size); - if (!msg->tx_data) { - rfree(msg); - return NULL; - } - } - - msg->header = header; - msg->extension = extension; - msg->tx_size = size; - list_init(&msg->list); - - return msg; -} +struct ipc_msg *ipc_msg_w_ext_init(struct k_heap *heap, uint32_t header, + uint32_t extension, uint32_t size); /** - * \brief Initialise a new IPC message. + * \brief Initialize a new IPC message using a specific heap. + * @param heap Heap to allocate from (NULL = default kernel heap) * @param header Message header metadata * @param size Message data size in bytes. * @return New IPC message. */ -static inline struct ipc_msg *ipc_msg_init(uint32_t header, uint32_t size) +static inline struct ipc_msg *ipc_msg_init(struct k_heap *heap, + uint32_t header, uint32_t size) { - return ipc_msg_w_ext_init(header, 0, size); + return ipc_msg_w_ext_init(heap, header, 0, size); } /** diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index 5ec530deea41..612e86c7f7b2 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -70,6 +70,50 @@ struct ipc *ipc_get(void) EXPORT_SYMBOL(ipc_get); #endif +struct ipc_msg *ipc_msg_w_ext_init(struct k_heap *heap, uint32_t header, + uint32_t extension, uint32_t size) +{ + struct ipc_msg *msg; + + if (heap) { + msg = sof_heap_alloc(heap, SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, + sizeof(*msg), 0); + if (msg) + memset(msg, 0, sizeof(*msg)); + } else { + msg = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, sizeof(*msg)); + } + if (!msg) + return NULL; + + if (size) { + if (heap) { + msg->tx_data = sof_heap_alloc(heap, + SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, + size, 0); + if (msg->tx_data) + memset(msg->tx_data, 0, size); + } else { + msg->tx_data = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, + size); + } + if (!msg->tx_data) { + if (heap) + sof_heap_free(heap, msg); + else + rfree(msg); + return NULL; + } + } + + msg->header = header; + msg->extension = extension; + msg->tx_size = size; + list_init(&msg->list); + + return msg; +} + int ipc_process_on_core(uint32_t core, bool blocking) { struct ipc *ipc = ipc_get(); diff --git a/src/library_manager/lib_notification.c b/src/library_manager/lib_notification.c index 36aa85835e70..0feab9660c3e 100644 --- a/src/library_manager/lib_notification.c +++ b/src/library_manager/lib_notification.c @@ -53,7 +53,7 @@ struct ipc_msg *lib_notif_msg_init(uint32_t header, uint32_t size) sizeof(*msg_pool_elem)); if (!msg_pool_elem) return NULL; - msg = ipc_msg_init(header, SRAM_OUTBOX_SIZE); + msg = ipc_msg_init(NULL, header, SRAM_OUTBOX_SIZE); if (!msg) { rfree(msg_pool_elem); return NULL; diff --git a/src/samples/audio/detect_test.c b/src/samples/audio/detect_test.c index 1e2b21ee9d9e..305211a07ac8 100644 --- a/src/samples/audio/detect_test.c +++ b/src/samples/audio/detect_test.c @@ -472,7 +472,7 @@ static struct ipc_msg *ipc4_kd_notification_init(uint32_t word_id, notif.extension.r.sv_score = score; - msg = ipc_msg_w_ext_init(notif.primary.dat, + msg = ipc_msg_w_ext_init(NULL, notif.primary.dat, notif.extension.dat, 0); if (!msg) @@ -731,7 +731,7 @@ static struct comp_dev *test_keyword_new(const struct comp_driver *drv, cd->msg = ipc4_kd_notification_init(NOTIFICATION_DEFAULT_WORD_ID, NOTIFICATION_DEFAULT_SCORE); #else - cd->msg = ipc_msg_init(cd->event.rhdr.hdr.cmd, sizeof(cd->event)); + cd->msg = ipc_msg_init(NULL, cd->event.rhdr.hdr.cmd, sizeof(cd->event)); #endif /* CONFIG_IPC_MAJOR_4 */ if (!cd->msg) { diff --git a/src/trace/dma-trace.c b/src/trace/dma-trace.c index 554204ac5c70..fc7e7b2e953c 100644 --- a/src/trace/dma-trace.c +++ b/src/trace/dma-trace.c @@ -160,7 +160,7 @@ int dma_trace_init_early(struct sof *sof) k_spinlock_init(&sof->dmat->lock); ipc_build_trace_posn(&sof->dmat->posn); - sof->dmat->msg = ipc_msg_init(sof->dmat->posn.rhdr.hdr.cmd, + sof->dmat->msg = ipc_msg_init(NULL, sof->dmat->posn.rhdr.hdr.cmd, sof->dmat->posn.rhdr.hdr.size); if (!sof->dmat->msg) { ret = -ENOMEM; From 4e648d9edf7c017c1a5075439a98a0f0cae2fe17 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 29 May 2026 14:02:02 +0300 Subject: [PATCH 77/94] squash! ipc: make IPC message allocation userspace-safe --- src/audio/mfcc/mfcc_ipc4.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/audio/mfcc/mfcc_ipc4.c b/src/audio/mfcc/mfcc_ipc4.c index bb20d85e413b..21850be8a5d6 100644 --- a/src/audio/mfcc/mfcc_ipc4.c +++ b/src/audio/mfcc/mfcc_ipc4.c @@ -49,10 +49,10 @@ int mfcc_ipc_notification_init(struct processing_module *mod) primary->r.type = SOF_IPC4_GLB_NOTIFICATION; primary->r.rsp = SOF_IPC4_MESSAGE_DIR_MSG_REQUEST; primary->r.msg_tgt = SOF_IPC4_MESSAGE_TARGET_FW_GEN_MSG; - cd->msg = ipc_msg_w_ext_init(msg_proto.header, msg_proto.extension, - sizeof(struct sof_ipc4_notify_module_data) + - sizeof(struct sof_ipc4_control_msg_payload) + - sizeof(struct sof_ipc4_ctrl_value_chan)); + cd->msg = mod_ipc_msg_w_ext_init(mod, msg_proto.header, msg_proto.extension, + sizeof(struct sof_ipc4_notify_module_data) + + sizeof(struct sof_ipc4_control_msg_payload) + + sizeof(struct sof_ipc4_ctrl_value_chan)); if (!cd->msg) { comp_err(dev, "Failed to initialize VAD notification"); return -ENOMEM; From 5ebf27343f29cf258e63e22e5ea8ce456ae576db Mon Sep 17 00:00:00 2001 From: Jyri Sarha Date: Fri, 24 Apr 2026 00:14:17 +0300 Subject: [PATCH 78/94] ipc4: notification: make send_resource_notif() a syscall Move user-facing notification functions (send_copier_gateway_xrun_notif_msg, send_gateway_xrun_notif_msg, send_mixer_underrun_notif_msg, send_process_data_error_notif_msg) to a new notification-user.c file so they can run in userspace. The send_resource_notif() function, which depends on the kernel-side notification pool and IPC message infrastructure, is converted to a Zephyr syscall. The implementation is renamed to z_impl_send_resource_notif() and remains in notification.c alongside is_notif_filtered_out() and ipc4_update_notification_mask(). The send_resource_notif() is converted to a system call only if CONFIG_SOF_USERSPACE_LL=y, without it the behaviour is same as befofe. A z_vrfy_send_resource_notif() handler is added to validate the user-provided data buffer and other parameters before forwarding to the kernel implementation. Signed-off-by: Jyri Sarha (cherry picked from commit e385f101a6cdd4965bbe7f88722f9e16206d4723) --- src/include/ipc4/notification.h | 17 +++++++++ src/ipc/ipc4/CMakeLists.txt | 1 + src/ipc/ipc4/notification-user.c | 54 +++++++++++++++++++++++++++ src/ipc/ipc4/notification.c | 64 ++++++++++++++------------------ zephyr/CMakeLists.txt | 2 + 5 files changed, 102 insertions(+), 36 deletions(-) create mode 100644 src/ipc/ipc4/notification-user.c diff --git a/src/include/ipc4/notification.h b/src/include/ipc4/notification.h index 614a926d16ad..bad08a39082c 100644 --- a/src/include/ipc4/notification.h +++ b/src/include/ipc4/notification.h @@ -297,4 +297,21 @@ void send_mixer_underrun_notif_msg(uint32_t resource_id, uint32_t eos_flag, uint uint32_t expected_data_mixed); void ipc4_update_notification_mask(uint32_t ntfy_mask, uint32_t enabled_mask); +#ifdef CONFIG_SOF_USERSPACE_LL + +__syscall bool send_resource_notif(uint32_t resource_id, uint32_t event_type, + uint32_t resource_type, void *data, uint32_t data_size); + +bool z_impl_send_resource_notif(uint32_t resource_id, uint32_t event_type, + uint32_t resource_type, void *data, uint32_t data_size); + +#include + +#else + +bool send_resource_notif(uint32_t resource_id, uint32_t event_type, + uint32_t resource_type, void *data, uint32_t data_size); + +#endif /* CONFIG_SOF_USERSPACE_LL */ + #endif /* __IPC4_NOTIFICATION_H__ */ diff --git a/src/ipc/ipc4/CMakeLists.txt b/src/ipc/ipc4/CMakeLists.txt index 360a4e988650..c9f9fcb5710d 100644 --- a/src/ipc/ipc4/CMakeLists.txt +++ b/src/ipc/ipc4/CMakeLists.txt @@ -7,6 +7,7 @@ add_local_sources(sof helper.c logging.c notification.c + notification-user.c ) # The DAI interface is not implemented in library builds and diff --git a/src/ipc/ipc4/notification-user.c b/src/ipc/ipc4/notification-user.c new file mode 100644 index 000000000000..37a881e32829 --- /dev/null +++ b/src/ipc/ipc4/notification-user.c @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright(c) 2023 Intel Corporation. All rights reserved. + * + * Author: Piotr Makaruk + * Adrian Warecki + */ + +#include +#include +#include + +#include + +static enum sof_ipc4_resource_event_type dir_to_xrun_event(enum sof_ipc_stream_direction dir) +{ + return (dir == SOF_IPC_STREAM_PLAYBACK) ? SOF_IPC4_GATEWAY_UNDERRUN_DETECTED : + SOF_IPC4_GATEWAY_OVERRUN_DETECTED; +} + +bool send_copier_gateway_xrun_notif_msg(uint32_t pipeline_id, enum sof_ipc_stream_direction dir) +{ + return send_resource_notif(pipeline_id, dir_to_xrun_event(dir), SOF_IPC4_PIPELINE, NULL, + 0); +} + +bool send_gateway_xrun_notif_msg(uint32_t resource_id, enum sof_ipc_stream_direction dir) +{ + return send_resource_notif(resource_id, dir_to_xrun_event(dir), SOF_IPC4_GATEWAY, NULL, 0); +} + +void send_mixer_underrun_notif_msg(uint32_t resource_id, uint32_t eos_flag, uint32_t data_mixed, + uint32_t expected_data_mixed) +{ + struct ipc4_mixer_underrun_event_data mixer_underrun_data; + + mixer_underrun_data.eos_flag = eos_flag; + mixer_underrun_data.data_mixed = data_mixed; + mixer_underrun_data.expected_data_mixed = expected_data_mixed; + + send_resource_notif(resource_id, SOF_IPC4_MIXER_UNDERRUN_DETECTED, SOF_IPC4_PIPELINE, + &mixer_underrun_data, sizeof(mixer_underrun_data)); +} +EXPORT_SYMBOL(send_mixer_underrun_notif_msg); + +void send_process_data_error_notif_msg(uint32_t resource_id, uint32_t error_code) +{ + struct ipc4_process_data_error_event_data error_data; + + error_data.error_code = error_code; + + send_resource_notif(resource_id, SOF_IPC4_PROCESS_DATA_ERROR, SOF_IPC4_MODULE_INSTANCE, + &error_data, sizeof(error_data)); +} diff --git a/src/ipc/ipc4/notification.c b/src/ipc/ipc4/notification.c index 9fd566ee1f93..485b875f8468 100644 --- a/src/ipc/ipc4/notification.c +++ b/src/ipc/ipc4/notification.c @@ -12,7 +12,9 @@ #include #include -#include +#ifdef CONFIG_SOF_USERSPACE_LL +#include +#endif static uint32_t notification_mask = 0xFFFFFFFF; @@ -37,8 +39,13 @@ static bool is_notif_filtered_out(uint32_t event_type) return (notification_mask & BIT(notif_idx)) == 0; } -static bool send_resource_notif(uint32_t resource_id, uint32_t event_type, uint32_t resource_type, - void *data, uint32_t data_size) +#ifdef CONFIG_SOF_USERSPACE_LL +bool z_impl_send_resource_notif(uint32_t resource_id, uint32_t event_type, + uint32_t resource_type, void *data, uint32_t data_size) +#else +bool send_resource_notif(uint32_t resource_id, uint32_t event_type, + uint32_t resource_type, void *data, uint32_t data_size) +#endif { struct ipc_msg *msg; @@ -80,49 +87,34 @@ static bool send_resource_notif(uint32_t resource_id, uint32_t event_type, uint3 return true; } -static enum sof_ipc4_resource_event_type dir_to_xrun_event(enum sof_ipc_stream_direction dir) -{ - return (dir == SOF_IPC_STREAM_PLAYBACK) ? SOF_IPC4_GATEWAY_UNDERRUN_DETECTED : - SOF_IPC4_GATEWAY_OVERRUN_DETECTED; -} - void ipc4_update_notification_mask(uint32_t ntfy_mask, uint32_t enabled_mask) { notification_mask &= enabled_mask | (~ntfy_mask); notification_mask |= enabled_mask & ntfy_mask; } -bool send_copier_gateway_xrun_notif_msg(uint32_t pipeline_id, enum sof_ipc_stream_direction dir) +#ifdef CONFIG_SOF_USERSPACE_LL +static inline bool z_vrfy_send_resource_notif(uint32_t resource_id, uint32_t event_type, + uint32_t resource_type, void *data, + uint32_t data_size) { - return send_resource_notif(pipeline_id, dir_to_xrun_event(dir), SOF_IPC4_PIPELINE, NULL, - 0); -} + /* Validate event_type is a known resource event */ + K_OOPS(K_SYSCALL_VERIFY(event_type < SOF_IPC4_INVALID_RESOURCE_EVENT_TYPE)); -bool send_gateway_xrun_notif_msg(uint32_t resource_id, enum sof_ipc_stream_direction dir) -{ - return send_resource_notif(resource_id, dir_to_xrun_event(dir), SOF_IPC4_GATEWAY, NULL, 0); -} + /* Validate resource_type is a known resource type */ + K_OOPS(K_SYSCALL_VERIFY(resource_type < SOF_IPC4_INVALID_RESOURCE_TYPE)); -void send_mixer_underrun_notif_msg(uint32_t resource_id, uint32_t eos_flag, uint32_t data_mixed, - uint32_t expected_data_mixed) -{ - struct ipc4_mixer_underrun_event_data mixer_underrun_data; - - mixer_underrun_data.eos_flag = eos_flag; - mixer_underrun_data.data_mixed = data_mixed; - mixer_underrun_data.expected_data_mixed = expected_data_mixed; + /* data and data_size must be consistent */ + K_OOPS(K_SYSCALL_VERIFY((!data && !data_size) || (data && data_size))); - send_resource_notif(resource_id, SOF_IPC4_MIXER_UNDERRUN_DETECTED, SOF_IPC4_PIPELINE, - &mixer_underrun_data, sizeof(mixer_underrun_data)); -} -EXPORT_SYMBOL(send_mixer_underrun_notif_msg); - -void send_process_data_error_notif_msg(uint32_t resource_id, uint32_t error_code) -{ - struct ipc4_process_data_error_event_data error_data; + /* Payload must fit in the event_data union and the IPC message */ + K_OOPS(K_SYSCALL_VERIFY(data_size <= sizeof(union ipc4_resource_event_data))); + K_OOPS(K_SYSCALL_VERIFY(data_size <= SOF_IPC_MSG_MAX_SIZE)); - error_data.error_code = error_code; + if (data && data_size) + K_OOPS(K_SYSCALL_MEMORY_READ(data, data_size)); - send_resource_notif(resource_id, SOF_IPC4_PROCESS_DATA_ERROR, SOF_IPC4_MODULE_INSTANCE, - &error_data, sizeof(error_data)); + return z_impl_send_resource_notif(resource_id, event_type, resource_type, data, data_size); } +#include +#endif diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index de4a9562b47a..4c4f539619b2 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -634,6 +634,8 @@ zephyr_library_sources_ifdef(CONFIG_SOF_USERSPACE_INTERFACE_ALLOC syscall/alloc. zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/dai-zephyr.h) zephyr_library_sources(syscall/dai.c) +zephyr_syscall_header(${SOF_SRC_PATH}/include/ipc4/notification.h) + zephyr_library_link_libraries(SOF) target_link_libraries(SOF INTERFACE zephyr_interface) From 5739b451c17285c999a4b72d36de32d84c3b0508 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 8 May 2026 18:58:45 +0300 Subject: [PATCH 79/94] (---section: IPC notifications STOP) From 5fc88fdb33968d007a998db313612d3fac74bfcf Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Mon, 25 May 2026 13:48:36 +0300 Subject: [PATCH 80/94] (---section: LL operlay START) From 81c699488dc5fa73029bf522860d473ef78429ef Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 14 Apr 2026 13:16:15 +0300 Subject: [PATCH 81/94] app: overlays: ptl: add ll_usespace_overlay.conf Add an overlay for Intel 'ptl' target that allows to build SOF with all audio pipeline code running in Zephyr user-sapce. Signed-off-by: Kai Vehmanen --- app/overlays/ptl/ll_userspace_overlay.conf | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/overlays/ptl/ll_userspace_overlay.conf diff --git a/app/overlays/ptl/ll_userspace_overlay.conf b/app/overlays/ptl/ll_userspace_overlay.conf new file mode 100644 index 000000000000..6dcaa859a47a --- /dev/null +++ b/app/overlays/ptl/ll_userspace_overlay.conf @@ -0,0 +1,22 @@ +CONFIG_SOF_USERSPACE_LL=y + +# temporary (but for now mandatory) settings +CONFIG_SOF_ZEPHYR_USERSPACE_MODULE_HEAP_SIZE=16384 + +# make the drivers work in user-space +CONFIG_SOF_USERSPACE_INTERFACE_DMA=y +CONFIG_DAI_USERSPACE=y + +# disable features that don't work in user-space (at least yet) +CONFIG_COLD_STORE_EXECUTE_DEBUG=n +CONFIG_CROSS_CORE_STREAM=n +CONFIG_INTEL_ADSP_MIC_PRIVACY=n +CONFIG_XRUN_NOTIFICATIONS_ENABLE=n +CONFIG_SOF_BOOT_TEST_ALLOWED=n +CONFIG_SOF_TELEMETRY_PERFORMANCE_MEASUREMENTS=n +CONFIG_SOF_TELEMETRY_IO_PERFORMANCE_MEASUREMENTS=n + +# disable llext (hits privilege issues in user-space now) +CONFIG_LLEXT_STORAGE_WRITABLE=n +CONFIG_LLEXT_EXPERIMENTAL=n +CONFIG_MODULES=n From 2f9d91cbf8d22c9f17b0c910611245c94638f6dd Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 6 May 2026 17:45:49 +0300 Subject: [PATCH 82/94] app: overlays: ptl: disable cold/dram execution for now A problem exists with DSP panics due to illegal instruction hit in user-space if cold store execution is enabled. Disable it for now until rootcause is found. Signed-off-by: Kai Vehmanen --- app/overlays/ptl/ll_userspace_overlay.conf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/overlays/ptl/ll_userspace_overlay.conf b/app/overlays/ptl/ll_userspace_overlay.conf index 6dcaa859a47a..176459e581e0 100644 --- a/app/overlays/ptl/ll_userspace_overlay.conf +++ b/app/overlays/ptl/ll_userspace_overlay.conf @@ -1,5 +1,9 @@ CONFIG_SOF_USERSPACE_LL=y +# Problem with DSP panics due to illegal instruction hit in user-space if cold +# store execution is enabled. Disable it for now until rootcause is found. +CONFIG_COLD_STORE_EXECUTE_DRAM=n + # temporary (but for now mandatory) settings CONFIG_SOF_ZEPHYR_USERSPACE_MODULE_HEAP_SIZE=16384 From 48c03db13914c09d56aa1fe5877059b7f0174fb2 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 29 Apr 2026 15:09:06 +0300 Subject: [PATCH 83/94] app: overlays: ptl: set CONFIG_SOF_ZEPHYR_LL_USER_HEAP_SIZE Set CONFIG_SOF_ZEPHYR_LL_USER_HEAP_SIZE to 124kiB in the LL user-space overlay. Signed-off-by: Kai Vehmanen --- app/overlays/ptl/ll_userspace_overlay.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/app/overlays/ptl/ll_userspace_overlay.conf b/app/overlays/ptl/ll_userspace_overlay.conf index 176459e581e0..cc3967bccb3c 100644 --- a/app/overlays/ptl/ll_userspace_overlay.conf +++ b/app/overlays/ptl/ll_userspace_overlay.conf @@ -6,6 +6,7 @@ CONFIG_COLD_STORE_EXECUTE_DRAM=n # temporary (but for now mandatory) settings CONFIG_SOF_ZEPHYR_USERSPACE_MODULE_HEAP_SIZE=16384 +CONFIG_SOF_ZEPHYR_LL_USER_HEAP_SIZE=0x1f000 # make the drivers work in user-space CONFIG_SOF_USERSPACE_INTERFACE_DMA=y From f92527b60108b4e7ea44e3e3b9d7342d933ca510 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Mon, 25 May 2026 19:21:42 +0300 Subject: [PATCH 84/94] squash! app: overlays: ptl: set CONFIG_SOF_ZEPHYR_LL_USER_HEAP_SIZE --- app/overlays/ptl/ll_userspace_overlay.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/app/overlays/ptl/ll_userspace_overlay.conf b/app/overlays/ptl/ll_userspace_overlay.conf index cc3967bccb3c..176459e581e0 100644 --- a/app/overlays/ptl/ll_userspace_overlay.conf +++ b/app/overlays/ptl/ll_userspace_overlay.conf @@ -6,7 +6,6 @@ CONFIG_COLD_STORE_EXECUTE_DRAM=n # temporary (but for now mandatory) settings CONFIG_SOF_ZEPHYR_USERSPACE_MODULE_HEAP_SIZE=16384 -CONFIG_SOF_ZEPHYR_LL_USER_HEAP_SIZE=0x1f000 # make the drivers work in user-space CONFIG_SOF_USERSPACE_INTERFACE_DMA=y From 849fb4b5229ca5ffa8cbaabb25777226c0fe5cbe Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Mon, 25 May 2026 13:48:36 +0300 Subject: [PATCH 85/94] (---section: LL operlay STOP) From 8db3636932799b0a0be01827daab36004bb7ea26 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 8 May 2026 14:36:35 +0300 Subject: [PATCH 86/94] ipc: add a mod_alloc_ctx context to IPC context A module context object needs to be allocated and stored in IPC context so that memory allocations can be done in IPC handlers when IPC handling is running in user-space. Signed-off-by: Kai Vehmanen --- src/include/sof/ipc/common.h | 1 + src/ipc/ipc-common.c | 7 +++++++ src/ipc/ipc4/helper.c | 7 ++----- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/include/sof/ipc/common.h b/src/include/sof/ipc/common.h index d7e76d3955a3..fc749f0b33a9 100644 --- a/src/include/sof/ipc/common.h +++ b/src/include/sof/ipc/common.h @@ -109,6 +109,7 @@ struct ipc { #ifdef CONFIG_SOF_USERSPACE_LL struct ipc_user *ipc_user_pdata; + struct mod_alloc_ctx *ll_alloc; #endif #ifdef CONFIG_SOF_TELEMETRY_IO_PERFORMANCE_MEASUREMENTS diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index 612e86c7f7b2..21f9bffd3189 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -713,6 +713,13 @@ __cold int ipc_init(struct sof *sof) ipc = ipc_get(); memset(ipc, 0, sizeof(*ipc)); + ipc->ll_alloc = sof_heap_alloc(heap, SOF_MEM_FLAG_USER, sizeof(*ipc->ll_alloc), 0); + if (!ipc->ll_alloc) { + tr_err(&ipc_tr, "Unable to allocate IPC ll_alloc"); + return -ENOMEM; + } + ipc->ll_alloc->heap = heap; + ipc->ll_alloc->vreg = NULL; #else heap = NULL; diff --git a/src/ipc/ipc4/helper.c b/src/ipc/ipc4/helper.c index e466711e8fe2..bc12f1cd7914 100644 --- a/src/ipc/ipc4/helper.c +++ b/src/ipc/ipc4/helper.c @@ -817,12 +817,9 @@ __cold int ipc_comp_connect(struct ipc *ipc, ipc_pipe_comp_connect *_connect) #endif /* CONFIG_ZEPHYR_DP_SCHEDULER */ #ifdef CONFIG_SOF_USERSPACE_LL - if (!dp_heap) { - /* use system user heap for non-DP module buffers */ - dp_heap = sof_sys_user_heap_get(); - } + if (!alloc) + alloc = ipc->ll_alloc; #endif - bool cross_core_bind = source->ipc_config.core != sink->ipc_config.core; /* If both components are on same core -- process IPC on that core, From 6861669b70cd9fd966cb0ba387e092607a0750e5 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 19 May 2026 22:22:25 +0300 Subject: [PATCH 87/94] zephyr: lib: make vregion_alloc/free system calls Make vregion_alloc(), vregion_alloc_coherent(), vregion_alloc_align(), vregion_alloc_coherent_align(), and vregion_free() available as Zephyr system calls for user-space threads. Add K_SYSCALL_MEMORY_WRITE verification to all syscall handlers to validate the calling thread has access to the vregion's managed memory area. Add CONFIG_SOF_USERSPACE_INTERFACE_VREGION Kconfig option to control the feature. It is auto-selected by SOF_USERSPACE_LL when SOF_VREGIONS is enabled. Signed-off-by: Kai Vehmanen --- src/include/sof/lib/vregion.h | 32 ++++++++++---- zephyr/CMakeLists.txt | 2 + zephyr/Kconfig | 9 ++++ zephyr/lib/vregion.c | 28 +++++++------ zephyr/syscall/vregion.c | 78 +++++++++++++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 zephyr/syscall/vregion.c diff --git a/src/include/sof/lib/vregion.h b/src/include/sof/lib/vregion.h index 612443f5bc48..0f2363107893 100644 --- a/src/include/sof/lib/vregion.h +++ b/src/include/sof/lib/vregion.h @@ -6,6 +6,8 @@ #define __SOF_LIB_VREGION_H__ #include +#include +#include #ifdef __cplusplus extern "C" { @@ -70,12 +72,16 @@ struct vregion *vregion_put(struct vregion *vr); * @param[in] size Size of memory to allocate in bytes. * @return void* Pointer to the allocated memory, or NULL on failure. */ -void *vregion_alloc(struct vregion *vr, enum vregion_mem_type type, size_t size); +__syscall void *vregion_alloc(struct vregion *vr, enum vregion_mem_type type, size_t size); + +void *z_impl_vregion_alloc(struct vregion *vr, enum vregion_mem_type type, size_t size); /** * @brief like vregion_alloc() but allocates coherent memory */ -void *vregion_alloc_coherent(struct vregion *vr, enum vregion_mem_type type, size_t size); +__syscall void *vregion_alloc_coherent(struct vregion *vr, enum vregion_mem_type type, size_t size); + +void *z_impl_vregion_alloc_coherent(struct vregion *vr, enum vregion_mem_type type, size_t size); /** * @brief Allocate aligned memory from the specified virtual region. @@ -88,14 +94,20 @@ void *vregion_alloc_coherent(struct vregion *vr, enum vregion_mem_type type, siz * @param[in] alignment Alignment of memory to allocate in bytes. * @return void* Pointer to the allocated memory, or NULL on failure. */ -void *vregion_alloc_align(struct vregion *vr, enum vregion_mem_type type, - size_t size, size_t alignment); +__syscall void *vregion_alloc_align(struct vregion *vr, enum vregion_mem_type type, + size_t size, size_t alignment); + +void *z_impl_vregion_alloc_align(struct vregion *vr, enum vregion_mem_type type, + size_t size, size_t alignment); /** * @brief like vregion_alloc_align() but allocates coherent memory */ -void *vregion_alloc_coherent_align(struct vregion *vr, enum vregion_mem_type type, - size_t size, size_t alignment); +__syscall void *vregion_alloc_coherent_align(struct vregion *vr, enum vregion_mem_type type, + size_t size, size_t alignment); + +void *z_impl_vregion_alloc_coherent_align(struct vregion *vr, enum vregion_mem_type type, + size_t size, size_t alignment); /** * @brief Free memory allocated from the specified virtual region. @@ -105,7 +117,9 @@ void *vregion_alloc_coherent_align(struct vregion *vr, enum vregion_mem_type typ * @param[in] vr Pointer to the virtual region instance. * @param[in] ptr Pointer to the memory to free. */ -void vregion_free(struct vregion *vr, void *ptr); +__syscall void vregion_free(struct vregion *vr, void *ptr); + +void z_impl_vregion_free(struct vregion *vr, void *ptr); /** * @brief Log virtual region memory usage. @@ -183,4 +197,8 @@ static inline void vregion_mem_info(struct vregion *vr, size_t *size, uintptr_t } #endif +#if CONFIG_SOF_VREGIONS +#include +#endif + #endif /* __SOF_LIB_VREGION_H__ */ diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index 4c4f539619b2..a2eb9b60c9a1 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -631,6 +631,8 @@ zephyr_syscall_header(${SOF_SRC_PATH}/include/ipc4/handler.h) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/ipc/ipc_msg_send.h) zephyr_syscall_header(include/rtos/alloc.h) zephyr_library_sources_ifdef(CONFIG_SOF_USERSPACE_INTERFACE_ALLOC syscall/alloc.c) +zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/vregion.h) +zephyr_library_sources(syscall/vregion.c) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/dai-zephyr.h) zephyr_library_sources(syscall/dai.c) diff --git a/zephyr/Kconfig b/zephyr/Kconfig index 82851bdd0980..208428a095df 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -43,12 +43,21 @@ config SOF_USERSPACE_INTERFACE_MUTEX Enables the sof_umutex API for dynamically-allocated user-space accessible mutexes backed by k_object_alloc. +config SOF_USERSPACE_INTERFACE_VREGION + bool "Enable SOF vregion interface to userspace threads" + depends on USERSPACE + depends on SOF_VREGIONS + help + Allow user-space threads to use vregion_alloc/vregion_free + and their variants as Zephyr system calls. + config SOF_USERSPACE_LL bool "Run Low-Latency pipelines in userspace threads" depends on USERSPACE select SOF_USERSPACE_INTERFACE_ALLOC select SOF_USERSPACE_INTERFACE_DMA select SOF_USERSPACE_INTERFACE_MUTEX + select SOF_USERSPACE_INTERFACE_VREGION if SOF_VREGIONS help Run Low-Latency (LL) pipelines in userspace threads. This adds memory protection between operating system resources and diff --git a/zephyr/lib/vregion.c b/zephyr/lib/vregion.c index 15683925196d..66c10b8bcc42 100644 --- a/zephyr/lib/vregion.c +++ b/zephyr/lib/vregion.c @@ -305,7 +305,7 @@ static void lifetime_free(struct vlinear_heap *heap, void *ptr) * @param vr Pointer to the virtual region instance. * @param ptr Pointer to the memory to free. */ -void vregion_free(struct vregion *vr, void *ptr) +void z_impl_vregion_free(struct vregion *vr, void *ptr) { if (!vr || !ptr) return; @@ -329,7 +329,7 @@ void vregion_free(struct vregion *vr, void *ptr) k_mutex_unlock(&vr->lock); } -EXPORT_SYMBOL(vregion_free); +EXPORT_SYMBOL(z_impl_vregion_free); /** * @brief Allocate memory type from the virtual region. @@ -341,8 +341,8 @@ EXPORT_SYMBOL(vregion_free); * * @return void* Pointer to the allocated memory, or NULL on failure. */ -void *vregion_alloc_align(struct vregion *vr, enum vregion_mem_type type, - size_t size, size_t alignment) +void *z_impl_vregion_alloc_align(struct vregion *vr, enum vregion_mem_type type, + size_t size, size_t alignment) { void *p; @@ -370,7 +370,7 @@ void *vregion_alloc_align(struct vregion *vr, enum vregion_mem_type type, return p; } -EXPORT_SYMBOL(vregion_alloc_align); +EXPORT_SYMBOL(z_impl_vregion_alloc_align); /** * @brief Allocate memory from the virtual region. @@ -379,17 +379,17 @@ EXPORT_SYMBOL(vregion_alloc_align); * @param[in] size Size of the allocation. * @return void* Pointer to the allocated memory, or NULL on failure. */ -void *vregion_alloc(struct vregion *vr, enum vregion_mem_type type, size_t size) +void *z_impl_vregion_alloc(struct vregion *vr, enum vregion_mem_type type, size_t size) { - return vregion_alloc_align(vr, type, size, 0); + return z_impl_vregion_alloc_align(vr, type, size, 0); } -EXPORT_SYMBOL(vregion_alloc); +EXPORT_SYMBOL(z_impl_vregion_alloc); -void *vregion_alloc_coherent(struct vregion *vr, enum vregion_mem_type type, size_t size) +void *z_impl_vregion_alloc_coherent(struct vregion *vr, enum vregion_mem_type type, size_t size) { size = ALIGN_UP(size, CONFIG_DCACHE_LINE_SIZE); - void *p = vregion_alloc_align(vr, type, size, CONFIG_DCACHE_LINE_SIZE); + void *p = z_impl_vregion_alloc_align(vr, type, size, CONFIG_DCACHE_LINE_SIZE); if (!p) return NULL; @@ -398,15 +398,16 @@ void *vregion_alloc_coherent(struct vregion *vr, enum vregion_mem_type type, siz return sys_cache_uncached_ptr_get(p); } +EXPORT_SYMBOL(z_impl_vregion_alloc_coherent); -void *vregion_alloc_coherent_align(struct vregion *vr, enum vregion_mem_type type, - size_t size, size_t alignment) +void *z_impl_vregion_alloc_coherent_align(struct vregion *vr, enum vregion_mem_type type, + size_t size, size_t alignment) { if (alignment < CONFIG_DCACHE_LINE_SIZE) alignment = CONFIG_DCACHE_LINE_SIZE; size = ALIGN_UP(size, CONFIG_DCACHE_LINE_SIZE); - void *p = vregion_alloc_align(vr, type, size, alignment); + void *p = z_impl_vregion_alloc_align(vr, type, size, alignment); if (!p) return NULL; @@ -415,6 +416,7 @@ void *vregion_alloc_coherent_align(struct vregion *vr, enum vregion_mem_type typ return sys_cache_uncached_ptr_get(p); } +EXPORT_SYMBOL(z_impl_vregion_alloc_coherent_align); /** * @brief Log virtual region memory usage. diff --git a/zephyr/syscall/vregion.c b/zephyr/syscall/vregion.c new file mode 100644 index 000000000000..777cf656161d --- /dev/null +++ b/zephyr/syscall/vregion.c @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. + +#include +#include +#include + +static inline void *z_vrfy_vregion_alloc(struct vregion *vr, + enum vregion_mem_type type, size_t size) +{ + size_t vr_size = 0; + uintptr_t vr_start; + + vregion_mem_info(vr, &vr_size, &vr_start); + if (vr_size) + K_OOPS(K_SYSCALL_MEMORY_WRITE((void *)vr_start, vr_size)); + + return z_impl_vregion_alloc(vr, type, size); +} +#include + +static inline void *z_vrfy_vregion_alloc_coherent(struct vregion *vr, + enum vregion_mem_type type, size_t size) +{ + size_t vr_size = 0; + uintptr_t vr_start; + + vregion_mem_info(vr, &vr_size, &vr_start); + if (vr_size) + K_OOPS(K_SYSCALL_MEMORY_WRITE((void *)vr_start, vr_size)); + + return z_impl_vregion_alloc_coherent(vr, type, size); +} +#include + +static inline void *z_vrfy_vregion_alloc_align(struct vregion *vr, + enum vregion_mem_type type, + size_t size, size_t alignment) +{ + size_t vr_size = 0; + uintptr_t vr_start; + + vregion_mem_info(vr, &vr_size, &vr_start); + if (vr_size) + K_OOPS(K_SYSCALL_MEMORY_WRITE((void *)vr_start, vr_size)); + + return z_impl_vregion_alloc_align(vr, type, size, alignment); +} +#include + +static inline void *z_vrfy_vregion_alloc_coherent_align(struct vregion *vr, + enum vregion_mem_type type, + size_t size, size_t alignment) +{ + size_t vr_size = 0; + uintptr_t vr_start; + + vregion_mem_info(vr, &vr_size, &vr_start); + if (vr_size) + K_OOPS(K_SYSCALL_MEMORY_WRITE((void *)vr_start, vr_size)); + + return z_impl_vregion_alloc_coherent_align(vr, type, size, alignment); +} +#include + +static inline void z_vrfy_vregion_free(struct vregion *vr, void *ptr) +{ + size_t vr_size = 0; + uintptr_t vr_start; + + vregion_mem_info(vr, &vr_size, &vr_start); + if (vr_size) + K_OOPS(K_SYSCALL_MEMORY_WRITE((void *)vr_start, vr_size)); + + z_impl_vregion_free(vr, ptr); +} +#include From 213957f0111f24fa6c8473258a009c64a9b9f634 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 19 Feb 2026 16:24:36 +0200 Subject: [PATCH 88/94] (---section test-case START) From 0c52794f38495f94ef4db2646321543b61c92d21 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 22 Apr 2026 15:17:31 +0300 Subject: [PATCH 89/94] zephyr: test: userspace: add pipeline_two_components test Add a new test to userspace_ll set that tests more of the functionality needed to run full audio pipelines in user-space. The test creates a pipeline with two components (IPC4 host and DAI copiers), does pipeline prepare, one copy cycle in prepared state and tears down the pipeline. One user-space thread is created to manage the pipelines. This would be equivalent to user IPC handler thread. Another user-space thread is created for the LL scheduler. Signed-off-by: Kai Vehmanen --- zephyr/test/userspace/test_ll_task.c | 392 ++++++++++++++++++++++++++- 1 file changed, 390 insertions(+), 2 deletions(-) diff --git a/zephyr/test/userspace/test_ll_task.c b/zephyr/test/userspace/test_ll_task.c index 234423defc60..5ac0280205c4 100644 --- a/zephyr/test/userspace/test_ll_task.c +++ b/zephyr/test/userspace/test_ll_task.c @@ -14,9 +14,20 @@ #include #include #include +#include +#include +#include +#include #include #include #include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -24,6 +35,7 @@ #include #include /* offsetof() */ +#include LOG_MODULE_DECLARE(sof_boot_test, LOG_LEVEL_DBG); @@ -36,10 +48,15 @@ K_APPMEM_PARTITION_DEFINE(userspace_ll_part); /* Global variable for test runs counter, accessible from user-space */ K_APP_BMEM(userspace_ll_part) static int test_runs; +/* User-space thread for pipeline_two_components test */ +#define PPL_USER_STACKSIZE 4096 + +static struct k_thread ppl_user_thread; +static K_THREAD_STACK_DEFINE(ppl_user_stack, PPL_USER_STACKSIZE); + static enum task_state task_callback(void *arg) { LOG_INF("entry"); - if (++test_runs > 3) return SOF_TASK_STATE_COMPLETED; @@ -77,7 +94,7 @@ static void ll_task_test(void) LOG_INF("task scheduled and running"); /* Let the task run for a bit */ - k_sleep(K_MSEC(10)); + k_sleep(K_MSEC(100)); /* Cancel the task to stop any scheduled execution */ ret = schedule_task_cancel(task); @@ -87,6 +104,9 @@ static void ll_task_test(void) ret = schedule_task_free(task); zassert_equal(ret, 0); + k_mem_domain_remove_partition(zephyr_ll_mem_domain(), &userspace_ll_part); + zephyr_ll_task_free(task); + LOG_INF("test complete"); } @@ -129,6 +149,374 @@ ZTEST(userspace_ll, pipeline_check) pipeline_check(); } +/* Copier UUID: 9ba00c83-ca12-4a83-943c-1fa2e82f9dda */ +static const uint8_t copier_uuid[16] = { + 0x83, 0x0c, 0xa0, 0x9b, 0x12, 0xca, 0x83, 0x4a, + 0x94, 0x3c, 0x1f, 0xa2, 0xe8, 0x2f, 0x9d, 0xda +}; + +/** + * Find the module_id (manifest entry index) for the copier module + * by iterating the firmware manifest and matching the copier UUID. + */ +static int find_copier_module_id(void) +{ + const struct sof_man_fw_desc *desc = basefw_vendor_get_manifest(); + const struct sof_man_module *mod; + uint32_t i; + + if (!desc) + return -1; + + for (i = 0; i < desc->header.num_module_entries; i++) { + mod = (const struct sof_man_module *)((const char *)desc + + SOF_MAN_MODULE_OFFSET(i)); + if (!memcmp(&mod->uuid, copier_uuid, sizeof(copier_uuid))) + return (int)i; + } + + return -1; +} + +/** + * IPC4 copier module config - used as payload for comp_new_ipc4(). + * Placed at MAILBOX_HOSTBOX_BASE before calling comp_new_ipc4(). + * Layout matches struct ipc4_copier_module_cfg from copier.h. + */ +struct copier_init_data { + struct ipc4_base_module_cfg base; + struct ipc4_audio_format out_fmt; + uint32_t copier_feature_mask; + /* Gateway config (matches struct ipc4_copier_gateway_cfg) */ + union ipc4_connector_node_id node_id; + uint32_t dma_buffer_size; + uint32_t config_length; +} __packed __aligned(4); + +static void fill_audio_format(struct ipc4_audio_format *fmt) +{ + memset(fmt, 0, sizeof(*fmt)); + fmt->sampling_frequency = IPC4_FS_48000HZ; + fmt->depth = IPC4_DEPTH_32BIT; + fmt->ch_cfg = IPC4_CHANNEL_CONFIG_STEREO; + fmt->channels_count = 2; + fmt->valid_bit_depth = 32; + fmt->s_type = IPC4_TYPE_MSB_INTEGER; + fmt->interleaving_style = IPC4_CHANNELS_INTERLEAVED; +} + +/** + * Create a copier component via IPC4. + * + * @param module_id Copier module_id from manifest + * @param instance_id Instance ID for this component + * @param pipeline_id Parent pipeline ID + * @param node_id Gateway node ID (type + virtual DMA index) + */ +static struct comp_dev *create_copier(int module_id, int instance_id, + int pipeline_id, + union ipc4_connector_node_id node_id) +{ + struct ipc4_module_init_instance module_init; + struct copier_init_data cfg; + struct comp_dev *dev; + + /* Prepare copier config payload */ + memset(&cfg, 0, sizeof(cfg)); + fill_audio_format(&cfg.base.audio_fmt); + /* 2 channels * 4 bytes * 48 frames = 384 bytes */ + cfg.base.ibs = 384; + cfg.base.obs = 384; + cfg.base.is_pages = 0; + cfg.base.cpc = 0; + cfg.out_fmt = cfg.base.audio_fmt; + cfg.copier_feature_mask = 0; + cfg.node_id = node_id; + cfg.dma_buffer_size = 768; + cfg.config_length = 0; + + /* Write config data to mailbox hostbox (where comp_new_ipc4 reads it). + * Flush cache so that data is visible in SRAM before comp_new_ipc4() + * invalidates the cache line (in normal IPC flow, host writes via DMA + * directly to SRAM, so the invalidation reads fresh data; here the DSP + * core itself writes, so an explicit flush is needed). + */ + memcpy((void *)MAILBOX_HOSTBOX_BASE, &cfg, sizeof(cfg)); + sys_cache_data_flush_range((void *)MAILBOX_HOSTBOX_BASE, sizeof(cfg)); + + /* Prepare IPC4 module init header */ + memset(&module_init, 0, sizeof(module_init)); + module_init.primary.r.module_id = module_id; + module_init.primary.r.instance_id = instance_id; + module_init.primary.r.type = SOF_IPC4_MOD_INIT_INSTANCE; + module_init.primary.r.msg_tgt = SOF_IPC4_MESSAGE_TARGET_MODULE_MSG; + module_init.primary.r.rsp = SOF_IPC4_MESSAGE_DIR_MSG_REQUEST; + + module_init.extension.r.param_block_size = sizeof(cfg) / sizeof(uint32_t); + module_init.extension.r.ppl_instance_id = pipeline_id; + module_init.extension.r.core_id = 0; + module_init.extension.r.proc_domain = 0; /* LL */ + + dev = comp_new_ipc4(&module_init); + + return dev; +} + +/** + * Context shared between kernel setup and the user-space pipeline thread. + */ +struct ppl_test_ctx { + struct pipeline *p; + struct k_heap *heap; + struct mod_alloc_ctx *alloc; + struct comp_dev *host_comp; + struct comp_dev *dai_comp; + struct comp_buffer *buf; + struct ipc *ipc; + struct ipc_comp_dev *ipc_pipe; +}; + +/** + * Pipeline operations: connect, complete, prepare, copy, verify, and clean up. + * This function is called either directly (kernel mode) or from a user-space + * thread, exercising pipeline_*() calls from the requested context. + */ +static void pipeline_ops(struct ppl_test_ctx *ctx) +{ + struct pipeline *p = ctx->p; + struct comp_dev *host_comp = ctx->host_comp; + struct comp_dev *dai_comp = ctx->dai_comp; + struct comp_buffer *buf = ctx->buf; + int ret; + + LOG_INF("pipeline_ops: user_context=%d", k_is_user_context()); + + /* Step: Connect host -> buffer -> DAI */ + ret = pipeline_connect(host_comp, buf, PPL_CONN_DIR_COMP_TO_BUFFER); + zassert_equal(ret, 0, "connect host to buffer failed"); + + ret = pipeline_connect(dai_comp, buf, PPL_CONN_DIR_BUFFER_TO_COMP); + zassert_equal(ret, 0, "connect buffer to DAI failed"); + + LOG_INF("host -> buffer -> DAI connected"); + + /* Step: Complete the pipeline */ + ret = pipeline_complete(p, host_comp, dai_comp); + zassert_equal(ret, 0, "pipeline complete failed"); + + /* Step: Prepare the pipeline */ + p->sched_comp = host_comp; + + ret = pipeline_prepare(p, host_comp); + zassert_equal(ret, 0, "pipeline prepare failed"); + + ret = pipeline_trigger(p, host_comp, COMP_TRIGGER_PRE_START); + //zassert_equal(ret, 0, "pipeline TRIGGER_START failed"); + + LOG_INF("pipeline complete, status = %d", p->status); + + /* Step: Run copies */ + pipeline_schedule_copy(p, 1000); + + /* Step: let run for 3 msec */ + k_sleep(K_MSEC(3)); + + /* Verify pipeline source and sink assignments */ + zassert_equal(p->source_comp, host_comp, "source comp mismatch"); + zassert_equal(p->sink_comp, dai_comp, "sink comp mismatch"); + + LOG_INF("pipeline_ops done"); +} + +/** + * User-space thread entry point for pipeline_two_components test. + * p1 points to the ppl_test_ctx shared with the kernel launcher. + */ +static void pipeline_user_thread(void *p1, void *p2, void *p3) +{ + struct ppl_test_ctx *ctx = (struct ppl_test_ctx *)p1; + + zassert_true(k_is_user_context(), "expected user context"); + pipeline_ops(ctx); +} + +/** + * Test creating a pipeline with a host copier and a DAI (link) copier, + * connected through a shared buffer. + * + * When run_in_user is true, all pipeline_*() calls are made from a + * separate user-space thread. + */ +static void pipeline_two_components(void) +{ + struct ppl_test_ctx *ctx; + struct k_heap *heap = NULL; + uint32_t pipeline_id = 2; + uint32_t priority = 0; + struct task *task; + uint32_t comp_id; + int copier_module_id; + int host_instance_id = 0; + int dai_instance_id = 1; + int core = 0; + int ret; + + /* Step: Find the copier module_id from the firmware manifest */ + copier_module_id = find_copier_module_id(); + zassert_true(copier_module_id >= 0, "copier module not found in manifest"); + LOG_INF("copier module_id = %d", copier_module_id); + + /* Step: Create pipeline */ + LOG_INF("running test with user memory domain"); + heap = zephyr_ll_user_heap(); + zassert_not_null(heap, "user heap not found"); + + task = zephyr_ll_task_alloc(); + zassert_not_null(task, "task allocation failed"); + + ctx = sof_heap_alloc(heap, SOF_MEM_FLAG_USER, sizeof(*ctx), 0); + ctx->alloc = sof_heap_alloc(heap, SOF_MEM_FLAG_USER, sizeof(struct mod_alloc_ctx), 0); + ctx->alloc->heap = heap; + ctx->alloc->vreg = NULL; + ctx->heap = heap; + ctx->ipc = ipc_get(); + + comp_id = IPC4_COMP_ID(copier_module_id, host_instance_id); + ctx->p = pipeline_new(ctx->heap, pipeline_id, priority, comp_id, NULL); + zassert_not_null(ctx->p, "pipeline creation failed"); + + /* create the LL scheduler thread by initializing one task */ + k_mem_domain_add_partition(zephyr_ll_mem_domain(), &userspace_ll_part); + + test_runs = 0; + ret = schedule_task_init_ll(task, SOF_UUID(test_task_uuid), SOF_SCHEDULE_LL_TIMER, + priority, task_callback, + (void *)&test_runs, core, 0); + zassert_equal(ret, 0); + + LOG_INF("task init done"); + + /* Set pipeline period so components get correct dev->period and dev->frames. + * This mirrors what ipc4_create_pipeline() does in normal IPC flow. + */ + ctx->p->time_domain = SOF_TIME_DOMAIN_TIMER; + ctx->p->period = LL_TIMER_PERIOD_US; + + /* Register pipeline in IPC component list so comp_new_ipc4() can + * find it via ipc_get_comp_by_ppl_id() and set dev->period. + */ + ctx->ipc_pipe = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, + sizeof(struct ipc_comp_dev)); + zassert_not_null(ctx->ipc_pipe, "ipc_comp_dev alloc failed"); + ctx->ipc_pipe->pipeline = ctx->p; + ctx->ipc_pipe->type = COMP_TYPE_PIPELINE; + ctx->ipc_pipe->id = pipeline_id; + ctx->ipc_pipe->core = 0; + list_item_append(&ctx->ipc_pipe->list, &ctx->ipc->comp_list); + + /* Step: Create host copier with HDA host output gateway */ + union ipc4_connector_node_id host_node_id = { .f = { + .dma_type = ipc4_hda_host_output_class, + .v_index = 0 + }}; + ctx->host_comp = create_copier(copier_module_id, host_instance_id, pipeline_id, + host_node_id); + zassert_not_null(ctx->host_comp, "host copier creation failed"); + + /* Assign pipeline to host component */ + ctx->host_comp->pipeline = ctx->p; + ctx->host_comp->ipc_config.type = SOF_COMP_HOST; + + LOG_INF("host copier created, comp_id = 0x%x", ctx->host_comp->ipc_config.id); + + /* Step: Create link copier with HDA link output gateway */ + union ipc4_connector_node_id link_node_id = { .f = { + .dma_type = ipc4_hda_link_output_class, + .v_index = 0 + }}; + ctx->dai_comp = create_copier(copier_module_id, dai_instance_id, pipeline_id, + link_node_id); + zassert_not_null(ctx->dai_comp, "DAI copier creation failed"); + + /* Assign pipeline to DAI component */ + ctx->dai_comp->pipeline = ctx->p; + ctx->dai_comp->ipc_config.type = SOF_COMP_DAI; + + LOG_INF("DAI copier created, comp_id = 0x%x", ctx->dai_comp->ipc_config.id); + + /* Step: Allocate a buffer to connect host -> DAI */ + ctx->buf = buffer_alloc(ctx->alloc, 384, 0, 0, false); + zassert_not_null(ctx->buf, "buffer allocation failed"); + + struct k_thread *task_thread; + + /* Create a user-space thread to execute pipeline operations */ + k_thread_create(&ppl_user_thread, ppl_user_stack, PPL_USER_STACKSIZE, + pipeline_user_thread, ctx, NULL, NULL, + -1, K_USER, K_FOREVER); + + /* Add thread to LL memory domain so it can access pipeline memory */ + k_mem_domain_add_thread(zephyr_ll_mem_domain(), &ppl_user_thread); + + user_grant_dai_access_all(&ppl_user_thread); + user_grant_dma_access_all(&ppl_user_thread); + user_access_to_mailbox(zephyr_ll_mem_domain(), &ppl_user_thread); + zephyr_ll_grant_access(&ppl_user_thread); + + task_thread = scheduler_init_context(task); + zassert_not_null(task_thread); + + k_thread_start(&ppl_user_thread); + + LOG_INF("user thread started, waiting for completion"); + + k_thread_join(&ppl_user_thread, K_FOREVER); + + /* Step: Clean up - reset, disconnect, free buffer, free components, free pipeline */ + /* Reset pipeline to bring components back to COMP_STATE_READY, + * required before ipc_comp_free() which rejects non-READY components. + */ + ret = pipeline_reset(ctx->p, ctx->host_comp); + zassert_equal(ret, 0, "pipeline reset failed"); + + pipeline_disconnect(ctx->host_comp, ctx->buf, PPL_CONN_DIR_COMP_TO_BUFFER); + pipeline_disconnect(ctx->dai_comp, ctx->buf, PPL_CONN_DIR_BUFFER_TO_COMP); + + buffer_free(ctx->buf); + + /* Free components through IPC to properly remove from IPC device list */ + ret = ipc_comp_free(ctx->ipc, ctx->host_comp->ipc_config.id); + zassert_equal(ret, 0, "host comp free failed"); + + ret = ipc_comp_free(ctx->ipc, ctx->dai_comp->ipc_config.id); + zassert_equal(ret, 0, "DAI comp free failed"); + + /* Unregister pipeline from IPC component list */ + list_item_del(&ctx->ipc_pipe->list); + rfree(ctx->ipc_pipe); + + ret = pipeline_free(ctx->p); + zassert_equal(ret, 0, "pipeline free failed"); + + schedule_free(0); + + ret = schedule_task_free(task); + zassert_equal(ret, 0); + + sof_heap_free(heap, ctx->alloc); + sof_heap_free(heap, ctx); + + zephyr_ll_task_free(task); + k_mem_domain_remove_partition(zephyr_ll_mem_domain(), &userspace_ll_part); + + LOG_INF("two component pipeline test complete"); +} + +ZTEST(userspace_ll, pipeline_two_components_user) +{ + pipeline_two_components(); +} + ZTEST_SUITE(userspace_ll, NULL, NULL, NULL, NULL, NULL); /** From 96900155bab49bc61e127fb40d630ce2f8deed18 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 19 Feb 2026 16:25:31 +0200 Subject: [PATCH 90/94] (---section WIP mandatory changes START) From 177ea9b793b71c2b14f68ffc6b50083a607af031 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Tue, 17 Mar 2026 19:47:17 +0200 Subject: [PATCH 91/94] audio: pipeline: enable position reporting for user-space pipelines Place the pipeline position lookup table in the sysuser memory partition and replace k_spinlock with a dynamically allocated k_mutex when CONFIG_SOF_USERSPACE_LL is enabled. Spinlocks disable interrupts which is a privileged operation unavailable from user-mode threads. The mutex pointer is stored in a separate APP_SYSUSER_BSS variable outside the SHARED_DATA struct so Zephyr's kernel object tracking can recognize it for syscall verification. Move pipeline_posn_init() from task_main_start() to primary_core_init() before platform_init(), so the mutex is allocated before ipc_user_init() grants thread access to it. In pipeline_posn_get(), bypass the sof_get() kernel singleton and access the shared structure directly when running in user-space. Grant the ipc_user_init thread access to the pipeline position mutex via new pipeline_posn_grant_access() helper. Signed-off-by: Kai Vehmanen --- src/audio/pipeline/pipeline-graph.c | 58 +++++++++++++++++++++++++++-- src/include/sof/audio/pipeline.h | 8 ++++ src/init/init.c | 6 +++ src/ipc/ipc-common.c | 1 + zephyr/wrapper.c | 3 -- 5 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/audio/pipeline/pipeline-graph.c b/src/audio/pipeline/pipeline-graph.c index 8c2802abad6d..b479477e0815 100644 --- a/src/audio/pipeline/pipeline-graph.c +++ b/src/audio/pipeline/pipeline-graph.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -41,10 +42,20 @@ DECLARE_TR_CTX(pipe_tr, SOF_UUID(pipe_uuid), LOG_LEVEL_INFO); /* lookup table to determine busy/free pipeline metadata objects */ struct pipeline_posn { bool posn_offset[PPL_POSN_OFFSETS]; /**< available offsets */ +#ifndef CONFIG_SOF_USERSPACE_LL struct k_spinlock lock; /**< lock mechanism */ +#endif }; /* the pipeline position lookup table */ -static SHARED_DATA struct pipeline_posn pipeline_posn_shared; +static APP_SYSUSER_BSS SHARED_DATA struct pipeline_posn pipeline_posn_shared; + +#ifdef CONFIG_SOF_USERSPACE_LL +/* Mutex pointer in user-accessible partition so user-space threads + * can read the pointer for syscalls. Kept outside the SHARED_DATA + * struct to avoid kernel object tracking issues. + */ +static APP_SYSUSER_BSS struct k_mutex *pipeline_posn_lock; +#endif /** * \brief Retrieves pipeline position structure. @@ -52,7 +63,12 @@ static SHARED_DATA struct pipeline_posn pipeline_posn_shared; */ static inline struct pipeline_posn *pipeline_posn_get(void) { +#ifdef CONFIG_SOF_USERSPACE_LL + return platform_shared_get(&pipeline_posn_shared, + sizeof(pipeline_posn_shared)); +#else return sof_get()->pipeline_posn; +#endif } /** @@ -65,9 +81,14 @@ static inline int pipeline_posn_offset_get(uint32_t *posn_offset) struct pipeline_posn *pipeline_posn = pipeline_posn_get(); int ret = -EINVAL; uint32_t i; + +#ifdef CONFIG_SOF_USERSPACE_LL + k_mutex_lock(pipeline_posn_lock, K_FOREVER); +#else k_spinlock_key_t key; key = k_spin_lock(&pipeline_posn->lock); +#endif for (i = 0; i < PPL_POSN_OFFSETS; ++i) { if (!pipeline_posn->posn_offset[i]) { @@ -78,8 +99,11 @@ static inline int pipeline_posn_offset_get(uint32_t *posn_offset) } } - +#ifdef CONFIG_SOF_USERSPACE_LL + k_mutex_unlock(pipeline_posn_lock); +#else k_spin_unlock(&pipeline_posn->lock, key); +#endif return ret; } @@ -92,22 +116,43 @@ static inline void pipeline_posn_offset_put(uint32_t posn_offset) { struct pipeline_posn *pipeline_posn = pipeline_posn_get(); int i = posn_offset / sizeof(struct sof_ipc_stream_posn); + +#ifdef CONFIG_SOF_USERSPACE_LL + k_mutex_lock(pipeline_posn_lock, K_FOREVER); + pipeline_posn->posn_offset[i] = false; + k_mutex_unlock(pipeline_posn_lock); +#else k_spinlock_key_t key; key = k_spin_lock(&pipeline_posn->lock); - pipeline_posn->posn_offset[i] = false; - k_spin_unlock(&pipeline_posn->lock, key); +#endif } void pipeline_posn_init(struct sof *sof) { sof->pipeline_posn = platform_shared_get(&pipeline_posn_shared, sizeof(pipeline_posn_shared)); +#ifdef CONFIG_SOF_USERSPACE_LL + pipeline_posn_lock = k_object_alloc(K_OBJ_MUTEX); + if (!pipeline_posn_lock) { + pipe_cl_err("pipeline posn mutex alloc failed"); + k_panic(); + } + k_mutex_init(pipeline_posn_lock); +#else k_spinlock_init(&sof->pipeline_posn->lock); +#endif } +#ifdef CONFIG_SOF_USERSPACE_LL +void pipeline_posn_grant_access(struct k_thread *thread) +{ + k_thread_access_grant(thread, pipeline_posn_lock); +} +#endif + /* create new pipeline - returns pipeline id or negative error */ struct pipeline *pipeline_new(struct k_heap *heap, uint32_t pipeline_id, uint32_t priority, uint32_t comp_id, struct create_pipeline_params *pparams) @@ -138,12 +183,17 @@ struct pipeline *pipeline_new(struct k_heap *heap, uint32_t pipeline_id, uint32_ p->pipeline_id = pipeline_id; p->status = COMP_STATE_INIT; p->trigger.cmd = COMP_TRIGGER_NO_ACTION; + +#ifdef CONFIG_SOF_USERSPACE_LL + LOG_WRN("pipeline trace settings cannot be copied"); +#else ret = memcpy_s(&p->tctx, sizeof(struct tr_ctx), &pipe_tr, sizeof(struct tr_ctx)); if (ret < 0) { pipe_err(p, "failed to copy trace settings"); goto free; } +#endif ret = pipeline_posn_offset_get(&p->posn_offset); if (ret < 0) { diff --git a/src/include/sof/audio/pipeline.h b/src/include/sof/audio/pipeline.h index 065a32063843..f8be1a5a409a 100644 --- a/src/include/sof/audio/pipeline.h +++ b/src/include/sof/audio/pipeline.h @@ -207,6 +207,14 @@ int pipeline_complete(struct pipeline *p, struct comp_dev *source, */ void pipeline_posn_init(struct sof *sof); +#ifdef CONFIG_SOF_USERSPACE_LL +/** + * \brief Grants user-space thread access to pipeline position mutex. + * \param[in] thread Thread to grant access to. + */ +void pipeline_posn_grant_access(struct k_thread *thread); +#endif + /** * \brief Resets the pipeline and free runtime resources. * \param[in] p pipeline. diff --git a/src/init/init.c b/src/init/init.c index 58607df1254a..546b1931db41 100644 --- a/src/init/init.c +++ b/src/init/init.c @@ -32,6 +32,7 @@ #include #include #include +#include #include #if CONFIG_IPC_MAJOR_4 #include @@ -240,6 +241,11 @@ __cold static int primary_core_init(int argc, char *argv[], struct sof *sof) zephyr_ll_user_resources_init(); #endif + /* init pipeline position offsets - must be before platform_init() + * which calls ipc_init() -> ipc_user_init() that needs the posn mutex. + */ + pipeline_posn_init(sof); + /* init the platform */ if (platform_init(sof) < 0) sof_panic(SOF_IPC_PANIC_PLATFORM); diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index 21f9bffd3189..3e8e344d2d28 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -666,6 +666,7 @@ __cold int ipc_user_init(void) user_grant_dma_access_all(&ipc_user_thread); user_access_to_mailbox(zephyr_ll_mem_domain(), &ipc_user_thread); zephyr_ll_grant_access(&ipc_user_thread); + pipeline_posn_grant_access(&ipc_user_thread); k_mem_domain_add_thread(zephyr_ll_mem_domain(), &ipc_user_thread); k_thread_cpu_pin(&ipc_user_thread, PLATFORM_PRIMARY_CORE_ID); diff --git a/zephyr/wrapper.c b/zephyr/wrapper.c index 349b27e44eab..8c732fc9092e 100644 --- a/zephyr/wrapper.c +++ b/zephyr/wrapper.c @@ -177,9 +177,6 @@ int task_main_start(struct sof *sof) /* init default audio components */ sys_comp_init(sof); - /* init pipeline position offsets */ - pipeline_posn_init(sof); - return 0; } From a319bf3677c7d8dcc77881ba499710327bca9fcb Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Mon, 25 May 2026 13:55:59 +0300 Subject: [PATCH 92/94] WIP: schedule: limit user-LL to core0 Multiple temporary limitations to limit user LL scheduler to only work on core 0. This is mostly related to privileged cpu_get_id() that is used in many places to check the id of the core code is running on. This is not available to user-space at the moment, so temporary measures are needed. Note: The following commits for scheduler depend on this, so this commit cannot be moved to the end of the series currently. Signed-off-by: Kai Vehmanen --- zephyr/schedule.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/zephyr/schedule.c b/zephyr/schedule.c index 90d50dd11f0a..885c482c7b52 100644 --- a/zephyr/schedule.c +++ b/zephyr/schedule.c @@ -22,6 +22,11 @@ static APP_SYSUSER_BSS struct schedulers *_schedulers[CONFIG_CORE_COUNT]; */ struct schedulers **arch_schedulers_get(void) { + if (k_is_user_context()) { + printk("FIXME: using core0 scheduler\n"); + return _schedulers; + } + return _schedulers + cpu_get_id(); } EXPORT_SYMBOL(arch_schedulers_get); From 41a069c7e2ba8bc2a8c6ede63e20002fe6b7384b Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 16 Apr 2026 15:12:56 +0300 Subject: [PATCH 93/94] WIP: ipc: expose coldrodata to IPC user thread If SOF is built with CONFIG_SOF_USERSPACE_LL, the IPC user handled will require access to coldrodata sections to initialize audio modules. This logic is not required for LLEXT modules, which have existing code to add access to coldrodata (and other sections). This commit is needed for builds where LLEXT is not used. Signed-off-by: Kai Vehmanen --- src/ipc/ipc-common.c | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index 3e8e344d2d28..83e386f9b65e 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -645,6 +645,40 @@ __cold int ipc_user_init(void) ret = k_mem_domain_add_partition(zephyr_ll_mem_domain(), &ipc_context_part); + /* + * Grant user-space access to .cold (execute) and .coldrodata (read) + * sections in IMR. The prepare path walks component code that may + * reference __cold functions and __cold_rodata data. + */ +#ifdef CONFIG_COLD_STORE_EXECUTE_DRAM + { + extern char __cold_start[], __cold_end[]; + extern char __coldrodata_start[]; + extern char _imr_end[]; + struct k_mem_partition cold_part; + + cold_part.start = (uintptr_t)__cold_start; + cold_part.size = ALIGN_UP((uintptr_t)__cold_end - + (uintptr_t)__cold_start, + CONFIG_MMU_PAGE_SIZE); + cold_part.attr = K_MEM_PARTITION_P_RX_U_RX; + ret = k_mem_domain_add_partition(zephyr_ll_mem_domain(), + &cold_part); + if (ret < 0) + LOG_WRN("cold text partition add failed: %d", ret); + + cold_part.start = (uintptr_t)__coldrodata_start; + cold_part.size = ALIGN_UP((uintptr_t)_imr_end - + (uintptr_t)__coldrodata_start, + CONFIG_MMU_PAGE_SIZE); + cold_part.attr = K_MEM_PARTITION_P_RO_U_RO; + ret = k_mem_domain_add_partition(zephyr_ll_mem_domain(), + &cold_part); + if (ret < 0) + LOG_WRN("cold rodata partition add failed: %d", ret); + } +#endif + k_sem_init(ipc_user->sem, 0, 1); /* Allocate kernel objects for the user-space thread */ From fb93a0f55102ba5856111f909194efd3e9207e5a Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Thu, 26 Feb 2026 17:14:39 +0200 Subject: [PATCH 94/94] HACK: audio: collection of feature limitations to run LL in user This is a set of temporary changes to audio code to remove calls to privileged interfaces that are not mandatory to run simple audio tests. These need proper solutions to be able to run all use-cases in user LL version. Signed-off-by: Kai Vehmanen --- src/audio/pipeline/pipeline-graph.c | 6 +++++- zephyr/include/sof/lib/cpu.h | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/audio/pipeline/pipeline-graph.c b/src/audio/pipeline/pipeline-graph.c index b479477e0815..78e461fd38ee 100644 --- a/src/audio/pipeline/pipeline-graph.c +++ b/src/audio/pipeline/pipeline-graph.c @@ -556,6 +556,10 @@ struct comp_dev *pipeline_get_dai_comp(uint32_t pipeline_id, int dir) */ struct comp_dev *pipeline_get_dai_comp_latency(uint32_t pipeline_id, uint32_t *latency) { +#ifdef CONFIG_SOF_USERSPACE_LL + LOG_WRN("latency cannot be computed in user-space pipelines!"); + *latency = 0; +#else struct ipc_comp_dev *ipc_sink; struct ipc_comp_dev *ipc_source; struct comp_dev *source; @@ -623,7 +627,7 @@ struct comp_dev *pipeline_get_dai_comp_latency(uint32_t pipeline_id, uint32_t *l /* Get a next sink component */ ipc_sink = ipc_get_ppl_sink_comp(ipc, source->pipeline->pipeline_id); } - +#endif return NULL; } EXPORT_SYMBOL(pipeline_get_dai_comp_latency); diff --git a/zephyr/include/sof/lib/cpu.h b/zephyr/include/sof/lib/cpu.h index c23405e85121..533cb29f3602 100644 --- a/zephyr/include/sof/lib/cpu.h +++ b/zephyr/include/sof/lib/cpu.h @@ -55,7 +55,11 @@ static inline bool cpu_is_primary(int id) static inline bool cpu_is_me(int id) { +#ifdef CONFIG_SOF_USERSPACE_LL + return true; +#else return id == cpu_get_id(); +#endif } int cpu_enable_core(int id);