Skip to content

Migrate virtio-snd from semu - #761

Open
Charlie-Tsai1123 wants to merge 4 commits into
sysprog21:masterfrom
Charlie-Tsai1123:research/virtio-snd
Open

Charlie-Tsai1123 wants to merge 4 commits into
sysprog21:masterfrom
Charlie-Tsai1123:research/virtio-snd

Conversation

@Charlie-Tsai1123

@Charlie-Tsai1123 Charlie-Tsai1123 commented Aug 23, 2026 •

Copy link
Copy Markdown
Collaborator

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

virtio-snd-architecture
  • 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.

# aplay -D hw:0,0 --period-size=12000 --buffer-size=48000 /mnt/songdisk/song.wav
Playing WAVE '/mnt/songdisk/song.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono
underrun!!! (at least 0.108 ms long)

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-size combined with a small --buffer-size can make playback fail with an I/O timeout. eg.

# aplay -D hw:0,0 --period-size=12000 --buffer-size=24000 /mnt/songdisk/song.wav
Playing WAVE '/mnt/songdisk/song.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono
aplay: pcm_write:2178: write error: Input/output error
  1. buffer-size directly affects the Linux PCM blocking wait timeout.
  2. period-size does not directly appear in the timeout calculation. However, period-size determines 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:

$$T_{\text{wait}}=\max\left(100\ \text{ms},\frac{N_{\text{buffer}} \times 1100}{f_s}\ \text{ms}\right)$$

where $N_{\text{buffer}}$ is the ALSA buffer-size in frames and $f_s$ is the sample rate.

Source:
https://github.com/torvalds/linux/blob/master/sound/core/pcm_lib.c

Therefore, a smaller buffer-size shortens the time Linux waits for more available PCM space, while a larger period-size increases 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.

$$T_{\text{ConsumePeriod}} < T_{\text{wait}}$$

Synchronization

Primitive Protected / coordinated state
props->lock.lock pending PCM queue, completed-buffer queue, pending_pcm_bytes, inflight_count, and release state
p->tx_mutex + p->tx_cond tx_ev_notify and TX-thread wakeup
p->tx_process_mutex TX avail-ring processing between the asynchronous TX thread and synchronous teardown processing
p->tx_used_mutex deferred TX used-ring/status publication
props->lock.completed notification from PCM consumption to the completion thread
props->lock.all_completed notification to RELEASE/reset paths when all device-owned TX requests have been completed

Test

Kconfig integration

Users can enable or disable the feature through:

make config

Choose Execution Modes then user can determine whether enable or disable Enable VirtIO sound device.

test virtio-snd

I have modified linux.config and buildroot.config. Therefore, build linux image:

make build-linux-image

Host

make system_defconfig
make ENABLE_SYSTEM=1

build/rv32emu \
  -k build/linux-image/Image \
  -i build/linux-image/rootfs.cpio \
  -x vsnd

Guest

readlink /sys/bus/virtio/devices/virtio0/driver
speaker-test -D hw:0,0 -c 1 -r 48000 -F S16_LE -t sine

ctrl + c to stop speaker-test

result

# readlink /sys/bus/virtio/devices/virtio0/driver
../../../../../bus/virtio/drivers/virtio_snd
# speaker-test -D hw:0,0 -c 1 -r 48000 -F S16_LE -t sine

speaker-test 1.2.14

Playback device is hw:0,0
Stream parameters are 48000Hz, S16_LE, 1 channels
Sine wave rate is 440.0000Hz
Rate set to 48000Hz (requested 48000Hz)
Buffer size range from 100 to 61440
Period size range from 50 to 30720
Periods = 4
was set period_size = 12000
was set buffer_size = 48000
 0 - Mono
Time per period = 6.064695
 0 - Mono
Time per period = 7.949389
 0 - Mono
Time per period = 7.874948
 0 - Mono
^CTime per period = 2.097201
# 

Play music by virtio-snd and virtio-blk

Check if the music stops halfway.

transfer mp3 to wav

