Repository navigation
Migrate virtio-snd from semu - #761
Charlie-Tsai1123 wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Benchmarks
Details
| Benchmark suite | Current: 4510c86 | Previous: ba208fc | Ratio |
|---|---|---|---|
Dhrystone |
2935.5 DMIPS |
2173 DMIPS |
0.74 |
CoreMark |
2144.793 iterations/sec |
1773.286 iterations/sec |
0.83 |
This comment was automatically generated by workflow using github-action-benchmark.
3f4365b to
e849ebf
Compare
|
Thanks for the patch. |
0d6ecd1 to
d29ac78
Compare
|
Thanks for the suggestion. I have split the original large commit into four logical commits to make the review easier:
I also addressed the review comment about silently disabling VirtIO sound by replacing it with a compile-time error for invalid feature combinations. |
d29ac78 to
ba208fc
Compare
|
The used ring index is published from the completion thread with a read-modify-write on |
ba208fc to
4510c86
Compare
|
I defer to @Cuda-Chen for confirmation. |
|
I also made an additional CI improvement while testing this change. The ARM64 T2C job can occasionally lose the rv32emu process during Linux boot in different test cases, so I added a shared per-case retry helper instead of retrying the whole boot-linux suite. Only the failed case is retried once on ARM64 T2C. |
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as outdated.
This comment was marked as outdated.
|
Hi @Charlie-Tsai1123 , While running |
6ff23be to
a6316ad
Compare
|
I tracked down the reboot and found it was not specific to The root cause was in This was especially visible after The fix moves the calculation of After this change, both |
I will send a pull request to rework |
a6316ad to
cf05e2c
Compare
|
I rebase onto the latest I also updated |
cf05e2c to
e2952d2
Compare
|
|
||
| ### Using virtio-snd with TAP networking | ||
|
|
||
| Linux TAP networking requires network administration privileges. When using virtio-snd and TAP together, avoid running the whole emulator with `sudo` if the host audio backend is provided by the logged-in user's PipeWire or PulseAudio session. In that case, PortAudio may fail to initialize because the root process does not have the same user audio session. |
There was a problem hiding this comment.
If the vnet backend is userspace slirp, the rv32emu can run without any privilege escalation? If yes, could we mention that in the docs?
There was a problem hiding this comment.
Yes. slirp does not require privilege escalation. I reorganized this section and added separate usage examples for user-mode slirp, Linux TAP, and macOS vmnet.
I also tested the macOS vmnet case separately with a github actions workflow in this branch, running -x vnet:vmnet and -x vsnd in the same sudo -E rv32emu process. The vmnet network test and virtio-snd playback test both completed successfully.
e2952d2 to
d090bf3
Compare
d090bf3 to
8c4606e
Compare
| endif | ||
| endif | ||
|
|
||
| VIRTIO_SND_BUILD_ENABLED := n |
There was a problem hiding this comment.
This looks quite similar to the CONFIG_VIRTIO_SND, can we reuse it?
Use minimal flags if possible.
There was a problem hiding this comment.
CONFIG_VIRTIO_SND also checks the availability of the PORTAUDIO. Thus, the PORTAUDIO_AVAILABLE also looks like a redundant flag?
There was a problem hiding this comment.
CONFIG_VIRTIO_SND covers the PortAudio dependency in the normal Kconfig path. However, $(call has,VIRTIO_SND) also supports the legacy ENABLE_VIRTIO_SND override, so ENABLE_VIRTIO_SND=1 can bypass the Kconfig dependency result (eg. disable in kconfig but opened by make ENABLE_VIRTIO_SND=1 so we need to check PortAudio because Kconfig disable therefore haven't check PortAudio yet)
I therefore keep PORTAUDIO_AVAILABLE as a build-time host dependency check, and use VIRTIO_SND_BUILD_ENABLED as the effective build state after considering SYSTEM_MMIO, the compiler, and the current PortAudio availability.
There was a problem hiding this comment.
If the hostOS does not have PULSEAUDIO support but user force ENABLE_VIRTIO_SND=1 during build, should simply show error and exit. So, no need VIRTIO_SND_BUILD_ENABLED anymore.
There was a problem hiding this comment.
Thanks for the suggestion. I simplified the virtio-snd build logic accordingly.
I removed VIRTIO_SND_BUILD_ENABLED and PORTAUDIO_AVAILABLE, and now rely on the existing CONFIG_VIRTIO_SND / legacy ENABLE_VIRTIO_SND handling directly. An explicit ENABLE_VIRTIO_SND=1 is rejected when the selected build mode or PortAudio dependency cannot support virtio-snd, while other overrides such as ENABLE_ELF_LOADER=1 simply disable virtio-snd as expected.
While testing the enable/disable transition, I also found that the special jit.o rule did not depend on EFFECTIVE_CONFIG_STAMP, which could leave stale JIT code after a feature change. I added that dependency as well.
Thanks again for pointing this out.
8c4606e to
4b9a4be
Compare
Migrate virtio-snd playback support from semu and adapt the device
model to rv32emu's VirtIO-MMIO and device infrastructure.
This migration makes the following changes:
1. Adapt the VirtIO sound state to rv32emu
Define the virtio-snd device and queue state in virtio.h and use
rv32emu's existing VirtIO-MMIO register definitions, status bits, and
descriptor definitions.
Expose virtio_snd_read() and virtio_snd_write() through rv32emu's
device interface instead of relying on semu-specific hart and memory
access handling.
2. Make virtio-snd state self-contained
Replace semu's process-wide virtio-snd configuration, stream state, and
synchronization objects with per-device private state.
Keep stream properties, PortAudio state, worker threads, and completion
state associated with the corresponding virtio-snd instance so device
resources have explicit ownership and lifecycle management.
3. Adapt PCM queue management to rv32emu
Use rv32emu's existing Linux-like list helpers for the pending PCM and
completed-request queues.
This keeps queue management consistent with rv32emu while allowing a
request to move from the playback path to the completion path without
maintaining separate linked-list implementations.
Validate buffer_bytes and period_bytes, bound each PCM transfer by
period_bytes, and cap pending PCM data at buffer_bytes before
enqueueing.
4. Separate TX processing, PCM consumption, and VirtIO completion
Use a TX worker thread to process guest TX requests and place their PCM
payloads in the pending queue. Unlike semu, keep each request in flight
until its PCM data has actually been consumed by PortAudio callback.
Consumed requests are moved to a completed queue. A dedicated
completion thread then writes the virtio_snd_pcm_status response,
updates the TX used ring, and raises used-ring interrupt outside the
real-time audio callback.
The TX completion paths differ as follows:
semu:
guest PCM --> TX thread --> [ PCM ] --> PortAudio callback --> speaker
|
+-------------------------------------------> used ring
rv32emu:
speaker
^
|
guest PCM --> TX thread --> [ PCM | PCM | PCM ] --> PortAudio callback
|
| consumed
v
used ring <-- completion thread <-- [ done | done | done ]
If no PCM buffer is available when the PortAudio callback requests
audio data, zero-fill the output buffer so the backend produces silence
instead of consuming invalid or stale PCM data.
5. Track pending and in-flight PCM requests
Track pending PCM bytes and the number of in-flight TX requests for
each stream.
PCM release and device reset can therefore move pending requests
through the completion path and wait for outstanding VirtIO requests
before stream resources are released.
6. Tighten descriptor and request validation
Validate queue sizes, descriptor indices and chains, guest memory
ranges, descriptor direction, request sizes, stream IDs, PCM
parameters, and stream state transitions before accessing guest memory
or host audio resources.
Report malformed or unsupported requests through the appropriate
VirtIO sound status codes and mark invalid transport state as requiring
device reset where appropriate.
7. Add explicit resource lifecycle management
Add initialization, reset, and teardown paths for the sound device.
Stop worker threads, close active PortAudio streams, release queued
requests, destroy synchronization objects, and terminate PortAudio when
the device is deleted.
Separate stream draining from device reset: stream release drains
descriptors already published to the live TX virtqueue, while device
reset quiesces TX processing and completes only host-owned requests
before clearing the old queue state.
The event and RX queues remain unsupported. This implementation
currently provides PCM playback through the control and TX queues.
This commit introduces the complete virtio-snd device model only.
System MMIO routing, interrupt routing, device-tree exposure, command
line configuration, and build integration are added in a next commit.
Co-authored-by: Cuda-Chen <clh960524@gmail.com>
a5a7d44 to
d715786
Compare
Integrate the virtio-snd device model with rv32emu system emulation so the guest can discover and access the sound device through VirtIO-MMIO. 1. Add virtio-snd build configuration Add the VIRTIO_SND configuration option and build rules required to compile the sound device with system emulation. Keep the PortAudio dependency conditional on virtio-snd support so builds without the sound device do not require the host audio backend. 2. Add virtio-snd command-line support Extend the system device option to allow virtio-snd to be enabled with `-x vsnd`. Propagate the selected configuration through the VM system attributes so device initialization and device-tree generation operate on the same virtio-snd instance. 3. Integrate virtio-snd with system MMIO Allocate and initialize the virtio-snd device during system startup, attach guest RAM to device state, and route accesses in its assigned VirtIO-MMIO region through virtio_snd_read() and virtio_snd_write(). This allows the Linux VirtIO transport to negotiate features, configure virtqueues, and submit control and PCM requests to the device. 4. Connect virtio-snd interrupts to the PLIC Assign an interrupt source to virtio-snd and propagate the device interrupt status to the PLIC. Update the PLIC state after virtio-snd MMIO accesses so used-ring and configuration interrupts generated by the device are visible to the guest. 5. Add virtio-snd to the generated device tree Allocate an MMIO region and interrupt number for virtio-snd and add a corresponding virtio,mmio node to the generated device tree. Use the existing optional VirtIO device allocation flow so virtio-snd can coexist with virtio-blk, virtio-rng, and virtio-net without reusing MMIO regions or interrupt sources. 6. Integrate virtio-snd with the system lifecycle Initialize the host sound backend when the device is created and keep the device state associated with the VM throughout system execution. Reset guest-visible VirtIO and PCM state during reboot and clear stale virtio-snd PLIC state so interrupts from the previous guest instance do not leak into the restarted guest. Release the virtio-snd device and its PortAudio resources during system teardown. With this integration, rv32emu can expose the virtio-snd device to a guest when started with `-x vsnd`.
Add automated validation and usage documentation for virtio-snd support. 1. Add virtio-snd playback testing Add a dedicated sound test that boots Linux with `-x vsnd` and verifies that the VirtIO device binds to the `virtio_snd` driver. Check `/proc/asound/cards` and `aplay -l` to confirm that ALSA enumerates the VirtIO SoundCard and its PCM playback device. 2. Exercise the PCM playback path Run `speaker-test` with the verified mono 48 kHz S16_LE configuration for two playback loops. This exercises PCM parameter setup, stream startup, TX virtqueue processing, PortAudio consumption, deferred completion, and used-ring notification instead of checking device enumeration alone. Use a bounded speaker-test loop count so the test exits normally without depending on an external playback timeout. 3. Integrate virtio-snd with CI Run the virtio-snd test as part of the Linux boot test flow and install the host PortAudio dependency required by sound-enabled builds. Add the corresponding macOS build dependency through Homebrew and update the shared build setup where required. 4. Document virtio-snd usage
The Linux image builder copies Doom and Quake sources from nested submodules, but a regular clone leaves them uninitialized. This causes make build-linux-image to fail when the expected Doom or Quake sources are missing. Initialize only the required submodules in the build-linux-image target before invoking the build script, so a fresh checkout can build the guest image without a manual submodule step.
d715786 to
171e0bb
Compare
| .status = completion_status, | ||
| .latency_bytes = latency_bytes, | ||
| }; | ||
| if (!vsnd_guest_write(vsnd, status_addr, &status, sizeof(status))) { |
There was a problem hiding this comment.
virtio_snd_complete_tx_request writes the PCM status and the used ring from the completion thread (and from the TX thread on its error paths), while memory_gc() on the emulator thread may reclaim the same 64 KiB chunk. If is_region_zero() passes just before this write lands, the following mprotect(PROT_NONE) and madvise(MADV_DONTNEED) discard the status. The guest then sees a used entry whose status is not VIRTIO_SND_S_OK. Simplest fix: have the worker threads only queue finished requests, and let rv_refresh_vsnd() publish them on the emulator thread. That also makes the used-ring locking simpler.
| vsnd_cancel_pending_and_wait(props, VSND_CANCEL_TX_QUIESCED); | ||
|
|
||
| if (props->pa_stream) | ||
| vsnd_close_stream(props); |
There was a problem hiding this comment.
vsnd_reset_stream ignores the results of Pa_AbortStream and vsnd_close_stream. If the close fails, pa_stream stays open and its callback can keep running. vsnd_destroy_prop then destroys props->lock.lock and vsnd_delete frees props, so the next callback uses a destroyed mutex and freed memory. When the stream cannot be closed, either keep the callback-owned state alive (leak it) or abort, rather than tearing it down underneath a live callback.
| *plen = 0; | ||
| if (!vsnd_collect_desc_chain(vsnd, queue, desc_idx, chain, &cnt)) | ||
| return -1; | ||
| if (cnt < 3) |
There was a problem hiding this comment.
TX parsing assumes the header, the PCM data and the status each sit in their own descriptor. A chain of [xfer header + PCM][status] is rejected with DEVICE_NEEDS_RESET, and any PCM that follows the header inside chain[0] is silently dropped because copying starts at chain[1]. Linux uses separate descriptors, so it works today, but VirtIO 1.x message framing forbids the device from assuming a layout. The control path makes the same assumption about the INFO payload in chain[2]. A follow-up could walk the readable and writable bytes by offset instead of by descriptor index.
| ifeq ($(SYSTEM_MMIO),1) | ||
| ifneq ($(CC_IS_EMCC),1) | ||
|
|
||
| CFLAGS += -DRV32_FEATURE_VIRTIO_SND=$(call config-to-feature,VIRTIO_SND) |
There was a problem hiding this comment.
config-to-feature is the callee of the set-feature, so use $(call set-features, VIRTIO_SND) in Makefile to group features together, see here.
| endif | ||
|
|
||
| else | ||
| CFLAGS += -DRV32_FEATURE_VIRTIO_SND=0 |
There was a problem hiding this comment.
IMO, should not override silently, should show error message and exit instead.
| CFLAGS += -DRV32_FEATURE_VIRTIO_SND=0 | ||
| endif | ||
| else | ||
| CFLAGS += -DRV32_FEATURE_VIRTIO_SND=0 |
Description
This PR migrates the VirtIO sound playback implementation from semu.
In addition to the migration, the PCM TX completion path, buffering, synchronization, PortAudio callback behavior, build configuration, and guest Linux configuration are reworked to provide more stable playback under rv32emu.
PCM TX completion design
The original semu design uses a single-buffer style flow:
guest PCM data is passed to virtio_snd_stream_cb, and the TX request can be completed once the data has been handed to the pcm buffer queue.
In the current rv32emu design, TX requests are first stored in a PCM buffer queue. PortAudio consumes data from that queue, and once a buffer has actually been consumed, it is moved to a complete buffer queue and wake up complete thread. A dedicated completion thread then publishes the used-ring entry.
Using the original semu-style structure directly in rv32emu caused unstable playback. In practice, blocking producer/consumer synchronization can stall the PortAudio callback and make playback timing unstable, which made speaker-test playback unstable. The following video shows the result:
https://youtu.be/T5NduRhwVj0?si=ZevcrT1AYzJYZv-T
Simply extending the PCM buffer queue did make playback smoother. However, it was still insufficient: the TX thread would publish the used ring immediately after pushing PCM data into the PCM buffer queue, even though the host side might not have finished playing it yet.
As a result, the guest could observe the request as completed before the audio had actually finished playing, which could cause playback to stop halfway.
For this reason, rv32emu uses the current two-stage design:
PCM data is first queued for playback, and only after it has actually been consumed is the request moved to the complete buffer queue and finished by the completion thread.
The following video shows the result after modification:
https://youtu.be/hRR-VepwyWI?si=gLwI-OOgesJMAH1m
Play music compare semu to rv32emu:
semu:
https://youtu.be/QIN-FOWNjWU?si=g3vCaIY6EQTpr2jh
rv32emu:
https://youtu.be/hK_aWG_lJHc?si=1LoRCLUhZz62EhhS
PortAudio underrun handling
The PortAudio callback must not wait for the producer when PCM data is temporarily unavailable. eg. at the begining, host side starts to play, however guest side haven't finished preparing pcm yet would cause underrun.
Therefore, I use
memset(output, 0, out_bytes);when pcm buffer queue is empty PortAudio would silent.ALSA period/buffer size and TX completion
large
--period-sizecombined with a small--buffer-sizecan make playback fail with an I/O timeout. eg.buffer-sizedirectly affects the Linux PCM blocking wait timeout.period-sizedoes not directly appear in the timeout calculation. However,period-sizedetermines how much PCM data must be consumed before one VirtIO TX request can be completed and returned through the used ring.In Linux, the default wait timeout used by
wait_for_avail()is:where$N_{\text{buffer}}$ is the ALSA $f_s$ is the sample rate.
buffer-sizein frames andSource:
https://github.com/torvalds/linux/blob/master/sound/core/pcm_lib.c
Therefore, a smaller
buffer-sizeshortens the time Linux waits for more available PCM space, while a largerperiod-sizeincreases the amount of PCM that must be consumed before a TX completion can be generated.When the buffer is full, at least one outstanding period must be fully consumed by the PortAudio callback and its TX completion must be published to the used ring before Linux can reclaim that space.
Synchronization
props->lock.lockpending_pcm_bytes,inflight_count, and release statep->tx_mutex+p->tx_condtx_ev_notifyand TX-thread wakeupp->tx_process_mutexp->tx_used_mutexprops->lock.completedprops->lock.all_completedTest
Kconfig integration
Users can enable or disable the feature through:
Choose
Execution Modesthen user can determine whether enable or disableEnable VirtIO sound device.test virtio-snd
I have modified
linux.configandbuildroot.config. Therefore, build linux image:Host
Guest
ctrl + cto stop speaker-testresult
Play music by virtio-snd and virtio-blk
Check if the music stops halfway.
transfer mp3 to wav
In rv32emu folder
guest
result
result
Problem
CI smoke test and guest Linux image
This PR also provides a VirtIO sound smoke-test script that checks:
The test is currently not wired into
.github/workflows/main.yml.The existing rv32emu system CI downloads the prebuilt Linux-Image artifact. With the current prebuilt guest image, rv32emu successfully exposes the sound device:
but the guest contains only:
and does not contain
virtio_snd. The current prebuilt root filesystem also does not provide the ALSA utilities required by the test.This PR updates:
to enable the VirtIO sound guest driver and provide aplay /speaker-test.
rv32emu already rebuilds and publishes a new Linux-Image after relevant guest configuration changes are merged to master. However, PR CI still uses the previously published artifact, which creates a bootstrap problem:
the sound smoke test cannot pass until the new guest artifact exists. I would appreciate the maintainers' preference on how this test should be integrated into PR CI.
There is also an ongoing CI refactoring in:
#638
In particular:
bda4649
This turns .ci/boot-linux.sh into a driver for individual Linux test scripts. If that approach is merged, the VirtIO sound smoke test could be integrated through the same mechanism instead of adding a separate GitHub Actions step.
Summary by cubic
Migrates VirtIO sound playback from semu and makes it an optional system device enabled with
-x vsnd. TX completion now occurs only after PortAudio consumes PCM (was: on queue push), adding backpressure, fixing stutter, and outputting silence on underrun.vblk/vrng. The PortAudio callback never blocks and fills silence when empty; STOP/RELEASE drain pending buffers and complete requests before teardown.cycles_targetis computed after the reboot jump point, and reboot clears stale device and PLIC state, so virtio-net and virtio-snd refresh paths keep making progress across warm/cold reboots.CONFIG_VIRTIO_SND(default in system defconfig), only for native system emulation (notELF_LOADER/WASM). Gated byHAVE_PORTAUDIOviapkg-configdetection ofportaudio-2.0; enabling it without PortAudio fails the build with a clear error. Docs atdocs/sound.md;docs/networking.mdrecommendsCAP_NET_ADMINinstead ofsudoso PortAudio can reach the desktop audio session.CONFIG_SND_VIRTIO; Buildroot adds ALSA tools.build-linux-imagenow initializes the nested Doom/Quake submodules so a fresh checkout can build. Adds a headless smoke test.ci/sound.sh(driver bind, ALSA enumeration, twospeaker-testloops), wired into the boot flow. A shared per-case retry helper in.ci/common.shreruns transiently failed boot cases; the ARM64 T2C job gets 2 attempts, other jobs keep 1, and read-onlyvblkcases may retry while writable ones stay single-attempt.Migration
Install
portaudiodev headers andpkg-config(packageportaudio-2.0) before building withCONFIG_VIRTIO_SND=y; rebuild the Linux image to includevirtio_sndand ALSA tools.Written for commit 171e0bb. Summary will update on new commits.