Skip to content

LL userspace: enable cross-core DP pipelines - #11397

Open
kv2019i wants to merge 8 commits into
thesofproject:mainfrom
kv2019i:202611-lluser-cross-core-ipc-idc
Open

kv2019i wants to merge 8 commits into
thesofproject:mainfrom
kv2019i:202611-lluser-cross-core-ipc-idc

Conversation

@kv2019i

@kv2019i kv2019i commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Initial implementation of cross-core pipeline support to full userspace builds. This covers the common use-case of running compressed codec module code in a different core (as DP module).

@kv2019i

kv2019i commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator Author

FYI @ujfalusi @lgirdwood with this, compressed playback can be supported also with full userspace (both LL pipes and the DP codecs running in user-space).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical multicore userspace defects in state handling, locking, access grants, event signaling, and component dispatch can cause failed transitions, panics, unauthorized access, and deadlocks.

5 open issues
What changed in this PR

Adds initial cross-core pipeline support to full userspace builds, allowing DP modules such as compressed codecs to run on a different core from their pipeline.

Changes:

  • Routes component IDC requests through per-core userspace IPC threads.
  • Adds shared request storage, access grants, and userspace scheduler locking.
  • Enables cross-core streams while keeping LL modules on their pipeline’s core.
File Description
zephyr/​include/​rtos/​idc.h Declares userspace cross-core messaging interfaces.
src/​schedule/​zephyr_domain.c Excludes kernel-style scheduler blocking from userspace LL builds.
src/​ipc/​ipc4/​helper.c Allows remote DP modules and adjusts binding and teardown locks.
src/​ipc/​ipc-common.c Implements cross-core request transport, dispatch, and access grants.
src/​include/​sof/​ipc/​common.h Defines per-core cross-core request slots.
src/​include/​sof/​audio/​component_ext.h Allocates remote attribute results in user-accessible memory.
src/​idc/​zephyr_idc.c Routes userspace IDC sends through userspace IPC threads.
src/​idc/​idc.c Adds userspace component dispatch and explicit payload handling.
app/​ll_userspace_overlay.conf Removes the forced disabling of cross-core streams.

Review effort: Balanced

Comment thread src/idc/idc.c Resolved
Comment thread src/ipc/ipc-common.c Resolved
Comment thread src/ipc/ipc-common.c Resolved
Comment thread src/ipc/ipc4/helper.c Resolved
Comment thread src/ipc/ipc4/helper.c Resolved
@intel-sofci

intel-sofci commented Nov 3, 2026 •

Copy link
Copy Markdown

PR 11397: test results

Run date: 2026-11-05 14:25 UTC

Tested commit: 6d2890d20df1608c4b88eed81d5e6fd6a3b5a893

mtl pass rate lnl pass rate ptl pass rate wcl pass rate nvl pass rate

zephyr_ll_locks[] is in a user-accessible partition, so user threads
can read the lock handles for the lock syscalls. user_ll_grant_access()
runs in kernel context, but took the handle to grant from that same
array. A user thread could overwrite an entry and make the kernel grant
it access to an arbitrary kernel object. This is reachable whenever
grants are done while user threads already run, e.g. when a secondary
core and its IPC user thread are brought up, and with cross-core
streams also for the locks of other cores.

Keep a kernel-only copy of the lock handles and use it for the grants.

Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
Pass the message payload to the component IDC message handlers as an
argument, instead of fetching it from the per-core IDC payload area,
which is kernel memory.

Add idc_cmd_user() to let user-space threads (with CONFIG_SOF_USERSPACE_LL)
process component messages, sent from another core, with a payload
in user-accessible memory. Only the messages, which can originate from
the user-space IPC and LL threads, are supported.

LL modules on a core different from their pipeline core aren't
supported with user-space LL, so fail the prepare instead of trying
to set up an LL task for them.

Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
@kv2019i
kv2019i force-pushed the 202611-lluser-cross-core-ipc-idc branch from 72b5b67 to 0c12b46 Compare November 4, 2026 18:00
@kv2019i

kv2019i commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

V2 pushed:

  • addressing code review comments so far

@kv2019i
kv2019i force-pushed the 202611-lluser-cross-core-ipc-idc branch from 0c12b46 to ba5a057 Compare November 5, 2026 09:28
@kv2019i

kv2019i commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

V3 pushed:

  • fixed the testbench build failure (undefined ARG_UNUSED())

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Two critical deadlock paths remain in synchronous cross-core IPC waits, where LL threads hold scheduler mutexes while awaiting IPC handlers that need those locks during BIND/UNBIND or remote DP startup.

6 open issues
1 resolved since last review

Review effort: Balanced

Comment thread src/ipc/ipc-common.c Resolved
Comment thread src/ipc/ipc-common.c Resolved
With CONFIG_SOF_USERSPACE_LL, component operations on a module on
another core (cross-core bind, remote get_attribute, free, params,
prepare, trigger, reset) are called from user-space IPC and LL threads.
They use idc_send_msg(), which relies on kernel objects and data and
faults in user mode.

Add a cross-core request slot per core to struct ipc_user. When called
in user context, idc_send_msg() now copies the message and its payload
to the slot of the target core and wakes the user-space IPC thread of
that core, which processes it with idc_cmd_user() and signals
completion. The IPC4 bind / unbind message, referenced by the bind
payload, usually is on the sender's stack, so it's copied as well.