ffmpeg -i song.mp3 -ac 1 -ar 48000 -sample_fmt s16 song.wav

In rv32emu folder

dd if=/dev/zero of=build/disks/song.img bs=1M count=128

mkdir -p /tmp/rv32-songdisk
sudo mount -o loop build/disks/song.img /tmp/rv32-songdisk
sudo cp <path to song.wav> /tmp/rv32-songdisk/song.wav
sync
sudo umount /tmp/rv32-songdisk

build/rv32emu \
  -k build/linux-image/Image \
  -i build/linux-image/rootfs.cpio \
  -x vsnd \
  -x vblk:build/disks/song.img

guest

  1. mount virtio-blk
mkdir -p /mnt/songdisk
mount /dev/vda /mnt/songdisk

result

# mkdir -p /mnt/songdisk
# mount /dev/vda /mnt/songdisk
[  190.094263] EXT4-fs (vda): mounted filesystem with ordered data mode. Quota mode: disabled.
  1. play music and check whether stop in half
aplay -D hw:0,0 --period-size=12000 --buffer-size=96000 /mnt/songdisk/song.wav

result

# aplay -D hw:0,0 --period-size=12000 --buffer-size=96000 /mnt/songdisk/song.wav
Playing WAVE '/mnt/songdisk/song.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono

Problem

CI smoke test and guest Linux image

This PR also provides a VirtIO sound smoke-test script that checks:

  • VirtIO sound driver binding
  • ALSA sound-card enumeration
  • ALSA PCM device enumeration

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:

/sys/bus/virtio/devices/virtio0
device = 0x0019

but the guest contains only:

/sys/bus/virtio/drivers/virtio_blk
/sys/bus/virtio/drivers/virtio_net
/sys/bus/virtio/drivers/virtio_rng

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:

assets/system/configs/linux.config
assets/system/configs/buildroot.config

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.

  • Device: one mono S16 playback stream with standard rates; MMIO, device-tree, and PLIC IRQ are assigned dynamically alongside vblk/vrng. The PortAudio callback never blocks and fills silence when empty; STOP/RELEASE drain pending buffers and complete requests before teardown. cycles_target is 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.
  • Build/config: new CONFIG_VIRTIO_SND (default in system defconfig), only for native system emulation (not ELF_LOADER/WASM). Gated by HAVE_PORTAUDIO via pkg-config detection of portaudio-2.0; enabling it without PortAudio fails the build with a clear error. Docs at docs/sound.md; docs/networking.md recommends CAP_NET_ADMIN instead of sudo so PortAudio can reach the desktop audio session.
  • Guest/CI/docs: Linux enables sound core and CONFIG_SND_VIRTIO; Buildroot adds ALSA tools. build-linux-image now initializes the nested Doom/Quake submodules so a fresh checkout can build. Adds a headless smoke test .ci/sound.sh (driver bind, ALSA enumeration, two speaker-test loops), wired into the boot flow. A shared per-case retry helper in .ci/common.sh reruns transiently failed boot cases; the ARM64 T2C job gets 2 attempts, other jobs keep 1, and read-only vblk cases may retry while writable ones stay single-attempt.

Migration

Install portaudio dev headers and pkg-config (package portaudio-2.0) before building with CONFIG_VIRTIO_SND=y; rebuild the Linux image to include virtio_snd and ALSA tools.

Written for commit 171e0bb. Summary will update on new commits.

View guided diff

cubic-dev-ai[bot]

This comment was marked as resolved.

@jserv jserv 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.

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.

@visitorckw

Copy link
Copy Markdown
Collaborator

Thanks for the patch.
This is a massive feature and looks promising.
However, it's a bit too huge for a single commit with 2k+ lines.
Could you split this into multiple commits?
This will make the review much easier.

Comment thread src/feature.h Outdated
@Charlie-Tsai1123
Charlie-Tsai1123 force-pushed the research/virtio-snd branch 2 times, most recently from 0d6ecd1 to d29ac78 Compare August 24, 2026 07:40
@Charlie-Tsai1123

Copy link
Copy Markdown
Collaborator Author

Thanks for the suggestion. I have split the original large commit into four logical commits to make the review easier:

  1. Migrate virtio-snd playback from semu
    Adds the initial VirtIO sound playback implementation migrated from semu.
  2. Integrate virtio-snd into rv32emu system emulation
    Adds the device integration, MMIO/IRQ handling, Kconfig/Makefile support, and system configuration.
  3. Defer TX completion until PCM data is consumed
    Reworks the TX path so descriptors are completed only after the corresponding PCM buffer has been consumed by the PortAudio callback.
  4. Add VirtIO sound guest configuration and tests
    Adds the Linux/Buildroot configuration, CI smoke test, and related documentation.

I also addressed the review comment about silently disabling VirtIO sound by replacing it with a compile-time error for invalid feature combinations.

@ChinYikMing

ChinYikMing commented Sep 5, 2026 •

Copy link
Copy Markdown
Collaborator

The upstream has merged reboot fix (#638), please help to extend the rv_cold_reboot to reset the virtio-snd device state when reboot if virtio-snd device is enabled.

bool rv_cold_reboot(riscv_t *rv, riscv_word_t pc)

You can take virtio block as reference

if (attr->vblk[i]) { /* check for reboot */

@jserv jserv added this to the release-2026.2 milestone Sep 20, 2026
jserv

This comment was marked as resolved.

Comment thread src/devices/virtio-snd.c Outdated
Comment thread src/devices/virtio-snd.c Outdated
Comment thread src/devices/virtio-snd.c Outdated
Comment thread src/devices/virtio-snd.c Outdated
Comment thread src/devices/virtio-snd.c
Comment thread src/devices/virtio-snd.c Outdated
@jserv

jserv commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

The used ring index is published from the completion thread with a read-modify-write on ram[queue->queue_used], so a guest polling used->idx while the host sits between the two stores reads zero and can pull a stale element off the ring. The inline comment on virtio_snd_complete_tx_request has the detail and a single-store fix.

@jserv

jserv commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

I defer to @Cuda-Chen for confirmation.

@Charlie-Tsai1123

Copy link
Copy Markdown
Collaborator Author

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.

@Cuda-Chen

This comment was marked as duplicate.

@visitorckw

This comment was marked as resolved.

@Cuda-Chen

This comment was marked as outdated.

@Cuda-Chen

Copy link
Copy Markdown

Hi @Charlie-Tsai1123 ,

While running make build-linux-image, I met the following error:

>>> doom 1.0 Building
/usr/bin/make  CROSS="/tmp/buildroot/output/host/bin/riscv32-buildroot-linux-gnu-" CC="/tmp/buildroot/output/host/bin/riscv32-buildroot-linux-gnu-gcc" LD="/tmp/buildroot/output/host/bin/riscv32-buildroot-linux-gnu-ld" -C /tmp/buildroot/output/build/doom-1.0/riscv
make[3]: *** /tmp/buildroot/output/build/doom-1.0/riscv: No such file or directory.  Stop.
make[2]: *** [package/pkg-generic.mk:273: /tmp/buildroot/output/build/doom-1.0/.stamp_built] Error 2

@Charlie-Tsai1123

Copy link
Copy Markdown
Collaborator Author

I tracked down the reboot and found it was not specific to virtio-snd. The same issue could also be reproduced with virtio-net when using system_defconfig, while system_interpreter_defconfig remained stable across repeated reboots.

The root cause was in rv_step(). cycles_target was calculated before the reboot setjmp() point. A cold or warm reboot resets csr_cycle to zero and then longjmp() returns to the same rv_step() invocation, but the loop was still using the cycle target calculated for the previous guest.

This was especially visible after system_defconfig switched to the tier-1 JIT. The JIT could continue executing toward the stale pre-reboot target, preventing rv_step() from returning to the outer run loop promptly. As a result, periodic VirtIO refreshes such as rv_refresh_vnet() and rv_refresh_vsnd() were delayed, causing networking and sound playback to stop making progress after reboot.

The fix moves the calculation of cycles_target after the reboot jump point, so both the normal boot path and the post-reboot path derive the execution budget from the current csr_cycle. branch_chain_cycle_target is initialized at the same point for the same reason.

After this change, both virtio-net and virtio-snd continue to work across repeated reboots with system_defconfig.

@jserv

jserv commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

The root cause was in rv_step(). cycles_target was calculated before the reboot setjmp() point. A cold or warm reboot resets csr_cycle to zero and then longjmp() returns to the same rv_step() invocation, but the loop was still using the cycle target calculated for the previous guest.

I will send a pull request to rework rv_step with refined coroutine implementation from semu.

@Charlie-Tsai1123

Copy link
Copy Markdown
Collaborator Author

I rebase onto the latest master.

I also updated docs/networking.md and docs/sound.md to clarify the interaction between Linux TAP networking and host audio. TAP requires network administration privileges, but running the whole emulator with sudo may prevent PortAudio from accessing the logged-in user's PipeWire/PulseAudio session. The documentation now recommends granting CAP_NET_ADMIN to the rv32emu executable and running the emulator as the regular user when TAP and virtio-snd are used together.

Comment thread docs/sound.md

### 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If the vnet backend is userspace slirp, the rv32emu can run without any privilege escalation? If yes, could we mention that in the docs?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

Comment thread docs/sound.md Outdated
jserv

This comment was marked as resolved.

Comment thread src/devices/virtio-snd.c Outdated
Comment thread src/devices/virtio-snd.c Outdated
Comment thread src/devices/virtio-snd.c Outdated
Comment thread .github/workflows/main.yml Outdated
Comment thread mk/virtio-snd.mk Outdated
Comment thread mk/virtio-snd.mk Outdated
endif
endif

VIRTIO_SND_BUILD_ENABLED := n

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This looks quite similar to the CONFIG_VIRTIO_SND, can we reuse it?

Use minimal flags if possible.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

CONFIG_VIRTIO_SND also checks the availability of the PORTAUDIO. Thus, the PORTAUDIO_AVAILABLE also looks like a redundant flag?

@Charlie-Tsai1123 Charlie-Tsai1123 Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

@ChinYikMing ChinYikMing Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

jserv

This comment was marked as resolved.

jserv

This comment was marked as resolved.

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>
@Charlie-Tsai1123
Charlie-Tsai1123 force-pushed the research/virtio-snd branch 2 times, most recently from a5a7d44 to d715786 Compare October 8, 2026 09:23
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.
Comment thread src/devices/virtio-snd.c
.status = completion_status,
.latency_bytes = latency_bytes,
};
if (!vsnd_guest_write(vsnd, status_addr, &status, sizeof(status))) {

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.

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.

Comment thread src/devices/virtio-snd.c
vsnd_cancel_pending_and_wait(props, VSND_CANCEL_TX_QUIESCED);

if (props->pa_stream)
vsnd_close_stream(props);

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.

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.

Comment thread src/devices/virtio-snd.c
*plen = 0;
if (!vsnd_collect_desc_chain(vsnd, queue, desc_idx, chain, &cnt))
return -1;
if (cnt < 3)

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.

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.

Comment thread mk/virtio-snd.mk
ifeq ($(SYSTEM_MMIO),1)
ifneq ($(CC_IS_EMCC),1)

CFLAGS += -DRV32_FEATURE_VIRTIO_SND=$(call config-to-feature,VIRTIO_SND)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread mk/virtio-snd.mk
endif

else
CFLAGS += -DRV32_FEATURE_VIRTIO_SND=0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

IMO, should not override silently, should show error message and exit instead.

Comment thread mk/virtio-snd.mk
CFLAGS += -DRV32_FEATURE_VIRTIO_SND=0
endif
else
CFLAGS += -DRV32_FEATURE_VIRTIO_SND=0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ditto.

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.

5 participants