From b138bf439aa7d0ad1b794e0fcbad2e20329ca9b8 Mon Sep 17 00:00:00 2001 From: CHENG-YU TSAI Date: Fri, 25 Sep 2026 00:45:54 +0800 Subject: [PATCH 1/4] Migrate virtio-snd from semu 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 and PortAudio state associated with each stream, while device-wide worker and completion state remain owned by the corresponding virtio-snd instance. This gives device resources explicit ownership and lifecycle management. 3. Adapt PCM queue management to rv32emu Use rv32emu existing Linux-like list helpers for PCM queue management. Keep pending PCM queues per stream, since their contents are consumed by corresponding PortAudio stream. Keep completed TX requests in one device-wide queue, since all streams ultimately publish completions to the same TX virtqueue. This allows requests to move from stream-owned playback state to device-owned completion state without maintaining separate linked-list implementations. 4. Separate TX processing, PCM consumption, and guest completion Use a TX worker thread to process guest TX requests and place their PCM payloads in per-stream pending queue. Unlike semu, keep each request in flight until its PCM data has actually been consumed by the PortAudio callback. Once a request is consumed, move it to a device-wide completed queue. Worker contexts only stage completed requests and never publish guest-visible completion state. The emulator thread drains the completed queue from rv_refresh_vsnd(), writes virtio_snd_pcm_status, updates the TX used ring, and then propagates the resulting interrupt. This keeps guest-memory writes on the emulator thread and avoids races with memory_gc(). 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 <-- emulator thread (rv_refresh_vsnd()) <-- [ done | done ] TX error paths use the same device-wide completed queue, including requests that do not belong to a valid stream. 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 host-owned in-flight TX requests for each stream. In-flight request remains stream-owned until its PCM data is consumed or moved out of the pending queue. Once staged in the device-wide completed queue, it no longer counts as stream-owned even though its guest-visible used-ring entry may not have been published yet. PCM release and device reset therefore first quiesce stream-owned work, then flush staged completions on the emulator thread before completing the release or clearing the old VirtIO queue state. 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 --- src/devices/virtio-snd.c | 2098 ++++++++++++++++++++++++++++++++++++++ src/devices/virtio.h | 49 + 2 files changed, 2147 insertions(+) create mode 100644 src/devices/virtio-snd.c diff --git a/src/devices/virtio-snd.c b/src/devices/virtio-snd.c new file mode 100644 index 000000000..7564c2b47 --- /dev/null +++ b/src/devices/virtio-snd.c @@ -0,0 +1,2098 @@ +/* + * rv32emu is freely redistributable under the MIT License. See the file + * "LICENSE" for information on usage and redistribution of this file. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "utils.h" +#include "virtio.h" + +#define VSND_DEV_CNT_MAX 1 +#define VSND_QUEUE_NUM_MAX 1024 +#define VSND_QUEUE_COUNT 4 +#define VSND_QUEUE (vsnd->queues[vsnd->queue_sel]) +#define VSND_CNFA_FRAME_SZ 2 /* S16 = 2 bytes per sample */ +/* + * Bound guest-controlled PCM allocations. The current device supports + * mono S16 playback up to 384 kHz: + * 384000 samples/sec * 1 channel * 2 bytes/sample = 768000 bytes/sec + * so 1 MiB is sufficient for more than + * one second of PCM even at the highest advertised sample rate. + */ +#define VSND_PCM_BUFFER_BYTES_MAX (1024U * 1024U) + +enum { + VSND_QUEUE_CTRL = 0, + VSND_QUEUE_EVT = 1, + VSND_QUEUE_TX = 2, + VSND_QUEUE_RX = 3, +}; + +enum { + VSND_FEATURES_0 = 0, + VSND_FEATURES_1 = 1, /* VIRTIO_F_VERSION_1 */ +}; + +enum { + /* jack control request types */ + VIRTIO_SND_R_JACK_INFO = 1, + + /* PCM control request types */ + VIRTIO_SND_R_PCM_INFO = 0x0100, + VIRTIO_SND_R_PCM_SET_PARAMS, + VIRTIO_SND_R_PCM_PREPARE, + VIRTIO_SND_R_PCM_RELEASE, + VIRTIO_SND_R_PCM_START, + VIRTIO_SND_R_PCM_STOP, + + /* channel map control request types */ + VIRTIO_SND_R_CHMAP_INFO = 0x0200, + + /* jack event types */ + VIRTIO_SND_EVT_JACK_CONNECTED = 0x1000, + VIRTIO_SND_EVT_JACK_DISCONNECTED, + + /* PCM event types */ + VIRTIO_SND_EVT_PCM_PERIOD_ELAPSED = 0x1100, + VIRTIO_SND_EVT_PCM_XRUN, + + /* common status codes */ + VIRTIO_SND_S_OK = 0x8000, + VIRTIO_SND_S_BAD_MSG, + VIRTIO_SND_S_NOT_SUPP, + VIRTIO_SND_S_IO_ERR, +}; + +/* Unit: Hz */ +#define SND_PCM_RATE \ + _(5512) \ + _(8000) \ + _(11025) \ + _(16000) \ + _(22050) \ + _(32000) \ + _(44100) \ + _(48000) \ + _(64000) \ + _(88200) \ + _(96000) \ + _(176400) \ + _(192000) \ + _(384000) + +enum { +#define _(rate) VIRTIO_SND_PCM_RATE_##rate, + SND_PCM_RATE +#undef _ + VIRTIO_SND_PCM_RATE_COUNT, +}; + +static const int pcm_rate_tbl[] = { +#define _(rate) [VIRTIO_SND_PCM_RATE_##rate] = rate, + SND_PCM_RATE +#undef _ +}; + +enum { + VIRTIO_SND_PCM_F_SHMEM_HOST = 0, + VIRTIO_SND_PCM_F_SHMEM_GUEST, + VIRTIO_SND_PCM_F_MSG_POLLING, + VIRTIO_SND_PCM_F_EVT_SHMEM_PERIODS, + VIRTIO_SND_PCM_F_EVT_XRUNS, +}; + +enum { +#define _(samp_fmt) VIRTIO_SND_PCM_FMT_##samp_fmt + _(IMA_ADPCM) = 0, + _(MU_LAW), + _(A_LAW), + _(S8), + _(U8), + _(S16), + _(U16), + _(S18_3), + _(U18_3), + _(S20_3), + _(U20_3), + _(S24_3), + _(U24_3), + _(S20), + _(U20), + _(S24), + _(U24), + _(S32), + _(U32), + _(FLOAT), + _(FLOAT64), + _(DSD_U8), + _(DSD_U16), + _(DSD_U32), + _(IEC958_SUBFRAME), +#undef _ +}; + +enum { +#define _(chmap_pos) VIRTIO_SND_CHMAP_##chmap_pos + _(NONE) = 0, + _(NA), + _(MONO), + _(FL), + _(FR), + _(RL), + _(RR), + _(FC), + _(LFE), + _(SL), + _(SR), + _(RC), + _(FLC), + _(FRC), + _(RLC), + _(RRC), + _(FLW), + _(FRW), + _(FLH), + _(FCH), + _(FRH), + _(TC), + _(TFL), + _(TFR), + _(TFC), + _(TRL), + _(TRR), + _(TRC), + _(TFLC), + _(TFRC), + _(TSL), + _(TSR), + _(LLFE), + _(RLFE), + _(BC), + _(BLC), + _(BRC), +#undef _ +}; + +enum { + VIRTIO_SND_D_OUTPUT = 0, + VIRTIO_SND_D_INPUT, +}; + +typedef struct { + uint32_t jacks; + uint32_t streams; + uint32_t chmaps; + uint32_t controls; +} virtio_snd_config_t; + +typedef struct { + uint32_t code; +} virtio_snd_hdr_t; + +typedef struct { + uint32_t hda_fn_nid; +} virtio_snd_info_t; + +typedef struct { + virtio_snd_hdr_t hdr; + uint32_t start_id; + uint32_t count; + uint32_t size; +} virtio_snd_query_info_t; + +typedef struct { + virtio_snd_info_t hdr; + uint32_t features; + uint32_t hda_reg_defconf; + uint32_t hda_reg_caps; + uint8_t connected; + uint8_t padding[7]; +} virtio_snd_jack_info_t; + +typedef struct { + virtio_snd_info_t hdr; + uint32_t features; + uint64_t formats; + uint64_t rates; + uint8_t direction; + uint8_t channels_min; + uint8_t channels_max; + uint8_t padding[5]; +} virtio_snd_pcm_info_t; + +typedef struct { + virtio_snd_hdr_t hdr; + uint32_t stream_id; +} virtio_snd_pcm_hdr_t; + +typedef struct { + virtio_snd_pcm_hdr_t hdr; + uint32_t buffer_bytes; + uint32_t period_bytes; + uint32_t features; + uint8_t channels; + uint8_t format; + uint8_t rate; + uint8_t padding; +} virtio_snd_pcm_set_params_t; + +typedef struct { + uint32_t stream_id; +} virtio_snd_pcm_xfer_t; + +typedef struct { + uint32_t status; + uint32_t latency_bytes; +} virtio_snd_pcm_status_t; + +#define VIRTIO_SND_CHMAP_MAX_SIZE 18 + +typedef struct { + virtio_snd_info_t hdr; + uint8_t direction; + uint8_t channels; + uint8_t positions[VIRTIO_SND_CHMAP_MAX_SIZE]; +} virtio_snd_chmap_info_t; + +typedef struct { + pthread_cond_t all_completed; + size_t pending_pcm_bytes; + uint32_t inflight_count; + bool releasing; + pthread_mutex_t lock; +} virtio_snd_queue_lock_t; + +typedef struct { + uint32_t stream_id; +} vsnd_stream_sel_t; + +typedef struct vsnd_buf_queue_node { + uint8_t *addr; + uint32_t len; + uint32_t pos; + + /* + * Keep the VirtIO request alive until its completion is published + * to the guest by the emulator thread. + */ + uint32_t desc_idx; + uint64_t status_addr; + uint32_t stream_id; + uint32_t completion_status; + uint32_t latency_bytes; + + struct list_head q; +} vsnd_buf_queue_node_t; + +typedef struct { + virtio_snd_jack_info_t j; + virtio_snd_pcm_info_t p; + virtio_snd_chmap_info_t c; + virtio_snd_pcm_set_params_t pp; + PaStream *pa_stream; + virtio_snd_queue_lock_t lock; + struct list_head buf_queue; + virtio_snd_state_t *vsnd; + vsnd_stream_sel_t v; +} virtio_snd_prop_t; + +typedef struct { + virtio_snd_config_t config; + virtio_snd_prop_t props[VSND_DEV_CNT_MAX]; + + pthread_mutex_t tx_mutex; + pthread_cond_t tx_cond; + bool tx_ev_notify; + bool tx_thread_stop; + bool tx_thread_started; + pthread_t tx_thread; + + pthread_mutex_t tx_process_mutex; + + /* + * Finished TX requests waiting for the emulator thread to publish + * their status and used-ring entries to guest memory. + */ + pthread_mutex_t completed_mutex; + struct list_head completed_queue; + + bool pa_initialized; + virtio_snd_state_t *vsnd; +} virtio_snd_priv_t; + +typedef enum { + VSND_CANCEL_DRAIN_TX, + VSND_CANCEL_TX_QUIESCED, +} vsnd_cancel_mode_t; + +typedef union { + virtio_snd_hdr_t hdr; + virtio_snd_query_info_t query; + virtio_snd_pcm_set_params_t set_params; + virtio_snd_pcm_hdr_t pcm; +} vsnd_ctrl_request_t; + +typedef union { + virtio_snd_jack_info_t jack[VSND_DEV_CNT_MAX]; + virtio_snd_pcm_info_t pcm[VSND_DEV_CNT_MAX]; + virtio_snd_chmap_info_t chmap[VSND_DEV_CNT_MAX]; +} vsnd_ctrl_info_t; + +typedef int (*vsnd_virtq_cb)(virtio_snd_state_t *vsnd, + virtio_snd_queue_t *queue, + uint32_t desc_idx, + uint32_t *plen); + +static int virtio_snd_stream_cb(const void *input, + void *output, + unsigned long frame_cnt, + const PaStreamCallbackTimeInfo *time_info, + PaStreamCallbackFlags status_flags, + void *user_data); + +static void virtio_queue_notify_handler(virtio_snd_state_t *vsnd, int index); +static void virtio_snd_process_tx_queue(virtio_snd_state_t *vsnd); +static void vsnd_cancel_pending_and_wait(virtio_snd_prop_t *props, + vsnd_cancel_mode_t mode); +static void virtio_snd_set_fail(virtio_snd_state_t *vsnd); +static void vsnd_completed_push(virtio_snd_state_t *vsnd, + vsnd_buf_queue_node_t *node); + +static inline virtio_snd_priv_t *vsnd_priv(virtio_snd_state_t *vsnd) +{ + return (virtio_snd_priv_t *) vsnd->priv; +} + +static inline uint32_t vsnd_min(uint32_t a, uint32_t b) +{ + return a < b ? a : b; +} + +static bool vsnd_stream_accepts_tx(uint32_t code) +{ + return code == VIRTIO_SND_R_PCM_PREPARE || code == VIRTIO_SND_R_PCM_START || + code == VIRTIO_SND_R_PCM_STOP; +} + +static void vsnd_queue_push(virtio_snd_prop_t *props, + vsnd_buf_queue_node_t *node) +{ + /* + * list_add() inserts after the list head. The oldest PCM buffer therefore + * remains at the tail and is consumed through list_last_entry(). + */ + list_add(&node->q, &props->buf_queue); +} + +static vsnd_buf_queue_node_t *vsnd_queue_front(virtio_snd_prop_t *props) +{ + if (list_empty(&props->buf_queue)) + return NULL; + + return list_last_entry(&props->buf_queue, vsnd_buf_queue_node_t, q); +} + +static void vsnd_queue_remove(vsnd_buf_queue_node_t *node) +{ + list_del(&node->q); +} + +static void vsnd_free_node(vsnd_buf_queue_node_t *node) +{ + if (!node) + return; + free(node->addr); + free(node); +} + +static void vsnd_completed_push(virtio_snd_state_t *vsnd, + vsnd_buf_queue_node_t *node) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + pthread_mutex_lock(&p->completed_mutex); + + /* + * Newest entries are inserted at the head; the emulator thread + * removes the oldest entry from the tail. + */ + list_add(&node->q, &p->completed_queue); + pthread_mutex_unlock(&p->completed_mutex); +} + +static bool vsnd_queue_completion(virtio_snd_state_t *vsnd, + uint32_t desc_idx, + uint64_t status_addr, + uint32_t stream_id, + uint32_t completion_status, + uint32_t latency_bytes) +{ + vsnd_buf_queue_node_t *node = calloc(1, sizeof(*node)); + if (!node) { + rv_log_error("virtio-snd: cannot allocate TX completion node"); + virtio_snd_set_fail(vsnd); + return false; + } + + node->desc_idx = desc_idx; + node->status_addr = status_addr; + node->stream_id = stream_id; + node->completion_status = completion_status; + node->latency_bytes = latency_bytes; + + vsnd_completed_push(vsnd, node); + return true; +} + +static vsnd_buf_queue_node_t *vsnd_completed_pop(virtio_snd_state_t *vsnd) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + + pthread_mutex_lock(&p->completed_mutex); + + if (list_empty(&p->completed_queue)) { + pthread_mutex_unlock(&p->completed_mutex); + return NULL; + } + + vsnd_buf_queue_node_t *node = + list_last_entry(&p->completed_queue, vsnd_buf_queue_node_t, q); + list_del(&node->q); + pthread_mutex_unlock(&p->completed_mutex); + return node; +} + +static void vsnd_finish_node_locked(virtio_snd_prop_t *props, + vsnd_buf_queue_node_t *node, + uint32_t status, + uint32_t latency_bytes) +{ + node->completion_status = status; + node->latency_bytes = latency_bytes; + + if (props->lock.inflight_count > 0) + props->lock.inflight_count--; + + if (props->lock.inflight_count == 0) + pthread_cond_broadcast(&props->lock.all_completed); + + vsnd_completed_push(props->vsnd, node); +} + +static void vsnd_move_pending_to_completed_locked(virtio_snd_prop_t *props) +{ + while (!list_empty(&props->buf_queue)) { + vsnd_buf_queue_node_t *node = vsnd_queue_front(props); + vsnd_queue_remove(node); + + /* + * RELEASE/reset drains the remaining PCM immediately, so the + * reported queued latency for these requests is zero. + */ + vsnd_finish_node_locked(props, node, VIRTIO_SND_S_OK, 0); + } + props->lock.pending_pcm_bytes = 0; +} + +static void vsnd_clear_all_nodes_locked(virtio_snd_prop_t *props) +{ + while (!list_empty(&props->buf_queue)) { + vsnd_buf_queue_node_t *node = vsnd_queue_front(props); + list_del(&node->q); + vsnd_free_node(node); + } + + props->lock.pending_pcm_bytes = 0; + props->lock.inflight_count = 0; +} + +static void virtio_snd_set_fail(virtio_snd_state_t *vsnd) +{ + uint32_t old_status = __atomic_fetch_or( + &vsnd->status, VIRTIO_STATUS_DEVICE_NEEDS_RESET, __ATOMIC_ACQ_REL); + + if (old_status & VIRTIO_STATUS_DRIVER_OK) + __atomic_fetch_or(&vsnd->interrupt_status, VIRTIO_INT_CONF_CHANGE, + __ATOMIC_RELEASE); + + rv_log_error("virtio-snd: DEVICE_NEEDS_RESET"); +} + +static bool vsnd_guest_range_ok(uint64_t addr, uint64_t len) +{ + if (addr >= MEM_SIZE) + return false; + if (len > MEM_SIZE - addr) + return false; + return true; +} + +static bool vsnd_word_range_ok(uint32_t word_addr, uint32_t words) +{ + uint64_t byte_addr = (uint64_t) word_addr << 2; + uint64_t byte_len = (uint64_t) words << 2; + return vsnd_guest_range_ok(byte_addr, byte_len); +} + +static bool vsnd_check_word_range(virtio_snd_state_t *vsnd, + uint32_t word_addr, + uint32_t words) +{ + if (!vsnd_word_range_ok(word_addr, words)) { + virtio_snd_set_fail(vsnd); + return false; + } + + return true; +} + +static inline uint32_t vsnd_preprocess(virtio_snd_state_t *vsnd, uint32_t addr) +{ +#if MEM_SIZE < 0x100000000ULL + if ((addr >= MEM_SIZE) || (addr & 0b11)) { +#else + if (addr & 0b11) { +#endif + virtio_snd_set_fail(vsnd); + return 0; + } + + return addr >> 2; +} + +static bool vsnd_close_stream(virtio_snd_prop_t *props) +{ + if (!props->pa_stream) + return true; + + PaError err = Pa_CloseStream(props->pa_stream); + if (err != paNoError) { + rv_log_error("PortAudio Pa_CloseStream failed: %s", + Pa_GetErrorText(err)); + return false; + } + + props->pa_stream = NULL; + return true; +} + +static void vsnd_reset_stream(virtio_snd_prop_t *props) +{ + pthread_mutex_lock(&props->lock.lock); + props->lock.releasing = true; + pthread_mutex_unlock(&props->lock.lock); + + if (props->pa_stream) { + int active = Pa_IsStreamActive(props->pa_stream); + + if (active < 0) { + rv_log_error("PortAudio Pa_IsStreamActive failed: %s", + Pa_GetErrorText(active)); + abort(); + } + + if (active == 1) { + PaError err = Pa_AbortStream(props->pa_stream); + if (err != paNoError) { + rv_log_error("PortAudio Pa_AbortStream failed: %s", + Pa_GetErrorText(err)); + abort(); + } + } + } + + /* + * Once Pa_AbortStream succeeds, the callback no longer owns the stream + * state. Move all remaining host-owned requests to completed_queue. + */ + vsnd_cancel_pending_and_wait(props, VSND_CANCEL_TX_QUIESCED); + + if (props->pa_stream && !vsnd_close_stream(props)) { + /* + * Do not destroy callback-owned state underneath a stream that + * PortAudio failed to close. + */ + abort(); + } + + pthread_mutex_lock(&props->lock.lock); + memset(&props->pp, 0, sizeof(props->pp)); + props->pp.hdr.hdr.code = VIRTIO_SND_R_PCM_SET_PARAMS; + props->lock.releasing = false; + pthread_mutex_unlock(&props->lock.lock); +} + +static void virtio_snd_update_status(virtio_snd_state_t *vsnd, uint32_t status) +{ + if (status) { + __atomic_fetch_or(&vsnd->status, status, __ATOMIC_RELEASE); + return; + } + + uint32_t *ram = vsnd->ram; + void *priv = vsnd->priv; + + if (priv) { + virtio_snd_priv_t *p = (virtio_snd_priv_t *) priv; + pthread_mutex_lock(&p->tx_process_mutex); + pthread_mutex_lock(&p->tx_mutex); + p->tx_ev_notify = false; + pthread_mutex_unlock(&p->tx_mutex); + + for (uint32_t i = 0; i < VSND_DEV_CNT_MAX; i++) + vsnd_reset_stream(&p->props[i]); + + virtio_snd_refresh_queue(vsnd); + } + + memset(vsnd, 0, sizeof(*vsnd)); + vsnd->ram = ram; + vsnd->priv = priv; + + if (priv) { + virtio_snd_priv_t *p = (virtio_snd_priv_t *) priv; + pthread_mutex_unlock(&p->tx_process_mutex); + } +} + +static void *vsnd_guest_ptr(virtio_snd_state_t *vsnd, + uint64_t addr, + uint32_t len) +{ + if (!vsnd_guest_range_ok(addr, len)) { + virtio_snd_set_fail(vsnd); + return NULL; + } + + return (void *) ((uintptr_t) vsnd->ram + (uintptr_t) addr); +} + +static bool vsnd_guest_read(virtio_snd_state_t *vsnd, + uint64_t addr, + void *dst, + uint32_t len) +{ + void *src = vsnd_guest_ptr(vsnd, addr, len); + if (!src) + return false; + + memcpy(dst, src, len); + return true; +} + +static bool vsnd_guest_write(virtio_snd_state_t *vsnd, + uint64_t addr, + const void *src, + uint32_t len) +{ + void *dst = vsnd_guest_ptr(vsnd, addr, len); + if (!dst) + return false; + + memcpy(dst, src, len); + return true; +} + +// Only emulator thread can call this function. +static bool virtio_snd_complete_tx_request(virtio_snd_state_t *vsnd, + uint32_t desc_idx, + uint64_t status_addr, + uint32_t completion_status, + uint32_t latency_bytes) +{ + virtio_snd_queue_t *queue = &vsnd->queues[VSND_QUEUE_TX]; + uint32_t *ram = vsnd->ram; + + if (!queue->ready || !queue->queue_num || + queue->queue_num > VSND_QUEUE_NUM_MAX) + return false; + + if (!vsnd_check_word_range(vsnd, queue->queue_used, 1) || + !vsnd_check_word_range(vsnd, queue->queue_avail, 1)) + return false; + + uint16_t used_idx = ram[queue->queue_used] >> 16; + uint32_t used_addr = + queue->queue_used + 1 + (used_idx % queue->queue_num) * 2; + + if (!vsnd_check_word_range(vsnd, used_addr, 2)) + return false; + + virtio_snd_pcm_status_t status = { + .status = completion_status, + .latency_bytes = latency_bytes, + }; + + if (!vsnd_guest_write(vsnd, status_addr, &status, sizeof(status))) + return false; + + ram[used_addr] = desc_idx; + ram[used_addr + 1] = sizeof(status); + + used_idx++; + uint32_t used_word = + (ram[queue->queue_used] & MASK(16)) | ((uint32_t) used_idx << 16); + + __atomic_store_n(&ram[queue->queue_used], used_word, __ATOMIC_RELEASE); + + if (!(ram[queue->queue_avail] & 1)) + __atomic_fetch_or(&vsnd->interrupt_status, VIRTIO_INT_USED_RING, + __ATOMIC_RELEASE); + + return true; +} + +static void vsnd_cancel_pending_and_wait(virtio_snd_prop_t *props, + vsnd_cancel_mode_t mode) +{ + pthread_mutex_lock(&props->lock.lock); + props->lock.releasing = true; + pthread_mutex_unlock(&props->lock.lock); + + /* + * Stream control operations keep the same virtqueue alive, so drain + * descriptors already published by the guest. Device reset instead + * quiesces TX processing before entering this function and discards + * descriptors that were never taken from the old avail ring. + */ + if (mode == VSND_CANCEL_DRAIN_TX && props->vsnd) + virtio_snd_process_tx_queue(props->vsnd); + + pthread_mutex_lock(&props->lock.lock); + vsnd_move_pending_to_completed_locked(props); + + while (props->lock.inflight_count > 0) + pthread_cond_wait(&props->lock.all_completed, &props->lock.lock); + pthread_mutex_unlock(&props->lock.lock); +} + + +static bool vsnd_collect_desc_chain(virtio_snd_state_t *vsnd, + virtio_snd_queue_t *queue, + uint32_t desc_idx, + struct virtq_desc *chain, + uint32_t *chain_cnt) +{ + if (!queue->queue_num || queue->queue_num > VSND_QUEUE_NUM_MAX) + return false; + + uint32_t cnt = 0; + + for (;;) { + if (desc_idx >= queue->queue_num) + return false; + + if (cnt >= queue->queue_num) + return false; + + uint32_t desc_word = queue->queue_desc + desc_idx * 4; + if (!vsnd_check_word_range(vsnd, desc_word, 4)) + return false; + struct virtq_desc desc; + memcpy(&desc, &vsnd->ram[desc_word], sizeof(desc)); + + if (!vsnd_guest_range_ok(desc.addr, desc.len)) + return false; + chain[cnt] = desc; + cnt++; + if (!(desc.flags & VIRTIO_DESC_F_NEXT)) + break; + desc_idx = desc.next; + } + + *chain_cnt = cnt; + return true; +} + +static void virtio_snd_read_jack_info_handler( + virtio_snd_state_t *vsnd, + virtio_snd_jack_info_t *info, + const virtio_snd_query_info_t *query, + uint32_t *payload_len) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + uint32_t cnt = query->count; + uint32_t written = 0; + + for (uint32_t i = 0; i < cnt; i++) { + uint32_t id = query->start_id + i; + if (id >= VSND_DEV_CNT_MAX) + break; + + info[i].hdr.hda_fn_nid = 0; + info[i].features = 0; + info[i].hda_reg_defconf = 0; + info[i].hda_reg_caps = 0; + info[i].connected = 1; + memset(info[i].padding, 0, sizeof(info[i].padding)); + + p->props[id].j = info[i]; + written++; + } + + *payload_len = written * sizeof(*info); +} + +static void virtio_snd_read_pcm_info_handler( + virtio_snd_state_t *vsnd, + virtio_snd_pcm_info_t *info, + const virtio_snd_query_info_t *query, + uint32_t *payload_len) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + uint32_t cnt = query->count; + uint32_t written = 0; + + for (uint32_t i = 0; i < cnt; i++) { + uint32_t id = query->start_id + i; + if (id >= VSND_DEV_CNT_MAX) + break; + + info[i].hdr.hda_fn_nid = 0; + info[i].features = 0; + info[i].formats = 1ULL << VIRTIO_SND_PCM_FMT_S16; + info[i].rates = 0; + +#define _(rate) info[i].rates |= 1ULL << VIRTIO_SND_PCM_RATE_##rate; + SND_PCM_RATE +#undef _ + + info[i].direction = VIRTIO_SND_D_OUTPUT; + info[i].channels_min = 1; + info[i].channels_max = 1; + memset(info[i].padding, 0, sizeof(info[i].padding)); + + p->props[id].p = info[i]; + written++; + } + + *payload_len = written * sizeof(*info); +} + +static void virtio_snd_read_chmap_info_handler( + virtio_snd_state_t *vsnd, + virtio_snd_chmap_info_t *info, + const virtio_snd_query_info_t *query, + uint32_t *payload_len) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + uint32_t cnt = query->count; + uint32_t written = 0; + + for (uint32_t i = 0; i < cnt; i++) { + uint32_t id = query->start_id + i; + if (id >= VSND_DEV_CNT_MAX) + break; + + memset(&info[i], 0, sizeof(info[i])); + info[i].hdr.hda_fn_nid = 0; + info[i].direction = VIRTIO_SND_D_OUTPUT; + info[i].channels = 1; + info[i].positions[0] = VIRTIO_SND_CHMAP_MONO; + + p->props[id].c = info[i]; + written++; + } + + *payload_len = written * sizeof(*info); +} + +static uint32_t virtio_snd_pcm_set_params( + virtio_snd_state_t *vsnd, + const virtio_snd_pcm_set_params_t *request, + uint32_t *payload_len) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + uint32_t id = request->hdr.stream_id; + + if (id >= VSND_DEV_CNT_MAX) + return VIRTIO_SND_S_BAD_MSG; + + if (request->channels != 1) + return VIRTIO_SND_S_NOT_SUPP; + + if (request->format != VIRTIO_SND_PCM_FMT_S16) + return VIRTIO_SND_S_NOT_SUPP; + + if (!request->buffer_bytes || !request->period_bytes || + request->period_bytes > request->buffer_bytes || + request->buffer_bytes > VSND_PCM_BUFFER_BYTES_MAX) + return VIRTIO_SND_S_BAD_MSG; + + if (request->rate >= VIRTIO_SND_PCM_RATE_COUNT || + pcm_rate_tbl[request->rate] == 0) + return VIRTIO_SND_S_NOT_SUPP; + + virtio_snd_prop_t *props = &p->props[id]; + + pthread_mutex_lock(&props->lock.lock); + uint32_t code = props->pp.hdr.hdr.code; + if (code != VIRTIO_SND_R_PCM_RELEASE && + code != VIRTIO_SND_R_PCM_SET_PARAMS && + code != VIRTIO_SND_R_PCM_PREPARE) { + pthread_mutex_unlock(&props->lock.lock); + return VIRTIO_SND_S_BAD_MSG; + } + if (props->lock.inflight_count != 0) { + pthread_mutex_unlock(&props->lock.lock); + return VIRTIO_SND_S_BAD_MSG; + } + + props->pp = *request; + props->pp.hdr.hdr.code = VIRTIO_SND_R_PCM_SET_PARAMS; + pthread_mutex_unlock(&props->lock.lock); + + *payload_len = 0; + return VIRTIO_SND_S_OK; +} + +static uint32_t virtio_snd_pcm_prepare(virtio_snd_state_t *vsnd, + const virtio_snd_pcm_hdr_t *request, + uint32_t *payload_len) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + uint32_t stream_id = request->stream_id; + if (stream_id >= VSND_DEV_CNT_MAX) + return VIRTIO_SND_S_BAD_MSG; + + virtio_snd_prop_t *props = &p->props[stream_id]; + pthread_mutex_lock(&props->lock.lock); + uint32_t code = props->pp.hdr.hdr.code; + pthread_mutex_unlock(&props->lock.lock); + + if (code != VIRTIO_SND_R_PCM_RELEASE && + code != VIRTIO_SND_R_PCM_SET_PARAMS && code != VIRTIO_SND_R_PCM_PREPARE) + return VIRTIO_SND_S_BAD_MSG; + if (props->pp.channels != 1 || props->pp.format != VIRTIO_SND_PCM_FMT_S16 || + props->pp.rate >= VIRTIO_SND_PCM_RATE_COUNT || + pcm_rate_tbl[props->pp.rate] == 0) + return VIRTIO_SND_S_BAD_MSG; + + pthread_mutex_lock(&props->lock.lock); + props->lock.releasing = true; + pthread_mutex_unlock(&props->lock.lock); + + if (props->pa_stream && Pa_IsStreamActive(props->pa_stream) == 1) + Pa_AbortStream(props->pa_stream); + + vsnd_cancel_pending_and_wait(props, VSND_CANCEL_DRAIN_TX); + + if (props->pa_stream && !vsnd_close_stream(props)) + goto prepare_failed; + props->v.stream_id = stream_id; + + uint32_t channels = props->pp.channels; + uint32_t rate = (uint32_t) pcm_rate_tbl[props->pp.rate]; + uint32_t frame_bytes = channels * VSND_CNFA_FRAME_SZ; + uint32_t period_frames = props->pp.period_bytes / frame_bytes; + + if (!period_frames) + period_frames = 1; + + PaStreamParameters params = { + .device = Pa_GetDefaultOutputDevice(), + .channelCount = (int) channels, + .sampleFormat = paInt16, + .suggestedLatency = (double) period_frames / rate, + .hostApiSpecificStreamInfo = NULL, + }; + + if (params.device == paNoDevice) + goto prepare_failed; + + PaError err = + Pa_OpenStream(&props->pa_stream, NULL, ¶ms, rate, period_frames, + paClipOff, virtio_snd_stream_cb, &props->v); + if (err != paNoError) { + rv_log_error("PortAudio Pa_OpenStream failed: %s", + Pa_GetErrorText(err)); + props->pa_stream = NULL; + goto prepare_failed; + } + + pthread_mutex_lock(&props->lock.lock); + props->lock.releasing = false; + props->pp.hdr.hdr.code = VIRTIO_SND_R_PCM_PREPARE; + pthread_mutex_unlock(&props->lock.lock); + *payload_len = 0; + return VIRTIO_SND_S_OK; + +prepare_failed: + pthread_mutex_lock(&props->lock.lock); + props->lock.releasing = false; + props->pp.hdr.hdr.code = VIRTIO_SND_R_PCM_SET_PARAMS; + pthread_mutex_unlock(&props->lock.lock); + return VIRTIO_SND_S_IO_ERR; +} + +static uint32_t virtio_snd_pcm_start(virtio_snd_state_t *vsnd, + const virtio_snd_pcm_hdr_t *request, + uint32_t *payload_len) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + uint32_t stream_id = request->stream_id; + if (stream_id >= VSND_DEV_CNT_MAX) + return VIRTIO_SND_S_BAD_MSG; + + virtio_snd_prop_t *props = &p->props[stream_id]; + + pthread_mutex_lock(&props->lock.lock); + uint32_t code = props->pp.hdr.hdr.code; + if (code == VIRTIO_SND_R_PCM_PREPARE || code == VIRTIO_SND_R_PCM_STOP) { + props->lock.releasing = false; + props->pp.hdr.hdr.code = VIRTIO_SND_R_PCM_START; + } + pthread_mutex_unlock(&props->lock.lock); + + if (code != VIRTIO_SND_R_PCM_PREPARE && code != VIRTIO_SND_R_PCM_STOP) + return VIRTIO_SND_S_BAD_MSG; + if (!props->pa_stream) + return VIRTIO_SND_S_IO_ERR; + + int active = Pa_IsStreamActive(props->pa_stream); + if (active < 0) { + rv_log_error("virtio-snd: Pa_IsStreamActive failed: %s", + Pa_GetErrorText(active)); + pthread_mutex_lock(&props->lock.lock); + props->pp.hdr.hdr.code = code; + pthread_mutex_unlock(&props->lock.lock); + return VIRTIO_SND_S_IO_ERR; + } + + if (active == 0) { + PaError err = Pa_StartStream(props->pa_stream); + if (err != paNoError) { + rv_log_error("virtio-snd: Pa_StartStream failed: %s", + Pa_GetErrorText(err)); + pthread_mutex_lock(&props->lock.lock); + props->pp.hdr.hdr.code = code; + pthread_mutex_unlock(&props->lock.lock); + return VIRTIO_SND_S_IO_ERR; + } + } + + *payload_len = 0; + return VIRTIO_SND_S_OK; +} + +static uint32_t virtio_snd_pcm_stop(virtio_snd_state_t *vsnd, + const virtio_snd_pcm_hdr_t *request, + uint32_t *payload_len) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + uint32_t stream_id = request->stream_id; + if (stream_id >= VSND_DEV_CNT_MAX) + return VIRTIO_SND_S_BAD_MSG; + virtio_snd_prop_t *props = &p->props[stream_id]; + + pthread_mutex_lock(&props->lock.lock); + uint32_t code = props->pp.hdr.hdr.code; + if (code == VIRTIO_SND_R_PCM_START) + props->pp.hdr.hdr.code = VIRTIO_SND_R_PCM_STOP; + pthread_mutex_unlock(&props->lock.lock); + if (code != VIRTIO_SND_R_PCM_START) + return VIRTIO_SND_S_BAD_MSG; + if (!props->pa_stream) + return VIRTIO_SND_S_IO_ERR; + + PaError err = paNoError; + int active = Pa_IsStreamActive(props->pa_stream); + if (active < 0) + err = (PaError) active; + else if (active == 1) + err = Pa_StopStream(props->pa_stream); + if (err != paNoError) { + rv_log_error("PortAudio Pa_StopStream failed: %s", + Pa_GetErrorText(err)); + return VIRTIO_SND_S_IO_ERR; + } + *payload_len = 0; + return VIRTIO_SND_S_OK; +} + +static uint32_t virtio_snd_pcm_release(virtio_snd_state_t *vsnd, + const virtio_snd_pcm_hdr_t *request, + uint32_t *payload_len) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + uint32_t stream_id = request->stream_id; + + if (stream_id >= VSND_DEV_CNT_MAX) + return VIRTIO_SND_S_BAD_MSG; + + virtio_snd_prop_t *props = &p->props[stream_id]; + pthread_mutex_lock(&props->lock.lock); + uint32_t code = props->pp.hdr.hdr.code; + if (code != VIRTIO_SND_R_PCM_PREPARE && code != VIRTIO_SND_R_PCM_STOP && + code != VIRTIO_SND_R_PCM_START) { + pthread_mutex_unlock(&props->lock.lock); + return VIRTIO_SND_S_BAD_MSG; + } + props->lock.releasing = true; + pthread_mutex_unlock(&props->lock.lock); + + if (props->pa_stream) { + int active = Pa_IsStreamActive(props->pa_stream); + if (active < 0) { + rv_log_error("PortAudio Pa_IsStreamActive failed: %s", + Pa_GetErrorText(active)); + return VIRTIO_SND_S_IO_ERR; + } + + if (active == 1) { + PaError err = Pa_AbortStream(props->pa_stream); + if (err != paNoError) { + rv_log_error("PortAudio Pa_AbortStream failed: %s", + Pa_GetErrorText(err)); + return VIRTIO_SND_S_IO_ERR; + } + } + } + + vsnd_cancel_pending_and_wait(props, VSND_CANCEL_DRAIN_TX); + + /* + * RELEASE must not complete until all pending PCM I/O messages have + * been returned to the used ring. This function runs on the emulator + * thread, so publishing here preserves single-threaded guest RAM access. + */ + virtio_snd_refresh_queue(vsnd); + + if (!vsnd_close_stream(props)) + return VIRTIO_SND_S_IO_ERR; + + pthread_mutex_lock(&props->lock.lock); + props->pp.hdr.hdr.code = VIRTIO_SND_R_PCM_RELEASE; + props->lock.releasing = false; + pthread_mutex_unlock(&props->lock.lock); + + *payload_len = 0; + return VIRTIO_SND_S_OK; +} + +static uint32_t vsnd_queue_read_locked(virtio_snd_prop_t *props, + void *dst, + uint32_t bytes) +{ + uint8_t *out = (uint8_t *) dst; + uint32_t copied = 0; + while (!list_empty(&props->buf_queue) && copied < bytes) { + vsnd_buf_queue_node_t *node = vsnd_queue_front(props); + uint32_t left = bytes - copied; + uint32_t available = node->len - node->pos; + uint32_t n = vsnd_min(left, available); + memcpy(out + copied, node->addr + node->pos, n); + copied += n; + node->pos += n; + if (props->lock.pending_pcm_bytes >= n) + props->lock.pending_pcm_bytes -= n; + else + props->lock.pending_pcm_bytes = 0; + if (node->pos >= node->len) { + vsnd_queue_remove(node); + uint32_t latency = props->lock.pending_pcm_bytes > UINT32_MAX + ? UINT32_MAX + : (uint32_t) props->lock.pending_pcm_bytes; + vsnd_finish_node_locked(props, node, VIRTIO_SND_S_OK, latency); + } + } + return copied; +} + +static int virtio_snd_stream_cb(const void *input, + void *output, + unsigned long frame_cnt, + const PaStreamCallbackTimeInfo *time_info, + PaStreamCallbackFlags status_flags, + void *user_data) +{ + (void) input; + (void) time_info; + (void) status_flags; + vsnd_stream_sel_t *sel = (vsnd_stream_sel_t *) user_data; + virtio_snd_prop_t *props = + (virtio_snd_prop_t *) ((uintptr_t) sel - + offsetof(virtio_snd_prop_t, v)); + + uint32_t channels = props->pp.channels ? props->pp.channels : 1U; + uint32_t out_bytes = (uint32_t) frame_cnt * channels * VSND_CNFA_FRAME_SZ; + uint8_t *out = (uint8_t *) output; + + /* + * Do not block the PortAudio callback on the PCM queue mutex. If another + * thread briefly owns the lock, output silence for this callback instead. + */ + if (pthread_mutex_trylock(&props->lock.lock) != 0) { + memset(output, 0, out_bytes); + return paContinue; + } + + if (props->lock.releasing || props->lock.pending_pcm_bytes == 0) { + pthread_mutex_unlock(&props->lock.lock); + memset(output, 0, out_bytes); + return paContinue; + } + + uint32_t written = vsnd_queue_read_locked(props, out, out_bytes); + if (written < out_bytes) + memset(out + written, 0, out_bytes - written); + pthread_mutex_unlock(&props->lock.lock); + return paContinue; +} + +static int virtio_snd_tx_desc_handler(virtio_snd_state_t *vsnd, + virtio_snd_queue_t *queue, + uint32_t desc_idx, + uint32_t *plen) +{ + struct virtq_desc chain[VSND_QUEUE_NUM_MAX]; + uint32_t cnt = 0; + *plen = 0; + if (!vsnd_collect_desc_chain(vsnd, queue, desc_idx, chain, &cnt)) + return -1; + if (cnt < 3) + return -1; + + struct virtq_desc *xfer_desc = &chain[0]; + struct virtq_desc *status_desc = &chain[cnt - 1]; + if (xfer_desc->flags & VIRTIO_DESC_F_WRITE) + return -1; + if (!(status_desc->flags & VIRTIO_DESC_F_WRITE)) + return -1; + if (xfer_desc->len < sizeof(virtio_snd_pcm_xfer_t) || + status_desc->len < sizeof(virtio_snd_pcm_status_t)) + return -1; + + virtio_snd_pcm_xfer_t xfer; + if (!vsnd_guest_read(vsnd, xfer_desc->addr, &xfer, sizeof(xfer))) + return -1; + if (!vsnd_guest_range_ok(status_desc->addr, + sizeof(virtio_snd_pcm_status_t))) + return -1; + + uint64_t total64 = 0; + for (uint32_t i = 1; i + 1 < cnt; i++) { + if (chain[i].flags & VIRTIO_DESC_F_WRITE) + return -1; + total64 += chain[i].len; + if (total64 > VSND_PCM_BUFFER_BYTES_MAX || + !vsnd_guest_range_ok(chain[i].addr, chain[i].len)) + return -1; + } + + uint32_t stream_id = xfer.stream_id; + + if (stream_id >= VSND_DEV_CNT_MAX) { + if (!vsnd_queue_completion(vsnd, desc_idx, status_desc->addr, stream_id, + VIRTIO_SND_S_IO_ERR, 0)) + return -1; + return 0; + } + + virtio_snd_priv_t *p = vsnd_priv(vsnd); + virtio_snd_prop_t *props = &p->props[stream_id]; + pthread_mutex_lock(&props->lock.lock); + bool accepting = !props->lock.releasing && + vsnd_stream_accepts_tx(props->pp.hdr.hdr.code); + uint32_t period_bytes = props->pp.period_bytes; + uint32_t buffer_bytes = props->pp.buffer_bytes; + + bool queue_has_room = + total64 <= buffer_bytes && props->lock.pending_pcm_bytes <= + (size_t) buffer_bytes - (size_t) total64; + + pthread_mutex_unlock(&props->lock.lock); + + if (!accepting || total64 == 0) { + if (!vsnd_queue_completion(vsnd, desc_idx, status_desc->addr, stream_id, + VIRTIO_SND_S_OK, 0)) + return -1; + return 0; + } + + if (total64 > period_bytes) { + if (!vsnd_queue_completion(vsnd, desc_idx, status_desc->addr, stream_id, + VIRTIO_SND_S_BAD_MSG, 0)) + return -1; + return 0; + } + + if (!queue_has_room) { + if (!vsnd_queue_completion(vsnd, desc_idx, status_desc->addr, stream_id, + VIRTIO_SND_S_IO_ERR, 0)) + return -1; + return 0; + } + + vsnd_buf_queue_node_t *node = calloc(1, sizeof(*node)); + if (!node) { + rv_log_error("virtio-snd: cannot allocate TX request node"); + virtio_snd_set_fail(vsnd); + return -1; + } + node->addr = malloc((size_t) total64); + + if (!node->addr) { + rv_log_error("virtio-snd: cannot allocate %" PRIu64 + " bytes for TX request", + total64); + node->desc_idx = desc_idx; + node->status_addr = status_desc->addr; + node->stream_id = stream_id; + node->completion_status = VIRTIO_SND_S_IO_ERR; + node->latency_bytes = 0; + vsnd_completed_push(vsnd, node); + return 0; + } + + uint32_t offset = 0; + for (uint32_t i = 1; i + 1 < cnt; i++) { + void *payload = vsnd_guest_ptr(vsnd, chain[i].addr, chain[i].len); + if (!payload) { + vsnd_free_node(node); + return -1; + } + memcpy(node->addr + offset, payload, chain[i].len); + offset += chain[i].len; + } + + node->len = (uint32_t) total64; + node->desc_idx = desc_idx; + node->status_addr = status_desc->addr; + node->stream_id = stream_id; + node->completion_status = VIRTIO_SND_S_OK; + + pthread_mutex_lock(&props->lock.lock); + props->lock.inflight_count++; + accepting = !props->lock.releasing && + vsnd_stream_accepts_tx(props->pp.hdr.hdr.code); + + queue_has_room = node->len <= props->pp.buffer_bytes && + props->lock.pending_pcm_bytes <= + (size_t) props->pp.buffer_bytes - node->len; + + if (accepting && queue_has_room) { + vsnd_queue_push(props, node); + props->lock.pending_pcm_bytes += node->len; + } else { + uint32_t status = accepting ? VIRTIO_SND_S_IO_ERR : VIRTIO_SND_S_OK; + vsnd_finish_node_locked(props, node, status, 0); + } + pthread_mutex_unlock(&props->lock.lock); + + return 0; +} + +static uint32_t virtio_snd_ctrl_process(virtio_snd_state_t *vsnd, + const void *query, + void *info, + uint32_t type, + uint32_t info_len, + uint32_t *payload_len) +{ + *payload_len = 0; + + switch (type) { + case VIRTIO_SND_R_JACK_INFO: { + const virtio_snd_query_info_t *q = query; + uint64_t need = (uint64_t) q->count * sizeof(virtio_snd_jack_info_t); + if (info_len < need) + return VIRTIO_SND_S_BAD_MSG; + virtio_snd_read_jack_info_handler(vsnd, info, q, payload_len); + return VIRTIO_SND_S_OK; + } + case VIRTIO_SND_R_PCM_INFO: { + const virtio_snd_query_info_t *q = query; + uint64_t need = (uint64_t) q->count * sizeof(virtio_snd_pcm_info_t); + if (info_len < need) + return VIRTIO_SND_S_BAD_MSG; + virtio_snd_read_pcm_info_handler(vsnd, info, q, payload_len); + return VIRTIO_SND_S_OK; + } + case VIRTIO_SND_R_CHMAP_INFO: { + const virtio_snd_query_info_t *q = query; + uint64_t need = (uint64_t) q->count * sizeof(virtio_snd_chmap_info_t); + if (info_len < need) + return VIRTIO_SND_S_BAD_MSG; + virtio_snd_read_chmap_info_handler(vsnd, info, q, payload_len); + return VIRTIO_SND_S_OK; + } + case VIRTIO_SND_R_PCM_SET_PARAMS: + return virtio_snd_pcm_set_params(vsnd, query, payload_len); + case VIRTIO_SND_R_PCM_PREPARE: + return virtio_snd_pcm_prepare(vsnd, query, payload_len); + case VIRTIO_SND_R_PCM_RELEASE: + return virtio_snd_pcm_release(vsnd, query, payload_len); + case VIRTIO_SND_R_PCM_START: + return virtio_snd_pcm_start(vsnd, query, payload_len); + case VIRTIO_SND_R_PCM_STOP: + return virtio_snd_pcm_stop(vsnd, query, payload_len); + default: + return VIRTIO_SND_S_NOT_SUPP; + } +} + +static int virtio_snd_ctrl_desc_handler(virtio_snd_state_t *vsnd, + virtio_snd_queue_t *queue, + uint32_t desc_idx, + uint32_t *plen) +{ + struct virtq_desc chain[VSND_QUEUE_NUM_MAX]; + uint32_t cnt = 0; + + if (!vsnd_collect_desc_chain(vsnd, queue, desc_idx, chain, &cnt)) + return -1; + + if (cnt < 2) + return -1; + + struct virtq_desc *request_desc = &chain[0]; + struct virtq_desc *response_desc = &chain[1]; + + if (request_desc->flags & VIRTIO_DESC_F_WRITE) + return -1; + + if (!(response_desc->flags & VIRTIO_DESC_F_WRITE)) + return -1; + + if (request_desc->len < sizeof(virtio_snd_hdr_t)) + return -1; + + if (response_desc->len < sizeof(virtio_snd_hdr_t)) + return -1; + + vsnd_ctrl_request_t request; + memset(&request, 0, sizeof(request)); + + if (!vsnd_guest_read(vsnd, request_desc->addr, &request.hdr, + sizeof(request.hdr))) + return -1; + + uint32_t type = request.hdr.code; + uint32_t request_size = sizeof(virtio_snd_hdr_t); + + switch (type) { + case VIRTIO_SND_R_JACK_INFO: + case VIRTIO_SND_R_PCM_INFO: + case VIRTIO_SND_R_CHMAP_INFO: + request_size = sizeof(virtio_snd_query_info_t); + break; + case VIRTIO_SND_R_PCM_SET_PARAMS: + request_size = sizeof(virtio_snd_pcm_set_params_t); + break; + case VIRTIO_SND_R_PCM_PREPARE: + case VIRTIO_SND_R_PCM_RELEASE: + case VIRTIO_SND_R_PCM_START: + case VIRTIO_SND_R_PCM_STOP: + request_size = sizeof(virtio_snd_pcm_hdr_t); + break; + default: + break; + } + + if (request_desc->len < request_size) + return -1; + + if (!vsnd_guest_read(vsnd, request_desc->addr, &request, request_size)) + return -1; + + /* + * INFO responses are also built in aligned host storage, then copied + * back to guest memory after the control request has been processed. + */ + vsnd_ctrl_info_t info_buf; + memset(&info_buf, 0, sizeof(info_buf)); + + struct virtq_desc *info_desc = NULL; + void *info = NULL; + uint32_t info_len = 0; + + if (cnt >= 3) { + info_desc = &chain[2]; + if (!(info_desc->flags & VIRTIO_DESC_F_WRITE)) + return -1; + if (!vsnd_guest_range_ok(info_desc->addr, info_desc->len)) + return -1; + info = &info_buf; + info_len = info_desc->len; + } + + switch (type) { + case VIRTIO_SND_R_JACK_INFO: + case VIRTIO_SND_R_PCM_INFO: + case VIRTIO_SND_R_CHMAP_INFO: + if (!info) + return -1; + break; + default: + break; + } + + uint32_t payload_len = 0; + uint32_t status = virtio_snd_ctrl_process(vsnd, &request, info, type, + info_len, &payload_len); + + /* + * Copy INFO payload produced by the handler back into guest memory. + */ + if (payload_len) { + if (!info_desc || payload_len > info_desc->len || + payload_len > sizeof(info_buf)) + return -1; + + if (!vsnd_guest_write(vsnd, info_desc->addr, &info_buf, payload_len)) + return -1; + } + + /* + * Build the control response locally as well, so the guest-provided + * response address does not need to satisfy host struct alignment. + */ + virtio_snd_hdr_t response = { + .code = status, + }; + + if (!vsnd_guest_write(vsnd, response_desc->addr, &response, + sizeof(response))) + return -1; + + *plen = sizeof(response) + payload_len; + + if (status != VIRTIO_SND_S_OK) + rv_log_error("virtio-snd: CTRL type=0x%x status=0x%x", type, status); + + return 0; +} + +static vsnd_virtq_cb virtio_snd_queue_op(int index) +{ + switch (index) { + case VSND_QUEUE_CTRL: + return virtio_snd_ctrl_desc_handler; + case VSND_QUEUE_TX: + return virtio_snd_tx_desc_handler; + default: + return NULL; + } +} + +static void virtio_queue_notify_handler(virtio_snd_state_t *vsnd, int index) +{ + uint32_t *ram = vsnd->ram; + virtio_snd_queue_t *queue = &vsnd->queues[index]; + bool async_tx = index == VSND_QUEUE_TX; + uint32_t status = __atomic_load_n(&vsnd->status, __ATOMIC_ACQUIRE); + if (status & VIRTIO_STATUS_DEVICE_NEEDS_RESET) + return; + if (!((status & VIRTIO_STATUS_DRIVER_OK) && queue->ready)) { + if (async_tx) + return; + virtio_snd_set_fail(vsnd); + return; + } + if (!queue->queue_num || queue->queue_num > VSND_QUEUE_NUM_MAX) { + virtio_snd_set_fail(vsnd); + return; + } + if (!vsnd_check_word_range(vsnd, queue->queue_avail, 1) || + !vsnd_check_word_range(vsnd, queue->queue_used, 1)) + return; + + uint16_t new_avail = ram[queue->queue_avail] >> 16; + uint16_t pending = new_avail - queue->last_avail; + + if (pending > (uint16_t) queue->queue_num) { + virtio_snd_set_fail(vsnd); + return; + } + if (queue->last_avail == new_avail) + return; + + uint16_t new_used = async_tx ? 0 : ram[queue->queue_used] >> 16; + while (queue->last_avail != new_avail) { + uint16_t queue_idx = queue->last_avail % queue->queue_num; + uint32_t avail_word = queue->queue_avail + 1 + queue_idx / 2; + if (!vsnd_check_word_range(vsnd, avail_word, 1)) + return; + uint16_t buffer_idx = ram[avail_word] >> (16 * (queue_idx % 2)); + if (buffer_idx >= queue->queue_num) { + virtio_snd_set_fail(vsnd); + return; + } + + uint32_t len = 0; + vsnd_virtq_cb handler = virtio_snd_queue_op(index); + if (!handler || handler(vsnd, queue, buffer_idx, &len) != 0) { + virtio_snd_set_fail(vsnd); + return; + } + if (async_tx) { + queue->last_avail++; + continue; + } + + uint32_t used_addr = + queue->queue_used + 1 + (new_used % queue->queue_num) * 2; + if (!vsnd_check_word_range(vsnd, used_addr, 2)) + return; + + ram[used_addr] = buffer_idx; + ram[used_addr + 1] = len; + queue->last_avail++; + new_used++; + } + + if (async_tx) + return; + uint32_t used_word = + (ram[queue->queue_used] & MASK(16)) | ((uint32_t) new_used << 16); + __atomic_store_n(&ram[queue->queue_used], used_word, __ATOMIC_RELEASE); + if (!(ram[queue->queue_avail] & 1)) + __atomic_fetch_or(&vsnd->interrupt_status, VIRTIO_INT_USED_RING, + __ATOMIC_RELEASE); +} + +static void virtio_snd_process_tx_queue(virtio_snd_state_t *vsnd) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + pthread_mutex_lock(&p->tx_process_mutex); + virtio_queue_notify_handler(vsnd, VSND_QUEUE_TX); + pthread_mutex_unlock(&p->tx_process_mutex); +} + +static void *virtio_snd_tx_thread(void *arg) +{ + virtio_snd_state_t *vsnd = (virtio_snd_state_t *) arg; + virtio_snd_priv_t *p = vsnd_priv(vsnd); + + for (;;) { + pthread_mutex_lock(&p->tx_mutex); + + while (!p->tx_ev_notify && !p->tx_thread_stop) + pthread_cond_wait(&p->tx_cond, &p->tx_mutex); + + if (p->tx_thread_stop) { + pthread_mutex_unlock(&p->tx_mutex); + break; + } + + p->tx_ev_notify = false; + pthread_mutex_unlock(&p->tx_mutex); + virtio_snd_process_tx_queue(vsnd); + } + + return NULL; +} + +static uint32_t virtio_snd_config_read(virtio_snd_state_t *vsnd, + uint32_t word_offset) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + uint32_t *cfg = (uint32_t *) &p->config; + + if (word_offset >= sizeof(p->config) / sizeof(uint32_t)) { + virtio_snd_set_fail(vsnd); + return 0; + } + + return cfg[word_offset]; +} + +static void virtio_snd_config_write(virtio_snd_state_t *vsnd, + uint32_t word_offset, + uint32_t value) +{ + virtio_snd_priv_t *p = vsnd_priv(vsnd); + + (void) value; + + if (word_offset >= sizeof(p->config) / sizeof(uint32_t)) { + virtio_snd_set_fail(vsnd); + return; + } + + /* The VirtIO sound device-specific configuration is read-only. */ +} + +uint32_t virtio_snd_read(virtio_snd_state_t *vsnd, uint32_t addr) +{ + addr = addr >> 2; + +#define _(reg) VIRTIO_##reg + switch (addr) { + case _(MagicValue): + return VIRTIO_MAGIC_NUMBER; + case _(Version): + return VIRTIO_VERSION; + case _(DeviceID): + return VIRTIO_SND_DEV_ID; + case _(VendorID): + return VIRTIO_VENDOR_ID; + case _(DeviceFeatures): + return vsnd->device_features_sel == 0 + ? VSND_FEATURES_0 | vsnd->device_features + : (vsnd->device_features_sel == 1 ? VSND_FEATURES_1 : 0); + case _(QueueNumMax): + return VSND_QUEUE_NUM_MAX; + case _(QueueReady): + return (uint32_t) VSND_QUEUE.ready; + case _(InterruptStatus): + return __atomic_load_n(&vsnd->interrupt_status, __ATOMIC_ACQUIRE); + case _(Status): + return __atomic_load_n(&vsnd->status, __ATOMIC_ACQUIRE); + case _(ConfigGeneration): + return VIRTIO_CONFIG_GENERATE; + default: + if (addr >= _(Config)) + return virtio_snd_config_read(vsnd, addr - _(Config)); + return 0; + } +#undef _ +} + +static bool vsnd_queue_configurable(virtio_snd_state_t *vsnd) +{ + if (!VSND_QUEUE.ready) + return true; + + virtio_snd_set_fail(vsnd); + return false; +} + +void virtio_snd_write(virtio_snd_state_t *vsnd, uint32_t addr, uint32_t value) +{ + addr = addr >> 2; + +#define _(reg) VIRTIO_##reg + switch (addr) { + case _(DeviceFeaturesSel): + vsnd->device_features_sel = value; + break; + case _(DriverFeatures): + if (vsnd->driver_features_sel == 0) + vsnd->driver_features = value; + break; + case _(DriverFeaturesSel): + vsnd->driver_features_sel = value; + break; + case _(QueueSel): + if (value < ARRAY_SIZE(vsnd->queues)) + vsnd->queue_sel = value; + else + virtio_snd_set_fail(vsnd); + break; + case _(QueueNum): + if (!vsnd_queue_configurable(vsnd)) + break; + + if (value > 0 && value <= VSND_QUEUE_NUM_MAX) + VSND_QUEUE.queue_num = value; + else + virtio_snd_set_fail(vsnd); + break; + case _(QueueReady): + if (value & ~1U) { + virtio_snd_set_fail(vsnd); + break; + } + + if (VSND_QUEUE.ready) { + if (!value) + virtio_snd_set_fail(vsnd); + break; + } + + if (!value) + break; + + if (!VSND_QUEUE.queue_num || + !vsnd_check_word_range(vsnd, VSND_QUEUE.queue_avail, 1) || + !vsnd_check_word_range(vsnd, VSND_QUEUE.queue_used, 1)) + break; + + VSND_QUEUE.ready = true; + VSND_QUEUE.last_avail = vsnd->ram[VSND_QUEUE.queue_avail] >> 16; + break; + case _(QueueDescLow): + if (!vsnd_queue_configurable(vsnd)) + break; + + VSND_QUEUE.queue_desc = vsnd_preprocess(vsnd, value); + break; + case _(QueueDescHigh): + if (!vsnd_queue_configurable(vsnd)) + break; + + if (value) + virtio_snd_set_fail(vsnd); + break; + case _(QueueDriverLow): + if (!vsnd_queue_configurable(vsnd)) + break; + + VSND_QUEUE.queue_avail = vsnd_preprocess(vsnd, value); + break; + case _(QueueDriverHigh): + if (!vsnd_queue_configurable(vsnd)) + break; + + if (value) + virtio_snd_set_fail(vsnd); + break; + case _(QueueDeviceLow): + if (!vsnd_queue_configurable(vsnd)) + break; + + VSND_QUEUE.queue_used = vsnd_preprocess(vsnd, value); + break; + case _(QueueDeviceHigh): + if (!vsnd_queue_configurable(vsnd)) + break; + + if (value) + virtio_snd_set_fail(vsnd); + break; + case _(QueueNotify): + if (value >= ARRAY_SIZE(vsnd->queues)) { + virtio_snd_set_fail(vsnd); + break; + } + + switch (value) { + case VSND_QUEUE_CTRL: + virtio_queue_notify_handler(vsnd, value); + break; + + case VSND_QUEUE_TX: { + virtio_snd_priv_t *p = vsnd_priv(vsnd); + pthread_mutex_lock(&p->tx_mutex); + p->tx_ev_notify = true; + pthread_cond_signal(&p->tx_cond); + pthread_mutex_unlock(&p->tx_mutex); + break; + } + + case VSND_QUEUE_EVT: + break; + + case VSND_QUEUE_RX: + break; + + default: + break; + } + break; + case _(InterruptACK): + __atomic_fetch_and(&vsnd->interrupt_status, ~value, __ATOMIC_ACQ_REL); + break; + case _(Status): + virtio_snd_update_status(vsnd, value); + break; + default: + if (addr >= _(Config)) + virtio_snd_config_write(vsnd, addr - _(Config), value); + break; + } +#undef _ +} + +static bool vsnd_init_prop(virtio_snd_prop_t *props) +{ + memset(props, 0, sizeof(*props)); + INIT_LIST_HEAD(&props->buf_queue); + if (pthread_mutex_init(&props->lock.lock, NULL)) + return false; + if (pthread_cond_init(&props->lock.all_completed, NULL)) { + pthread_mutex_destroy(&props->lock.lock); + return false; + } + + props->pp.hdr.hdr.code = VIRTIO_SND_R_PCM_SET_PARAMS; + return true; +} + +static void vsnd_destroy_prop(virtio_snd_prop_t *props) +{ + if (props->vsnd) + vsnd_reset_stream(props); + + pthread_mutex_lock(&props->lock.lock); + vsnd_clear_all_nodes_locked(props); + pthread_mutex_unlock(&props->lock.lock); + pthread_cond_destroy(&props->lock.all_completed); + pthread_mutex_destroy(&props->lock.lock); +} + +void virtio_snd_reset(virtio_snd_state_t *vsnd) +{ + if (!vsnd) + return; + + virtio_snd_update_status(vsnd, 0); +} + +bool virtio_snd_init(virtio_snd_state_t *vsnd) +{ + if (!vsnd || !vsnd->priv) + return false; + + virtio_snd_priv_t *p = vsnd_priv(vsnd); + p->vsnd = vsnd; + + PaError err = Pa_Initialize(); + if (err != paNoError) { + rv_log_error("PortAudio Pa_Initialize failed: %s", + Pa_GetErrorText(err)); + return false; + } + + p->pa_initialized = true; + + for (uint32_t i = 0; i < VSND_DEV_CNT_MAX; i++) + p->props[i].vsnd = vsnd; + if (pthread_create(&p->tx_thread, NULL, virtio_snd_tx_thread, vsnd) != 0) { + rv_log_error("cannot create virtio-snd TX thread"); + Pa_Terminate(); + p->pa_initialized = false; + return false; + } + p->tx_thread_started = true; + return true; +} + +virtio_snd_state_t *vsnd_new(void) +{ + virtio_snd_state_t *vsnd = calloc(1, sizeof(*vsnd)); + assert(vsnd); + + virtio_snd_priv_t *p = calloc(1, sizeof(*p)); + if (!p) { + free(vsnd); + return NULL; + } + + p->config.jacks = 1; + p->config.streams = 1; + p->config.chmaps = 1; + p->config.controls = 0; + + if (pthread_mutex_init(&p->tx_mutex, NULL)) { + free(p); + free(vsnd); + return NULL; + } + + if (pthread_cond_init(&p->tx_cond, NULL)) { + pthread_mutex_destroy(&p->tx_mutex); + free(p); + free(vsnd); + return NULL; + } + + if (pthread_mutex_init(&p->tx_process_mutex, NULL)) { + pthread_cond_destroy(&p->tx_cond); + pthread_mutex_destroy(&p->tx_mutex); + free(p); + free(vsnd); + return NULL; + } + + INIT_LIST_HEAD(&p->completed_queue); + if (pthread_mutex_init(&p->completed_mutex, NULL)) { + pthread_mutex_destroy(&p->tx_process_mutex); + pthread_cond_destroy(&p->tx_cond); + pthread_mutex_destroy(&p->tx_mutex); + free(p); + free(vsnd); + return NULL; + } + + for (uint32_t i = 0; i < VSND_DEV_CNT_MAX; i++) { + if (!vsnd_init_prop(&p->props[i])) { + for (uint32_t j = 0; j < i; j++) + vsnd_destroy_prop(&p->props[j]); + pthread_mutex_destroy(&p->completed_mutex); + pthread_mutex_destroy(&p->tx_process_mutex); + pthread_cond_destroy(&p->tx_cond); + pthread_mutex_destroy(&p->tx_mutex); + free(p); + free(vsnd); + return NULL; + } + } + + vsnd->priv = p; + return vsnd; +} + +static void vsnd_clear_completed_queue(virtio_snd_priv_t *p) +{ + pthread_mutex_lock(&p->completed_mutex); + + while (!list_empty(&p->completed_queue)) { + vsnd_buf_queue_node_t *node = + list_last_entry(&p->completed_queue, vsnd_buf_queue_node_t, q); + + list_del(&node->q); + vsnd_free_node(node); + } + + pthread_mutex_unlock(&p->completed_mutex); +} + +void vsnd_delete(virtio_snd_state_t *vsnd) +{ + if (!vsnd) + return; + + virtio_snd_priv_t *p = vsnd_priv(vsnd); + + if (p) { + if (p->tx_thread_started) { + pthread_mutex_lock(&p->tx_mutex); + p->tx_thread_stop = true; + pthread_cond_signal(&p->tx_cond); + pthread_mutex_unlock(&p->tx_mutex); + pthread_join(p->tx_thread, NULL); + p->tx_thread_started = false; + } + + for (uint32_t i = 0; i < VSND_DEV_CNT_MAX; i++) + vsnd_destroy_prop(&p->props[i]); + + if (p->pa_initialized) { + Pa_Terminate(); + p->pa_initialized = false; + } + + vsnd_clear_completed_queue(p); + pthread_mutex_destroy(&p->completed_mutex); + pthread_mutex_destroy(&p->tx_process_mutex); + pthread_cond_destroy(&p->tx_cond); + pthread_mutex_destroy(&p->tx_mutex); + free(p); + } + + free(vsnd); +} + +void virtio_snd_refresh_queue(virtio_snd_state_t *vsnd) +{ + if (!vsnd || !vsnd->priv) + return; + for (;;) { + vsnd_buf_queue_node_t *node = vsnd_completed_pop(vsnd); + if (!node) + break; + virtio_snd_complete_tx_request(vsnd, node->desc_idx, node->status_addr, + node->completion_status, + node->latency_bytes); + vsnd_free_node(node); + } +} diff --git a/src/devices/virtio.h b/src/devices/virtio.h index e38d74011..f1a718992 100644 --- a/src/devices/virtio.h +++ b/src/devices/virtio.h @@ -45,6 +45,11 @@ #define VIRTIO_RNG_DEV_ID 4 +#if RV32_HAS(VIRTIO_SND) +#define VIRTIO_SND_DEV_ID 25 +#define IRQ_VSND_BIT(irq) (1U << (irq)) +#endif + #if RV32_HAS(VIRTIO_NET) #define VIRTIO_NET_DEV_ID 1 @@ -180,6 +185,50 @@ virtio_rng_state_t *vrng_new(void); void vrng_delete(virtio_rng_state_t *vrng); +#if RV32_HAS(VIRTIO_SND) +typedef struct { + uint32_t queue_num; + uint32_t queue_desc; + uint32_t queue_avail; + uint32_t queue_used; + uint16_t last_avail; + bool ready; +} virtio_snd_queue_t; + +typedef struct { + uint32_t magic; + + uint32_t device_features; + uint32_t driver_features; + uint32_t device_features_sel; + uint32_t driver_features_sel; + + uint32_t queue_sel; + + uint32_t interrupt_status; + uint32_t status; + + uint32_t *ram; + void *priv; + + virtio_snd_queue_t queues[4]; +} virtio_snd_state_t; + +virtio_snd_state_t *vsnd_new(void); + +void vsnd_delete(virtio_snd_state_t *vsnd); + +bool virtio_snd_init(virtio_snd_state_t *vsnd); + +void virtio_snd_reset(virtio_snd_state_t *vsnd); + +uint32_t virtio_snd_read(virtio_snd_state_t *vsnd, uint32_t addr); + +void virtio_snd_write(virtio_snd_state_t *vsnd, uint32_t addr, uint32_t value); + +void virtio_snd_refresh_queue(virtio_snd_state_t *vsnd); +#endif /* RV32_HAS(VIRTIO_SND) */ + #if RV32_HAS(VIRTIO_NET) typedef struct { uint32_t queue_num; From 8da85faa6f0ef94b5a02f0eb9edd2206bf9fdf8c Mon Sep 17 00:00:00 2001 From: CHENG-YU TSAI Date: Fri, 25 Sep 2026 00:59:33 +0800 Subject: [PATCH 2/4] Integrate virtio-snd into system emulation 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`. --- Makefile | 9 +- configs/Kconfig | 22 ++++ configs/system_defconfig | 1 + configs/system_interpreter_defconfig | 2 + mk/compat.mk | 1 + mk/system.mk | 9 ++ mk/virtio-snd.mk | 50 ++++++++ src/feature.h | 11 ++ src/gdbstub.c | 3 + src/main.c | 18 ++- src/riscv.c | 183 ++++++++++++++++++++++++++- src/riscv.h | 14 ++ src/riscv_private.h | 4 + src/system.c | 24 ++++ src/system.h | 30 +++++ 15 files changed, 374 insertions(+), 7 deletions(-) create mode 100644 mk/virtio-snd.mk diff --git a/Makefile b/Makefile index 7c600baf8..c70dd00a0 100644 --- a/Makefile +++ b/Makefile @@ -72,7 +72,7 @@ deps := # Feature Flags (Kconfig -> RV32_FEATURE_*) $(call set-features, ELF_LOADER MOP_FUSION BLOCK_CHAINING LOG_COLOR) $(call set-features, SYSTEM GOLDFISH_RTC ARCH_TEST) -$(call set-features, VIRTIO_NET VIRTIO_NET_TAP VIRTIO_NET_USER VIRTIO_NET_VMNET) +$(call set-features, VIRTIO_NET VIRTIO_NET_TAP VIRTIO_NET_USER VIRTIO_NET_VMNET VIRTIO_SND) $(call set-features, EXT_M EXT_A EXT_F EXT_C EXT_V RV32E) $(call set-features, Zicsr Zifencei Zba Zbb Zbc Zbs) $(call set-features, SDL SDL_MIXER GDBSTUB JIT LINK_ZLIB ROOTFS_EXT4) @@ -219,7 +219,7 @@ ifeq ($(CONFIG_JIT),y) $(error JIT only supports x86_64 and ARM64 platforms.) endif endif -$(OUT)/jit.o: src/jit.c src/rv32_jit.c $(CONFIG_HEADER) +$(OUT)/jit.o: src/jit.c src/rv32_jit.c $(CONFIG_HEADER) $(EFFECTIVE_CONFIG_STAMP) $(VECHO) " CC\t$@\n" $(Q)$(CC) -o $@ $(CFLAGS) -c -MMD -MF $@.d $< # T2C optimization level from Kconfig (0-3, default 3) @@ -240,6 +240,9 @@ include mk/http.mk # VirtIO networking include mk/virtio-net.mk +# VirtIO sound +include mk/virtio-snd.mk + # External Dependencies & System Emulation include mk/external.mk include mk/artifact.mk @@ -265,7 +268,7 @@ deps += $(OBJS:%.o=%.o.d) EFFECTIVE_CONFIG_VARS := \ CONFIG_BUILD_WASM CONFIG_SYSTEM CONFIG_GOLDFISH_RTC CONFIG_ELF_LOADER \ CONFIG_VIRTIO_NET CONFIG_VIRTIO_NET_TAP CONFIG_VIRTIO_NET_USER \ - CONFIG_VIRTIO_NET_VMNET \ + CONFIG_VIRTIO_NET_VMNET CONFIG_VIRTIO_SND CONFIG_HAVE_PORTAUDIO \ CONFIG_EXT_M CONFIG_EXT_A CONFIG_EXT_F CONFIG_EXT_C CONFIG_EXT_V CONFIG_RV32E \ CONFIG_Zicsr CONFIG_Zifencei CONFIG_Zba CONFIG_Zbb CONFIG_Zbc CONFIG_Zbs \ CONFIG_MOP_FUSION CONFIG_BLOCK_CHAINING CONFIG_LOG_COLOR CONFIG_ARCH_TEST \ diff --git a/configs/Kconfig b/configs/Kconfig index 88b0e11c2..4712dc705 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -99,6 +99,11 @@ config HAVE_RISCV_TOOLCHAIN bool default $(python,assert run(sys.executable, 'tools/detect-env.py', '--have-riscv-toolchain')) +config HAVE_PORTAUDIO + bool + default n if CC_IS_EMCC + default $(python,assert run('pkg-config', '--exists', 'portaudio-2.0')) + # Host platform detection used by platform-specific system devices. config HOST_LINUX def_bool $(python,assert sys.platform.startswith('linux')) @@ -310,6 +315,23 @@ config ELF_LOADER Enable this for running 'make check' tests in system mode. Disable this for booting Linux kernel images. +config VIRTIO_SND + bool "Enable VirtIO sound device" + default y + depends on SYSTEM && !ELF_LOADER && !CC_IS_EMCC + depends on HAVE_PORTAUDIO + help + Enable VirtIO sound device emulation for Linux system mode. + + PCM playback is provided through PortAudio. The device can be + instantiated at runtime with the '-x vsnd' option. + + Disable this option to exclude virtio-snd and its PortAudio + dependency from the rv32emu executable. + +comment "PortAudio not found - VirtIO sound support is unavailable" + depends on SYSTEM && !ELF_LOADER && !CC_IS_EMCC && !HAVE_PORTAUDIO + config VIRTIO_NET bool "Enable VirtIO network device" default y diff --git a/configs/system_defconfig b/configs/system_defconfig index 0b8ee52a1..759963ae7 100644 --- a/configs/system_defconfig +++ b/configs/system_defconfig @@ -11,3 +11,4 @@ CONFIG_VIRTIO_NET_TAP=y CONFIG_VIRTIO_NET_USER=y CONFIG_VIRTIO_NET_VMNET=y CONFIG_ROOTFS_EXT4=y +CONFIG_VIRTIO_SND=y diff --git a/configs/system_interpreter_defconfig b/configs/system_interpreter_defconfig index 9da1266c5..1993e7de6 100644 --- a/configs/system_interpreter_defconfig +++ b/configs/system_interpreter_defconfig @@ -8,3 +8,5 @@ CONFIG_VIRTIO_NET=y CONFIG_VIRTIO_NET_TAP=y CONFIG_VIRTIO_NET_USER=y CONFIG_VIRTIO_NET_VMNET=y +CONFIG_VIRTIO_SND=y + diff --git a/mk/compat.mk b/mk/compat.mk index 65a03cfcc..5b44b0847 100644 --- a/mk/compat.mk +++ b/mk/compat.mk @@ -82,6 +82,7 @@ $(eval $(call enable-to-config,UBSAN)) # Graphics and audio $(eval $(call enable-to-config,SDL)) $(eval $(call enable-to-config,SDL_MIXER)) +$(eval $(call enable-to-config,VIRTIO_SND)) # Testing $(eval $(call enable-to-config,ARCH_TEST)) diff --git a/mk/system.mk b/mk/system.mk index f627d44b8..c2adef639 100644 --- a/mk/system.mk +++ b/mk/system.mk @@ -81,6 +81,15 @@ DEV_OBJS := $(filter-out $(DEV_OUT)/netdev-vmnet.o, $(DEV_OBJS)) endif endif +# VirtIO sound is available only for native kernel system emulation. +ifneq ($(SYSTEM_MMIO),1) +DEV_OBJS := $(filter-out $(DEV_OUT)/virtio-snd.o, $(DEV_OBJS)) +else ifeq ($(CC_IS_EMCC),1) +DEV_OBJS := $(filter-out $(DEV_OUT)/virtio-snd.o, $(DEV_OBJS)) +else ifneq ($(call has,VIRTIO_SND),1) +DEV_OBJS := $(filter-out $(DEV_OUT)/virtio-snd.o, $(DEV_OBJS)) +endif + # Enable Goldfish RTC peripheral ifneq ($(CONFIG_GOLDFISH_RTC),y) DEV_OBJS := $(filter-out $(DEV_OUT)/rtc.o, $(DEV_OBJS)) diff --git a/mk/virtio-snd.mk b/mk/virtio-snd.mk new file mode 100644 index 000000000..089b0eac0 --- /dev/null +++ b/mk/virtio-snd.mk @@ -0,0 +1,50 @@ +# VirtIO sound device support +# +# This file contains build rules and host dependency constraints specific to +# virtio-snd. + +ifndef _MK_VIRTIO_SND_INCLUDED +_MK_VIRTIO_SND_INCLUDED := 1 + +# Reject an explicitly forced virtio-snd configuration when the selected +# build mode cannot support it. Kconfig-derived virtio-snd settings may become +# stale when another legacy ENABLE_* override changes a dependency; those are +# excluded from unsupported build modes below instead of treated as an error. +check-vsnd-config: + $(Q)if [ "$(ENABLE_VIRTIO_SND_NORMALIZED)" = "y" ]; then \ + if [ "$(SYSTEM_MMIO)" != "1" ]; then \ + echo "Error: VirtIO sound requires kernel system emulation." >&2; \ + exit 1; \ + fi; \ + if [ "$(CC_IS_EMCC)" = "1" ]; then \ + echo "Error: VirtIO sound is not supported with Emscripten." >&2; \ + exit 1; \ + fi; \ + fi; \ + if [ "$(SYSTEM_MMIO)" = "1" ] && \ + [ "$(CC_IS_EMCC)" != "1" ] && \ + [ "$(call has,VIRTIO_SND)" = "1" ] && \ + [ "$(call pkg-exists,portaudio-2.0)" != "y" ]; then \ + echo "Error: VirtIO sound requires PortAudio (portaudio-2.0)." >&2; \ + exit 1; \ + fi + +ifeq ($(SYSTEM_MMIO),1) +ifneq ($(CC_IS_EMCC),1) +ifeq ($(call has,VIRTIO_SND),1) +ifneq ($(NEEDS_CONFIG),) +PORTAUDIO_CFLAGS := $(call dep,cflags,portaudio-2.0) +PORTAUDIO_LIBS := $(call dep,libs,portaudio-2.0) +$(OUT)/devices/virtio-snd.o: CFLAGS += $(PORTAUDIO_CFLAGS) +LDFLAGS += $(PORTAUDIO_LIBS) -pthread -lm +endif +endif +endif +endif + +$(OUT)/devices/virtio-snd.o: | check-vsnd-config +$(BIN): | check-vsnd-config + +.PHONY: check-vsnd-config + +endif # _MK_VIRTIO_SND_INCLUDED diff --git a/src/feature.h b/src/feature.h index b336f04b1..098ec6067 100644 --- a/src/feature.h +++ b/src/feature.h @@ -167,6 +167,17 @@ #define RV32_FEATURE_SYSTEM_MMIO 0 #endif +/* VirtIO sound device */ +#ifndef RV32_FEATURE_VIRTIO_SND +#define RV32_FEATURE_VIRTIO_SND 0 +#endif + +/* VirtIO sound is only valid for native kernel system emulation. */ +#if !RV32_FEATURE_SYSTEM_MMIO || defined(__EMSCRIPTEN__) +#undef RV32_FEATURE_VIRTIO_SND +#define RV32_FEATURE_VIRTIO_SND 0 +#endif + /* VirtIO network device */ #ifndef RV32_FEATURE_VIRTIO_NET #define RV32_FEATURE_VIRTIO_NET 0 diff --git a/src/gdbstub.c b/src/gdbstub.c index 7e3ca906e..499934a59 100644 --- a/src/gdbstub.c +++ b/src/gdbstub.c @@ -148,6 +148,9 @@ void rv_debug_run(riscv_t *rv) rv_step_debug(rv); #if RV32_HAS(VIRTIO_NET) rv_refresh_vnet(rv); +#endif +#if RV32_HAS(VIRTIO_SND) + rv_refresh_vsnd(rv); #endif } while (rv->debug_continue && !rv_debug_should_stop(rv)); } diff --git a/src/main.c b/src/main.c index b3d110229..ae01eddd5 100644 --- a/src/main.c +++ b/src/main.c @@ -71,11 +71,14 @@ static char *opt_bootargs; static char *opt_virtio_blk_img[VBLK_DEV_MAX]; static int opt_virtio_blk_idx = 0; -/* enable virtio-rng device */ +/* enable virtio-rng, virtio-net, virtio-snd device */ static bool opt_virtio_rng = false; #if RV32_HAS(VIRTIO_NET) static char *opt_virtio_net_backend; #endif +#if RV32_HAS(VIRTIO_SND) +static bool opt_virtio_snd = false; +#endif #endif static void reset_getopt_state(void) @@ -121,6 +124,9 @@ static void reset_runtime_options(void) #if RV32_HAS(VIRTIO_NET) opt_virtio_net_backend = NULL; #endif +#if RV32_HAS(VIRTIO_SND) + opt_virtio_snd = false; +#endif #endif reset_getopt_state(); @@ -156,6 +162,9 @@ static void print_usage(const char *filename) "multiple times for multiple block devices\n" #endif " -x vrng : enable virtio-rng device\n" +#if RV32_HAS(VIRTIO_SND) + " -x vsnd : enable virtio-snd device\n" +#endif #if RV32_HAS(VIRTIO_NET) " -x vnet:: use as virtio-net backend " "interface (supported backend:" @@ -266,6 +275,10 @@ static bool parse_args(int argc, char **args) #endif } else if (!strcmp("vrng", optarg)) { opt_virtio_rng = true; +#if RV32_HAS(VIRTIO_SND) + } else if (!strcmp("vsnd", optarg)) { + opt_virtio_snd = true; +#endif } else { return false; } @@ -482,6 +495,9 @@ int main(int argc, char **args) attr.data.system.vrng_enabled = opt_virtio_rng; #if RV32_HAS(VIRTIO_NET) attr.data.system.vnet_backend = opt_virtio_net_backend; +#endif +#if RV32_HAS(VIRTIO_SND) + attr.data.system.vsnd_enabled = opt_virtio_snd; #endif if (opt_virtio_blk_idx) { attr.data.system.vblk_device = opt_virtio_blk_img; diff --git a/src/riscv.c b/src/riscv.c index 27d1a226b..3d1657eb7 100644 --- a/src/riscv.c +++ b/src/riscv.c @@ -56,10 +56,23 @@ #define BLOCK_IR_MAP_CAPACITY_BITS 10 +/* + * VirtIO devices with asynchronous host-side activity (eg. network package + * receiving, sound PCM completion) are refreshed periodically rather than on + * every guest instruction. A 5000-cycle interval keeps interrupt propagation + * responsive while avoiding unnecessary polling overhead in the main execution + * loop. + */ #if RV32_HAS(VIRTIO_NET) #define VNET_REFRESH_INTERVAL 5000ULL #endif +#if RV32_HAS(VIRTIO_SND) +#define VSND_REFRESH_INTERVAL 5000ULL +static void rv_destroy_vsnd(vm_attr_t *attr); +static bool rv_init_vsnd(vm_attr_t *attr); +#endif + #if !RV32_HAS(JIT) /* initialize the block map */ static void block_map_init(block_map_t *map, const uint8_t bits) @@ -645,11 +658,14 @@ static void load_dtb(char **ram_loc, vm_attr_t *attr) char *bootargs = attr->data.system.bootargs; char **vblk = attr->data.system.vblk_device; bool vrng_enabled = attr->data.system.vrng_enabled; + bool have_optional_virtio = vblk || vrng_enabled; #if RV32_HAS(VIRTIO_NET) char *vnet = attr->data.system.vnet_backend; - bool have_optional_virtio = vblk || vrng_enabled || vnet; -#else - bool have_optional_virtio = vblk || vrng_enabled; + have_optional_virtio = have_optional_virtio || vnet; +#endif +#if RV32_HAS(VIRTIO_SND) + bool vsnd_enabled = attr->data.system.vsnd_enabled; + have_optional_virtio = have_optional_virtio || vsnd_enabled; #endif char *blob = *ram_loc; char *buf; @@ -884,6 +900,41 @@ static void load_dtb(char **ram_loc, vm_attr_t *attr) DTB_SET_OR_FAIL( fdt_setprop(dtb_buf, subnode, "interrupts", &irq, sizeof(irq)), "virtio-net interrupts"); + dev_idx++; + } +#endif +#if RV32_HAS(VIRTIO_SND) + if (vsnd_enabled) { + uint32_t new_addr = next_addr + dev_idx * addr_offset; + uint32_t new_irq = next_irq + dev_idx; + + attr->vsnd_mmio_base_hi = new_addr >> 20; + attr->vsnd_irq = new_irq; + + char node_name[32]; + snprintf(node_name, sizeof(node_name), "virtio@%x", new_addr); + + int subnode = fdt_add_subnode(dtb_buf, node, node_name); + if (subnode == -FDT_ERR_NOSPACE) + rv_log_warn("add virtio-snd subnode no space!\n"); + assert(subnode >= 0); + + DTB_SET_OR_FAIL(fdt_setprop_string(dtb_buf, subnode, "compatible", + "virtio,mmio"), + "virtio-snd compatible"); + + uint32_t reg[2] = { + cpu_to_fdt32(new_addr), + cpu_to_fdt32(size), + }; + DTB_SET_OR_FAIL( + fdt_setprop(dtb_buf, subnode, "reg", reg, sizeof(reg)), + "virtio-snd reg"); + + uint32_t irq = cpu_to_fdt32(new_irq); + DTB_SET_OR_FAIL( + fdt_setprop(dtb_buf, subnode, "interrupts", &irq, sizeof(irq)), + "virtio-snd interrupts"); } #endif } @@ -1013,6 +1064,9 @@ static void rv_fsync_device(void) rv_log_error("Failed to clean up virtio-net backend"); } #endif +#if RV32_HAS(VIRTIO_SND) + rv_destroy_vsnd(attr); +#endif } #endif /* RV32_HAS(SYSTEM_MMIO) */ @@ -1099,6 +1153,30 @@ void rv_refresh_vnet(riscv_t *rv) } #endif +#if RV32_HAS(VIRTIO_SND) +void rv_refresh_vsnd(riscv_t *rv) +{ + assert(rv); + + vm_attr_t *attr = PRIV(rv); + + if (!attr->vsnd) + return; + + if (rv->csr_cycle - rv->last_vsnd_refresh < VSND_REFRESH_INTERVAL) + return; + + /* + * Worker threads only stage completed TX requests. Publish their + * guest-visible status and used-ring entries here on the emulator + * thread before propagating the resulting interrupt to the PLIC. + */ + virtio_snd_refresh_queue(attr->vsnd); + emu_update_vsnd_interrupts(rv); + rv->last_vsnd_refresh = rv->csr_cycle; +} +#endif + #if RV32_HAS_HART_CORO /* The hart's whole run loop. In normal operation it only leaves the coroutine * when the hart halts or a guest reboot restarts it: per-slice host work such @@ -1122,6 +1200,9 @@ static void rv_hart_coroutine(void *arg) rv_step(rv); #if RV32_HAS(VIRTIO_NET) rv_refresh_vnet(rv); +#endif +#if RV32_HAS(VIRTIO_SND) + rv_refresh_vsnd(rv); #endif } } @@ -1348,6 +1429,10 @@ static void rv_reset_hart(riscv_t *rv, riscv_word_t pc) rv->last_vnet_refresh = rv->csr_cycle; #endif +#if RV32_HAS(VIRTIO_SND) + rv->last_vsnd_refresh = rv->csr_cycle; +#endif + /* Set the reset address */ rv->PC = pc; @@ -1517,6 +1602,15 @@ void rv_delete(riscv_t *rv) { assert(rv); vm_attr_t *attr = PRIV(rv); + +#if RV32_HAS(VIRTIO_SND) + /* + * Sound workers may still access VirtIO descriptors in guest RAM. + * Destroy the device before releasing guest memory. + */ + rv_destroy_vsnd(attr); +#endif + #if !RV32_HAS(JIT) map_delete(attr->fd_map); memory_delete(attr->mem); @@ -1613,6 +1707,47 @@ static void load_boot_images(vm_attr_t *attr) } #endif /* RV32_HAS(SYSTEM_MMIO) */ +#if RV32_HAS(VIRTIO_SND) +static void rv_destroy_vsnd(vm_attr_t *attr) +{ + if (!attr->vsnd) + return; + + /* + * virtio-snd owns worker threads which may still access guest queue + * descriptors and PCM status in RAM. They must be stopped before guest + * memory is cleared or released. + */ + vsnd_delete(attr->vsnd); + attr->vsnd = NULL; +} + +static bool rv_init_vsnd(vm_attr_t *attr) +{ + if (!attr->data.system.vsnd_enabled) + return true; + + if (attr->vsnd) { + attr->vsnd->ram = (uint32_t *) attr->mem->mem_base; + return true; + } + + attr->vsnd = vsnd_new(); + if (!attr->vsnd) + return false; + + attr->vsnd->ram = (uint32_t *) attr->mem->mem_base; + + if (!virtio_snd_init(attr->vsnd)) { + vsnd_delete(attr->vsnd); + attr->vsnd = NULL; + return false; + } + + return true; +} +#endif + #if RV32_HAS(JIT) /* Initialize or reinitialize JIT state. * Cleans up existing state if present (reboot case). @@ -1690,6 +1825,21 @@ void rv_warm_reboot(riscv_t *rv, riscv_word_t pc) } #endif +#if RV32_HAS(VIRTIO_SND) + if (attr->vsnd) { + /* + * Reset guest-visible VirtIO sound state while preserving the host + * backend and worker threads across a warm reboot. + */ + virtio_snd_reset(attr->vsnd); + + uint32_t irq_bit = IRQ_VSND_BIT(attr->vsnd_irq); + attr->plic->active &= ~irq_bit; + attr->plic->ip &= ~irq_bit; + attr->plic->masked &= ~irq_bit; + } +#endif + /* clear memory */ memory_fill(attr->mem, 0, attr->mem->mem_size, 0); @@ -1721,6 +1871,16 @@ bool rv_cold_reboot(riscv_t *rv, riscv_word_t pc) assert(rv); vm_attr_t *attr = PRIV(rv); +#if RV32_HAS(VIRTIO_SND) + /* + * A reboot invalidates all guest-owned VirtIO state. Reset the device + * before clearing guest memory so outstanding requests can be retired + * while their descriptors are still accessible. + */ + if (attr->vsnd) + virtio_snd_reset(attr->vsnd); +#endif + /* Initialize memory */ if (attr->mem) /* check for reboot */ memory_fill(attr->mem, 0, attr->mem->mem_size, 0); @@ -1855,6 +2015,11 @@ bool rv_cold_reboot(riscv_t *rv, riscv_word_t pc) attr->vrng_mmio_base_hi = 0; attr->vrng_irq = 0; +#if RV32_HAS(VIRTIO_SND) + attr->vsnd_mmio_base_hi = 0; + attr->vsnd_irq = 0; +#endif + /* Load kernel, DTB, and initrd */ load_boot_images(attr); @@ -2039,6 +2204,13 @@ bool rv_cold_reboot(riscv_t *rv, riscv_word_t pc) } #endif +#if RV32_HAS(VIRTIO_SND) + if (!rv_init_vsnd(attr)) { + rv_log_error("Failed to initialize virtio-snd"); + exit(EXIT_FAILURE); + } +#endif + capture_keyboard_input(); #endif /* !RV32_HAS(SYSTEM_MMIO) */ @@ -2107,6 +2279,11 @@ bool rv_cold_reboot(riscv_t *rv, riscv_word_t pc) if (attr->vnet) vnet_delete(attr->vnet); #endif + +#if RV32_HAS(VIRTIO_SND) + rv_destroy_vsnd(attr); +#endif + #endif map_delete(attr->fd_map); memory_delete(attr->mem); diff --git a/src/riscv.h b/src/riscv.h index 807951dda..09e573dfc 100644 --- a/src/riscv.h +++ b/src/riscv.h @@ -489,6 +489,11 @@ void rv_coroutine_stop(void); void rv_refresh_vnet(riscv_t *rv); #endif +#if RV32_HAS(VIRTIO_SND) +/* Propagate asynchronous virtio-snd completion interrupts periodically. */ +void rv_refresh_vsnd(riscv_t *rv); +#endif + /* set the program counter of a RISC-V emulator */ bool rv_set_pc(riscv_t *rv, riscv_word_t pc); @@ -588,6 +593,9 @@ typedef struct { #if RV32_HAS(VIRTIO_NET) char *vnet_backend; #endif +#if RV32_HAS(VIRTIO_SND) + bool vsnd_enabled; +#endif } vm_system_t; #endif /* RV32_HAS(SYSTEM) */ @@ -633,6 +641,12 @@ typedef struct { uint32_t vnet_mmio_base_hi; int vnet_irq; #endif /* RV32_HAS(VIRTIO_NET) */ +#if RV32_HAS(VIRTIO_SND) + /* virtio-snd device */ + virtio_snd_state_t *vsnd; + uint32_t vsnd_mmio_base_hi; + int vsnd_irq; +#endif /* RV32_HAS(VIRTIO_SND) */ #endif /* RV32_HAS(SYSTEM_MMIO) */ /* vm memory object */ diff --git a/src/riscv_private.h b/src/riscv_private.h index 94a16095a..2b724480d 100644 --- a/src/riscv_private.h +++ b/src/riscv_private.h @@ -432,6 +432,10 @@ struct riscv_internal { uint64_t last_vnet_refresh; #endif +#if RV32_HAS(VIRTIO_SND) + uint64_t last_vsnd_refresh; +#endif + #if RV32_HAS(SYSTEM) /* Nesting depth of the traps being handled by __trap_handler(). Raising * a trap increments it and sret decrements it, so the sret of a nested diff --git a/src/system.c b/src/system.c index 0fbcd67c0..85b0e12f1 100644 --- a/src/system.c +++ b/src/system.c @@ -65,6 +65,30 @@ void emu_update_vnet_interrupts(riscv_t *rv) } #endif +#if RV32_HAS(VIRTIO_SND) +void emu_update_vsnd_interrupts(riscv_t *rv) +{ + vm_attr_t *attr = PRIV(rv); + + if (!attr->vsnd) + return; + + /* + * TX completion runs asynchronously in the sound completion thread. + * Read the interrupt status atomically before propagating it to the PLIC. + */ + uint32_t interrupt_status = + __atomic_load_n(&attr->vsnd->interrupt_status, __ATOMIC_ACQUIRE); + + if (interrupt_status) + attr->plic->active |= IRQ_VSND_BIT(attr->vsnd_irq); + else + attr->plic->active &= ~IRQ_VSND_BIT(attr->vsnd_irq); + + plic_update_interrupts(attr->plic); +} +#endif + #if RV32_HAS(GOLDFISH_RTC) void emu_update_rtc_interrupts(riscv_t *rv) { diff --git a/src/system.h b/src/system.h index c7b885477..522d9cac3 100644 --- a/src/system.h +++ b/src/system.h @@ -33,6 +33,9 @@ enum SUPPORTED_MMIO { #if RV32_HAS(VIRTIO_NET) MMIO_VIRTIONET, #endif +#if RV32_HAS(VIRTIO_SND) + MMIO_VIRTIOSND, +#endif #if RV32_HAS(GOLDFISH_RTC) MMIO_RTC, #endif /* RV32_HAS(GOLDFISH_RTC) */ @@ -98,6 +101,19 @@ enum SUPPORTED_MMIO { ) \ break; \ ) \ + IIF(RV32_FEATURE_VIRTIO_SND)( \ + case MMIO_VIRTIOSND: \ + IIF(rw)( /* read */ \ + mmio_read_val = virtio_snd_read(PRIV(rv)->vsnd, addr & 0xFFFFF); \ + emu_update_vsnd_interrupts(rv); \ + return mmio_read_val; \ + , /* write */ \ + virtio_snd_write(PRIV(rv)->vsnd, addr & 0xFFFFF, val); \ + emu_update_vsnd_interrupts(rv); \ + return; \ + ) \ + break; \ + ) \ IIF(RV32_FEATURE_GOLDFISH_RTC)( \ case MMIO_RTC: \ IIF(rw)( /* read */ \ @@ -135,6 +151,12 @@ enum SUPPORTED_MMIO { MMIO_OP(MMIO_VIRTIONET, MMIO_R); \ } \ ) \ + IIF(RV32_FEATURE_VIRTIO_SND)( \ + else if (PRIV(rv)->vsnd && \ + hi == PRIV(rv)->vsnd_mmio_base_hi) { \ + MMIO_OP(MMIO_VIRTIOSND, MMIO_R); \ + } \ + ) \ else { \ switch (hi) { \ case 0x0: \ @@ -175,6 +197,11 @@ enum SUPPORTED_MMIO { MMIO_OP(MMIO_VIRTIONET, MMIO_W); \ } \ ) \ + IIF(RV32_FEATURE_VIRTIO_SND)( \ + else if (PRIV(rv)->vsnd && hi == PRIV(rv)->vsnd_mmio_base_hi) { \ + MMIO_OP(MMIO_VIRTIOSND, MMIO_W); \ + } \ + ) \ else { \ switch (hi) { \ case 0x0: \ @@ -204,6 +231,9 @@ void emu_update_vrng_interrupts(riscv_t *rv); #if RV32_HAS(VIRTIO_NET) void emu_update_vnet_interrupts(riscv_t *rv); #endif +#if RV32_HAS(VIRTIO_SND) +void emu_update_vsnd_interrupts(riscv_t *rv); +#endif #if RV32_HAS(GOLDFISH_RTC) void emu_update_rtc_interrupts(riscv_t *rv); #endif /* RV32_HAS(GOLDFISH_RTC) */ From 80f5a0cd1a65b9bb2db1ebacca63b8b6ed0a72d3 Mon Sep 17 00:00:00 2001 From: CHENG-YU TSAI Date: Fri, 25 Sep 2026 01:23:10 +0800 Subject: [PATCH 3/4] CI: Add virtio-snd CI and documentation 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 --- .ci/boot-linux.sh | 6 + .ci/brew-install.sh | 6 +- .ci/sound.sh | 153 +++++++++++++++ .github/actions/setup-build/action.yml | 4 +- README.md | 1 + docs/networking.md | 26 ++- docs/sound.md | 259 +++++++++++++++++++++++++ 7 files changed, 444 insertions(+), 11 deletions(-) create mode 100755 .ci/sound.sh create mode 100644 docs/sound.md diff --git a/.ci/boot-linux.sh b/.ci/boot-linux.sh index 4a0a2d76d..24807c196 100755 --- a/.ci/boot-linux.sh +++ b/.ci/boot-linux.sh @@ -50,4 +50,10 @@ if grep -q '^CONFIG_VIRTIO_NET_VMNET=y$' .config; then RET=$((${RET} + $?)) fi +# Virtio-snd tests +if grep -q '^CONFIG_VIRTIO_SND=y$' .config; then + (. "${SCRIPT_DIR}/sound.sh") + RET=$((${RET} + $?)) +fi + exit ${RET} diff --git a/.ci/brew-install.sh b/.ci/brew-install.sh index 356bd7ae5..f34c98c11 100755 --- a/.ci/brew-install.sh +++ b/.ci/brew-install.sh @@ -14,7 +14,7 @@ set -e -u -o pipefail LLVM_VERSION=$(cat "${SCRIPT_DIR}/llvm-version") -brew install make dtc expect sdl2 bc e2fsprogs p7zip "llvm@${LLVM_VERSION}" dcfldd +brew install make dtc expect sdl2 bc e2fsprogs p7zip "llvm@${LLVM_VERSION}" dcfldd pkgconf portaudio # SDL_mixer only gates the audio tests, so a bottle outage should not take the # whole job down with it. @@ -24,12 +24,12 @@ brew install sdl2_mixer \ # Relink every formula the later build steps invoke by name. Failures are # tolerated (a keg-only formula refuses to link, and is reached through "brew # --prefix" instead), so the verification below is what actually decides. -for formula in make dtc expect sdl2 bc p7zip dcfldd; do +for formula in make dtc expect sdl2 bc p7zip dcfldd pkgconf portaudio; do brew link --overwrite "${formula}" > /dev/null 2>&1 || true done MISSING= -for tool in dtc expect bc; do +for tool in dtc expect bc pkg-config; do command -v "${tool}" > /dev/null || MISSING="${MISSING} ${tool}" done if [ -n "${MISSING}" ]; then diff --git a/.ci/sound.sh b/.ci/sound.sh new file mode 100755 index 000000000..0ddefd621 --- /dev/null +++ b/.ci/sound.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash + +# Get the directory of this script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "${SCRIPT_DIR}/common.sh" + +check_platform + +# Overwrite the variable to use initrd rootfs when explicitly requested. +SPECIFY_ROOTFS=${1:-} +if [[ -n "${SPECIFY_ROOTFS}" && "${SPECIFY_ROOTFS}" != "initrd" ]]; then + print_error "Unsupported rootfs for virtio-snd test: ${SPECIFY_ROOTFS}" + exit 2 +fi +if [[ "${SPECIFY_ROOTFS}" == "initrd" ]]; then + OPTS_BASE_ROOTFS_EXT4="${OPTS_BASE}" +fi + +# Ensure a failed/timeout test does not leave rv32emu running. +register_cleanup cleanup_emulator + +RET=0 + +MESSAGES=( + "${COLOR_G}OK!" + "${COLOR_R}Fail to boot" + "${COLOR_R}Fail to login" + "${COLOR_R}Fail to bind virtio-snd driver" + "${COLOR_R}Fail to enumerate ALSA card" + "${COLOR_R}Fail to enumerate ALSA PCM device" + "${COLOR_R}Fail to play PCM with speaker-test" +) + +# GitHub Actions runners do not provide a physical audio output device. +# Use the ALSA null PCM as the default output so PortAudio can exercise +# the virtio-snd playback path in headless CI. +if [[ "${CI:-}" == "true" && "$(uname -s)" == "Linux" ]]; then + cat > "${HOME}/.asoundrc" << 'EOF' +pcm.!default { + type null +} +EOF +fi + +RUN_LINUX="build/rv32emu ${OPTS_BASE_ROOTFS_EXT4} -x vsnd" + +printf "${COLOR_Y}===== Test option: ${OPTS_BASE_ROOTFS_EXT4} -x vsnd =====${COLOR_N}\n" + +ASSERT expect <<- DONE + set timeout ${TIMEOUT} + + spawn ${RUN_LINUX} + + expect { + "buildroot login:" { + send "root\r" + } + timeout { + exit 1 + } + } + + expect { + "# " {} + timeout { + exit 2 + } + } + + send "readlink /sys/bus/virtio/devices/virtio0/driver\r" + expect { + "virtio_snd" {} + timeout { + exit 3 + } + } + + expect "# " + send "cat /proc/asound/cards\r" + expect { + "VirtIO SoundCard" {} + timeout { + exit 4 + } + } + + expect "# " + send "aplay -l\r" + expect { + "VirtIO SoundCard" {} + timeout { + exit 5 + } + } + + # Exercise the PCM playback path with a bounded single loop. Use an + # explicitly sized buffer to tolerate scheduling jitter on shared CI runners. + expect "# " + set timeout 60 + send "speaker-test -D hw:0,0 -c 1 -r 48000 -F S16_LE -t sine -b 1200000 -p 300000 -P 4 -l 1; echo SPEAKER_TEST_RC:\\\$?\r" + + expect { + "Playback open error:" { + exit 6 + } + "Setting of hwparams failed:" { + exit 6 + } + "Setting of swparams failed:" { + exit 6 + } + "Transfer failed:" { + exit 6 + } + "0 - Mono" {} + timeout { + exit 6 + } + } + + # One completed loop is sufficient to verify end-to-end PCM progress. + expect { + "Time per period =" {} + "Transfer failed:" { + exit 6 + } + timeout { + exit 6 + } + } + + expect { + "SPEAKER_TEST_RC:0" {} + -re {SPEAKER_TEST_RC:([1-9][0-9]*)} { + exit 6 + } + timeout { + exit 6 + } + } + + expect "# " + send "\x01" + send "x" +DONE + +ret=$? +RET=$((${RET} + ${ret})) +cleanup + +printf "\nVirtio-snd Test: [ ${MESSAGES[$ret]}${COLOR_N} ]\n" + +exit ${RET} diff --git a/.github/actions/setup-build/action.yml b/.github/actions/setup-build/action.yml index 79548f075..bb7eec923 100644 --- a/.github/actions/setup-build/action.yml +++ b/.github/actions/setup-build/action.yml @@ -57,8 +57,8 @@ runs: shell: bash run: | .ci/apt-install.sh curl wget lsb-release software-properties-common \ - gnupg libsdl2-dev libsdl2-mixer-dev device-tree-compiler expect \ - bc p7zip-full e2fsprogs + gnupg libsdl2-dev libsdl2-mixer-dev portaudio19-dev pkg-config \ + device-tree-compiler expect bc p7zip-full e2fsprogs # No Homebrew cache on purpose. The cache can only carry the Cellar, not # the opt/ and bin/ symlinks, and "brew install" on a formula it already diff --git a/README.md b/README.md index 1202889d2..0f3c6ab80 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ button that switches directly to the other. | Prebuilt binaries | [docs/prebuilt.md](docs/prebuilt.md) | | Base image preparation | [docs/base-image.md](docs/base-image.md) | | Networking guidance | [docs/networking.md](docs/networking.md) | +| Sound guidance | [docs/sound.md](docs/sound.md) | ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. diff --git a/docs/networking.md b/docs/networking.md index 42c5f7b06..ebd940429 100644 --- a/docs/networking.md +++ b/docs/networking.md @@ -62,14 +62,28 @@ where `` is one of the supported backend names for the current host. ### Linux TAP mode -TAP mode requires root privileges or `CAP_NET_ADMIN`. +TAP mode requires network administration privileges, which can be provided either by running rv32emu with `sudo` or by granting the executable `CAP_NET_ADMIN`. -Start rv32emu: +When TAP networking is used together with virtio-snd, prefer `CAP_NET_ADMIN` instead of running the whole emulator with `sudo`. PortAudio normally connects to the audio session of the logged-in desktop user, such as PipeWire or PulseAudio. Running rv32emu as root may prevent PortAudio from accessing that user session. + +Grant the required capability to the rv32emu executable: + +```shell +sudo setcap cap_net_admin+ep build/rv32emu +getcap build/rv32emu + +build/rv32emu \ + -k build/linux-image/Image \ + -x vblk:build/linux-image/rootfs.ext4,rootfs \ + -x vnet:tap \ + -x vsnd +``` +If you just want to use virtio-net only (without virtio-snd), you can start rv32emu with `sudo -E`: ```shell sudo -E build/rv32emu \ -k build/linux-image/Image \ - -i build/linux-image/rootfs.cpio \ + -x vblk:build/linux-image/rootfs.ext4,rootfs \ -x vnet:tap ``` @@ -100,7 +114,7 @@ Start rv32emu: ```shell build/rv32emu \ -k build/linux-image/Image \ - -i build/linux-image/rootfs.cpio \ + -x vblk:build/linux-image/rootfs.ext4,rootfs \ -x vnet:user ``` @@ -125,7 +139,7 @@ mode through: ```shell sudo -E build/rv32emu \ -k build/linux-image/Image \ - -i build/linux-image/rootfs.cpio \ + -x vblk:build/linux-image/rootfs.ext4,rootfs \ -x vnet:vmnet ``` @@ -167,7 +181,7 @@ On macOS, use the user-mode SLIRP backend: ```shell build/rv32emu \ -k build/linux-image/Image \ - -i build/linux-image/rootfs.cpio \ + -x vblk:build/linux-image/rootfs.ext4,rootfs \ -x vnet:user ``` diff --git a/docs/sound.md b/docs/sound.md new file mode 100644 index 000000000..06662dca7 --- /dev/null +++ b/docs/sound.md @@ -0,0 +1,259 @@ +# VirtIO sound in rv32emu + +This document explains how to build, run, and verify the virtio-snd +playback device in rv32emu. + +The current implementation provides PCM playback through the VirtIO sound +control and TX queues. Event and capture (RX) queues are not supported. + +## Host dependency + +virtio-snd uses PortAudio as the host playback backend. + +On Debian/Ubuntu: + +```shell +sudo apt install portaudio19-dev +``` + +On macOS with Homebrew: + +```shell +brew install portaudio +``` + +## Build + +Build rv32emu with system emulation and virtio-snd enabled. The default `system_defconfig` uses the tier-1 JIT: + +```shell +make system_defconfig +``` +To build the interpreter-only system emulator instead: +```shell +make system_interpreter_defconfig +``` +Or can use Kconfig to choose virtio-snd: +```shell +make config +``` + +The virtio-snd build option is only available with system emulation. Builds +without virtio-snd do not require PortAudio. + +## Run + +Start system emulation with the sound device enabled: + +```shell +build/rv32emu \ + -k build/linux-image/Image \ + -x vblk:build/linux-image/rootfs.ext4,rootfs \ + -x vsnd +``` + +The generated device tree exposes the sound device through VirtIO-MMIO. +After Linux boots, the device should bind to the `virtio_snd` driver. + +### Using virtio-snd with virtio-net + +virtio-snd can be used together with the available virtio-net backends. The required host privileges depend on the selected network backend. + +#### User-mode SLIRP + +User-mode SLIRP does not require root privileges or additional capabilities, so rv32emu can run as the regular user: + +```shell +build/rv32emu \ + -k build/linux-image/Image \ + -x vblk:build/linux-image/rootfs.ext4,rootfs \ + -x vsnd \ + -x vnet:user +``` +This is the simplest option when direct host-side networking through TAP or vmnet is not required. + +#### Linux TAP + +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. + +Instead, grant the rv32emu executable `CAP_NET_ADMIN` and run it as the regular user: + +```shell +sudo setcap cap_net_admin+ep build/rv32emu + +build/rv32emu \ + -k build/linux-image/Image \ + -x vblk:build/linux-image/rootfs.ext4,rootfs \ + -x vsnd \ + -x vnet:tap +``` +The capability may need to be applied again after rebuilding or relinking the rv32emu executable. See [Networking guidance](networking.md#linux-tap-mode) in rv32emu for TAP configuration details. + +#### macOS vmnet +On macOS, the vmnet backend requires elevated privileges. It can be used together with virtio-snd by preserving the user environment with sudo -E: +```shell +sudo -E build/rv32emu \ + -k build/linux-image/Image \ + -x vblk:build/linux-image/rootfs.ext4,rootfs \ + -x vsnd \ + -x vnet:vmnet +``` +For additional backend-specific network configuration, see [Networking in rv32emu](networking.md). + +## Guest verification + +Check the bound VirtIO driver: + +```shell +readlink /sys/bus/virtio/devices/virtio0/driver +``` + +The output should end with: + +```text +bus/virtio/drivers/virtio_snd +``` + +Check the ALSA card: + +```shell +cat /proc/asound/cards +``` + +A detected device should contain: + +```text +VirtIO SoundCard +``` + +Check the playback PCM device: + +```shell +aplay -l +``` + +A working configuration should report a VirtIO playback device similar to: + +```text +card 0: SoundCard [VirtIO SoundCard], device 0: virtio-snd [VirtIO PCM 0] +``` + +## Playback test + +`speaker-test` can exercise the actual PCM path without requiring a test audio +file: + +```shell +speaker-test -D hw:0,0 -c 1 -r 48000 -F S16_LE -t sine -l 2 +``` + +This configuration generates a mono sine wave at 48 kHz using 16-bit +little-endian samples and runs two playback loops. + +A successful run prints playback progress such as: + +```text +0 - Mono +Time per period = ... +``` + +and returns to the shell after the requested loops complete. + +The playback path exercised by this test is: + +```text +guest PCM -> TX thread -> pending PCM queue -> PortAudio callback -> speaker + | + v +used ring <- completion thread <- completed queue +``` + +A TX request remains in flight until the corresponding PCM data has been +consumed. The PortAudio callback then moves the request to the completed queue, +and the completion thread updates the used ring and raises the VirtIO +interrupt. + +When the PortAudio callback requests data before a PCM buffer is available, +the output buffer is zero-filled so the host plays silence instead of stale or +invalid PCM data. + +## Additional Cool Test +Play music by `virtio-blk` and `virtio-snd` + +Host side transfer mp3 to wav: +```shell +ffmpeg -i song.mp3 -ac 1 -ar 48000 -sample_fmt s16 song.wav +``` +In rv32emu directory +```shell +mkdir -p build/disks +dd if=/dev/zero of=build/disks/song.img bs=1M count=128 +mkfs.ext4 -F build/disks/song.img + +mkdir -p /tmp/rv32-songdisk +sudo mount -o loop build/disks/song.img /tmp/rv32-songdisk +sudo cp /tmp/rv32-songdisk/song.wav +sync +sudo umount /tmp/rv32-songdisk + +build/rv32emu \ + -k build/linux-image/Image \ + -x vblk:build/linux-image/rootfs.ext4,rootfs \ + -x vsnd \ + -x vblk:build/disks/song.img +``` + +Guest side + +Mount the virtio-blk disk. The attached disk appears as `/dev/vdX` in the +guest. With the single `-x vblk` option used in this example, it is normally +`/dev/vda`. For multiple virtio-blk devices and their device ordering, see +[Virtio block device](system.md#virtio-block-device-optional). + +1. mount virtio-blk +```shell +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. +``` + +2. play music and check whether stop in half +```shell +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 +``` + +## CI + +The virtio-snd CI test is implemented in: + +```text +.ci/sound.sh +``` + +It verifies: + +1. Linux boots and the guest can log in. +2. The VirtIO device binds to the `virtio_snd` driver. +3. ALSA enumerates the VirtIO SoundCard. +4. `aplay -l` enumerates the playback PCM device. +5. `speaker-test` completes one mono playback loop using the known working + 48 kHz S16_LE configuration with an explicitly sized buffer and period. + +The playback test uses a bounded loop count instead of terminating +`speaker-test` with an external timeout so slower emulator or CI environments +do not fail merely because PCM playback takes longer than native execution. + +## Future Work +1. Implement PCM capture over the RX queue. +2. Implement device-generated notifications through the event queue. +3. Replace periodic sound interrupt polling with event-driven wakeup. From f08451014b24e6f8201bbaa1d33d7b1654699d4c Mon Sep 17 00:00:00 2001 From: CHENG-YU TSAI Date: Tue, 29 Sep 2026 01:38:03 +0800 Subject: [PATCH 4/4] Initialize Linux image submodules 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. --- mk/tools.mk | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mk/tools.mk b/mk/tools.mk index b9501a685..f0816ca64 100644 --- a/mk/tools.mk +++ b/mk/tools.mk @@ -43,6 +43,9 @@ TOOLS_BIN += $(HIST_BIN) # Build Linux image LINUX_IMAGE_SRC = $(BUILDROOT_DATA) $(LINUX_DATA) $(SIMPLEFS_DATA) build-linux-image: $(LINUX_IMAGE_SRC) + $(Q)git submodule update --init \ + ./tests/system/br_pkgs/doom_riscv \ + ./tests/system/br_pkgs/quake $(Q)./tools/build-linux-image.sh $(Q)$(PRINTF) "Build done.\n"