The user-space IPC thread of a core now has two independent producers,
the kernel IPC handler (IPC_USER_EVENT_CMD) and user threads on other
cores (IPC_USER_EVENT_XCORE). Post the events with k_event_post()
instead of k_event_set(), which would clear a pending request of the
other producer.

An LL thread holds the LL scheduler lock of its core for the whole LL
cycle, also when it calls a remote module, e.g. from a timer-driven
pipeline trigger task or when a failed copy resets the pipeline. Waiting
for the target IPC thread with the lock held could deadlock, if that
thread needs the lock, e.g. for a bind locking both cores, or needs the
LL lock of its own core, whose LL thread may in turn wait for a request
to this core. Release all holds of an LL thread on its LL lock while it
waits for a cross-core request, and re-take them afterwards, like the
kernel-mode IDC path does implicitly. Track the LL lock owner and depth
also without CONFIG_ASSERT for this.

Grant all user-space IPC and LL threads access to the request slots
and LL scheduler locks of all initialized cores. The grants are done
from kernel-only copies of the thread pointers, so keep one for the LL
audio threads as well.

Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
comp_ipc4_get_attribute_remote() allocates the buffer for the remote
core to fill with rzalloc(), which takes a spinlock and fails in user
mode. With CONFIG_SOF_USERSPACE_LL it's called from user-space threads
and the buffer is filled by the user-space IPC thread of the remote
core, so allocate it from the coherent LL user heap instead.

Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
With CONFIG_SOF_USERSPACE_LL, ipc_pipeline_module_free() takes the LL
scheduler lock of the core of the first module in the pipeline. That
is supposed to be the pipeline core, but a module can be on another
core (e.g. an offloaded DP module). Freeing such a remote module is
done on its core, where ipc_comp_free() takes the LL lock of that core,
which would deadlock, if it's the lock taken here.

Pass the pipeline core to ipc_pipeline_module_free() and lock that.

Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
Cross-core binding was rejected at build time with
CONFIG_SOF_USERSPACE_LL. Implement the LL blocking for it:

The user-space LL thread holds its scheduler lock for the whole LL
cycle, so taking the LL lock of a core both blocks its next cycle and
waits for the current one to finish. ll_block() / ll_unblock() now take
the two modules and lock the cores of both modules and of their
pipelines, in ascending order to avoid deadlocks.
ll_wait_finished_on_core() doesn't need to send an IDC anymore.

pipeline_connect() / pipeline_disconnect() asserted the LL lock of the
buffer core. Cross-core buffers always belong to the primary core,
which isn't locked for e.g. a bind between cores 1 and 2, nor when
ipc_pipeline_module_free() disconnects a DP module on core 0 in a
pipeline on core 1. The modified lists are walked by the LL thread of
the pipeline core, so assert the lock of that core instead.

struct pipeline is in cached memory of the pipeline core, so its core
field can be stale on other cores, e.g. on the primary core, which
processes cross-core binds. Record the pipeline core in the coherent
comp_dev, in the new ipc_config.ppl_core, and read it with
comp_ppl_core().

The remote parts of the bind (get_attribute, bind, unbind) go through
idc_send_msg(), which forwards them to the user-space IPC thread of
the remote core.

The domain block / unblock in zephyr_domain.c uses a mutex and a
condition variable, embedded in user memory, and isn't used with
user-space LL, so don't build it then.

Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
With CONFIG_CROSS_CORE_STREAM and CONFIG_SOF_USERSPACE_LL, a DP module
on a different core than its pipeline can now be bound and operated
across cores. With user-space LL, keep rejecting only LL modules on
another core than their pipeline. Such an LL module would need its own
LL task on that core, which is only set up by the kernel-mode IDC
handlers.

comp_buffer_connect() marked a component shared only when the buffer
is on another core. Cross-core buffers always belong to the primary
core, so e.g. a DP module on core 0 in a pipeline on core 1 was never
marked shared, and its operations, called on the pipeline core, were
run there instead of on core 0. Also mark a component shared, if it's
on another core than its pipeline.

Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
Cross-core streams are now supported with user-space LL. Stop
disabling CONFIG_CROSS_CORE_STREAM in the LL user-space overlay.

This allows e.g. the compressed playback topology, which runs the
decoder (DP) on core 1, bound to the host copier on core 0.

The overlay is used by all ACE3.x and newer targets (e.g. PTL, WCL,
NVL) built with os_linux_overlay.conf. Tested on PTL.

Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
@kv2019i
kv2019i force-pushed the 202611-lluser-cross-core-ipc-idc branch from ba5a057 to 6d2890d Compare November 5, 2026 13:55
@kv2019i

kv2019i commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

V4 pushed:

  • fixing review issues so far

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/ipc/ipc-common.c
*/
for (ll_core = 0; ll_core < CONFIG_CORE_COUNT; ll_core++) {
if (ipc_user->audio_thread[ll_core] == k_current_get()) {
ll_depth = user_ll_unlock_sched_all(ll_core);
Comment thread src/ipc/ipc-common.c
}
}

k_sem_take(xc->lock, K_FOREVER);
Comment thread src/ipc/ipc-helper.c
Comment on lines +231 to +232
if (comp_ppl_core(comp) != comp_core && !comp->is_shared)
comp_make_shared(comp);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants