diff options
Diffstat (limited to 'hw/audio/virtio-snd.c')
-rw-r--r-- | hw/audio/virtio-snd.c | 288 |
1 files changed, 283 insertions, 5 deletions
diff --git a/hw/audio/virtio-snd.c b/hw/audio/virtio-snd.c index 9cff724f62..6c91d0a740 100644 --- a/hw/audio/virtio-snd.c +++ b/hw/audio/virtio-snd.c @@ -32,6 +32,10 @@ #define VIRTIO_SOUND_CHMAP_DEFAULT 0 #define VIRTIO_SOUND_HDA_FN_NID 0 +static void virtio_snd_pcm_out_cb(void *data, int available); +static void virtio_snd_process_cmdq(VirtIOSound *s); +static void virtio_snd_pcm_flush(VirtIOSoundPCMStream *stream); + static uint32_t supported_formats = BIT(VIRTIO_SND_PCM_FMT_S8) | BIT(VIRTIO_SND_PCM_FMT_U8) | BIT(VIRTIO_SND_PCM_FMT_S16) @@ -124,6 +128,13 @@ virtio_snd_set_config(VirtIODevice *vdev, const uint8_t *config) } static void +virtio_snd_pcm_buffer_free(VirtIOSoundPCMBuffer *buffer) +{ + g_free(buffer->elem); + g_free(buffer); +} + +static void virtio_snd_ctrl_cmd_free(virtio_snd_ctrl_command *cmd) { g_free(cmd->elem); @@ -396,6 +407,13 @@ static void virtio_snd_get_qemu_audsettings(audsettings *as, */ static void virtio_snd_pcm_close(VirtIOSoundPCMStream *stream) { + if (stream) { + if (stream->info.direction == VIRTIO_SND_D_OUTPUT) { + virtio_snd_pcm_flush(stream); + AUD_close_out(&stream->pcm->snd->card, stream->voice.out); + stream->voice.out = NULL; + } + } } /* @@ -429,6 +447,9 @@ static uint32_t virtio_snd_pcm_prepare(VirtIOSound *s, uint32_t stream_id) stream->id = stream_id; stream->pcm = s->pcm; stream->s = s; + qemu_mutex_init(&stream->queue_mutex); + QSIMPLEQ_INIT(&stream->queue); + QSIMPLEQ_INIT(&stream->invalid); /* * stream_id >= s->snd_conf.streams was checked before so this is @@ -452,6 +473,18 @@ static uint32_t virtio_snd_pcm_prepare(VirtIOSound *s, uint32_t stream_id) stream->positions[1] = VIRTIO_SND_CHMAP_FR; stream->as = as; + if (stream->info.direction == VIRTIO_SND_D_OUTPUT) { + stream->voice.out = AUD_open_out(&s->card, + stream->voice.out, + "virtio-sound.out", + stream, + virtio_snd_pcm_out_cb, + &as); + AUD_set_volume_out(stream->voice.out, 0, 255, 255); + } else { + qemu_log_mask(LOG_UNIMP, "virtio_snd: input/capture is unimplemented."); + } + return cpu_to_le32(VIRTIO_SND_S_OK); } @@ -532,9 +565,17 @@ static void virtio_snd_handle_pcm_start_stop(VirtIOSound *s, cmd->resp.code = cpu_to_le32(VIRTIO_SND_S_OK); trace_virtio_snd_handle_pcm_start_stop(start ? "VIRTIO_SND_R_PCM_START" : "VIRTIO_SND_R_PCM_STOP", stream_id); + stream = virtio_snd_pcm_get_stream(s, stream_id); - if (stream == NULL) { - error_report("Invalid stream id: %"PRIu32, req.stream_id); + if (stream) { + WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) { + stream->active = start; + } + if (stream->info.direction == VIRTIO_SND_D_OUTPUT) { + AUD_set_active_out(stream->voice.out, start); + } + } else { + error_report("Invalid stream id: %"PRIu32, stream_id); cmd->resp.code = cpu_to_le32(VIRTIO_SND_S_BAD_MSG); return; } @@ -542,8 +583,28 @@ static void virtio_snd_handle_pcm_start_stop(VirtIOSound *s, } /* - * Handles VIRTIO_SND_R_PCM_RELEASE. Releases the buffer resources allocated to - * a stream. + * Returns the number of I/O messages that are being processed. + * + * @stream: VirtIOSoundPCMStream + */ +static size_t virtio_snd_pcm_get_io_msgs_count(VirtIOSoundPCMStream *stream) +{ + VirtIOSoundPCMBuffer *buffer, *next; + size_t count = 0; + + WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) { + QSIMPLEQ_FOREACH_SAFE(buffer, &stream->queue, entry, next) { + count += 1; + } + QSIMPLEQ_FOREACH_SAFE(buffer, &stream->invalid, entry, next) { + count += 1; + } + } + return count; +} + +/* + * Handles VIRTIO_SND_R_PCM_RELEASE. * * @s: VirtIOSound device * @cmd: The request command queue element from VirtIOSound cmdq field @@ -584,6 +645,21 @@ static void virtio_snd_handle_pcm_release(VirtIOSound *s, cmd->resp.code = cpu_to_le32(VIRTIO_SND_S_BAD_MSG); return; } + + if (virtio_snd_pcm_get_io_msgs_count(stream)) { + /* + * virtio-v1.2-csd01, 5.14.6.6.5.1, + * Device Requirements: Stream Release + * + * - The device MUST complete all pending I/O messages for the + * specified stream ID. + * - The device MUST NOT complete the control request while there + * are pending I/O messages for the specified stream ID. + */ + trace_virtio_snd_pcm_stream_flush(stream_id); + virtio_snd_pcm_flush(stream); + } + cmd->resp.code = cpu_to_le32(VIRTIO_SND_S_OK); } @@ -739,6 +815,108 @@ static void virtio_snd_handle_event(VirtIODevice *vdev, VirtQueue *vq) } /* + * The tx virtqueue handler. Makes the buffers available to their respective + * streams for consumption. + * + * @vdev: VirtIOSound device + * @vq: tx virtqueue + */ +static void virtio_snd_handle_tx(VirtIODevice *vdev, VirtQueue *vq) +{ + VirtIOSound *s = VIRTIO_SND(vdev); + VirtIOSoundPCMStream *stream = NULL; + VirtIOSoundPCMBuffer *buffer; + VirtQueueElement *elem; + size_t msg_sz, size; + virtio_snd_pcm_xfer hdr; + virtio_snd_pcm_status resp = { 0 }; + uint32_t stream_id; + /* + * If any of the I/O messages are invalid, put them in stream->invalid and + * return them after the for loop. + */ + bool must_empty_invalid_queue = false; + + if (!virtio_queue_ready(vq)) { + return; + } + trace_virtio_snd_handle_xfer(); + + for (;;) { + elem = virtqueue_pop(vq, sizeof(VirtQueueElement)); + if (!elem) { + break; + } + /* get the message hdr object */ + msg_sz = iov_to_buf(elem->out_sg, + elem->out_num, + 0, + &hdr, + sizeof(virtio_snd_pcm_xfer)); + if (msg_sz != sizeof(virtio_snd_pcm_xfer)) { + goto tx_err; + } + stream_id = le32_to_cpu(hdr.stream_id); + + if (stream_id >= s->snd_conf.streams + || s->pcm->streams[stream_id] == NULL) { + goto tx_err; + } + + stream = s->pcm->streams[stream_id]; + if (stream->info.direction != VIRTIO_SND_D_OUTPUT) { + goto tx_err; + } + + WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) { + size = iov_size(elem->out_sg, elem->out_num) - msg_sz; + + buffer = g_malloc0(sizeof(VirtIOSoundPCMBuffer) + size); + buffer->elem = elem; + buffer->populated = false; + buffer->vq = vq; + buffer->size = size; + buffer->offset = 0; + + QSIMPLEQ_INSERT_TAIL(&stream->queue, buffer, entry); + } + continue; + +tx_err: + WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) { + must_empty_invalid_queue = true; + buffer = g_malloc0(sizeof(VirtIOSoundPCMBuffer)); + buffer->elem = elem; + buffer->vq = vq; + QSIMPLEQ_INSERT_TAIL(&stream->invalid, buffer, entry); + } + } + + if (must_empty_invalid_queue) { + WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) { + while (!QSIMPLEQ_EMPTY(&stream->invalid)) { + buffer = QSIMPLEQ_FIRST(&stream->invalid); + + resp.status = cpu_to_le32(VIRTIO_SND_S_BAD_MSG); + iov_from_buf(buffer->elem->in_sg, + buffer->elem->in_num, + 0, + &resp, + sizeof(virtio_snd_pcm_status)); + virtqueue_push(vq, buffer->elem, sizeof(virtio_snd_pcm_status)); + QSIMPLEQ_REMOVE_HEAD(&stream->invalid, entry); + virtio_snd_pcm_buffer_free(buffer); + } + /* + * Notify vq about virtio_snd_pcm_status responses. + * Buffer responses must be notified separately later. + */ + virtio_notify(vdev, vq); + } + } +} + +/* * Stub buffer virtqueue handler. * * @vdev: VirtIOSound device @@ -832,7 +1010,7 @@ static void virtio_snd_realize(DeviceState *dev, Error **errp) vsnd->queues[VIRTIO_SND_VQ_EVENT] = virtio_add_queue(vdev, 64, virtio_snd_handle_event); vsnd->queues[VIRTIO_SND_VQ_TX] = - virtio_add_queue(vdev, 64, virtio_snd_handle_xfer); + virtio_add_queue(vdev, 64, virtio_snd_handle_tx); vsnd->queues[VIRTIO_SND_VQ_RX] = virtio_add_queue(vdev, 64, virtio_snd_handle_xfer); qemu_mutex_init(&vsnd->cmdq_mutex); @@ -856,6 +1034,105 @@ static void virtio_snd_realize(DeviceState *dev, Error **errp) } } +static inline void return_tx_buffer(VirtIOSoundPCMStream *stream, + VirtIOSoundPCMBuffer *buffer) +{ + virtio_snd_pcm_status resp = { 0 }; + resp.status = cpu_to_le32(VIRTIO_SND_S_OK); + resp.latency_bytes = cpu_to_le32((uint32_t)buffer->size); + iov_from_buf(buffer->elem->in_sg, + buffer->elem->in_num, + 0, + &resp, + sizeof(virtio_snd_pcm_status)); + virtqueue_push(buffer->vq, + buffer->elem, + sizeof(virtio_snd_pcm_status)); + virtio_notify(VIRTIO_DEVICE(stream->s), buffer->vq); + QSIMPLEQ_REMOVE(&stream->queue, + buffer, + VirtIOSoundPCMBuffer, + entry); + virtio_snd_pcm_buffer_free(buffer); +} + +/* + * AUD_* output callback. + * + * @data: VirtIOSoundPCMStream stream + * @available: number of bytes that can be written with AUD_write() + */ +static void virtio_snd_pcm_out_cb(void *data, int available) +{ + VirtIOSoundPCMStream *stream = data; + VirtIOSoundPCMBuffer *buffer; + size_t size; + + WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) { + while (!QSIMPLEQ_EMPTY(&stream->queue)) { + buffer = QSIMPLEQ_FIRST(&stream->queue); + if (!virtio_queue_ready(buffer->vq)) { + return; + } + if (!stream->active) { + /* Stream has stopped, so do not perform AUD_write. */ + return_tx_buffer(stream, buffer); + continue; + } + if (!buffer->populated) { + iov_to_buf(buffer->elem->out_sg, + buffer->elem->out_num, + sizeof(virtio_snd_pcm_xfer), + buffer->data, + buffer->size); + buffer->populated = true; + } + for (;;) { + size = AUD_write(stream->voice.out, + buffer->data + buffer->offset, + MIN(buffer->size, available)); + assert(size <= MIN(buffer->size, available)); + if (size == 0) { + /* break out of both loops */ + available = 0; + break; + } + buffer->size -= size; + buffer->offset += size; + available -= size; + if (buffer->size < 1) { + return_tx_buffer(stream, buffer); + break; + } + if (!available) { + break; + } + } + if (!available) { + break; + } + } + } +} + +/* + * Flush all buffer data from this stream's queue into the driver's virtual + * queue. + * + * @stream: VirtIOSoundPCMStream *stream + */ +static inline void virtio_snd_pcm_flush(VirtIOSoundPCMStream *stream) +{ + VirtIOSoundPCMBuffer *buffer; + + WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) { + while (!QSIMPLEQ_EMPTY(&stream->queue)) { + buffer = QSIMPLEQ_FIRST(&stream->queue); + return_tx_buffer(stream, buffer); + } + } +} + static void virtio_snd_unrealize(DeviceState *dev) { VirtIODevice *vdev = VIRTIO_DEVICE(dev); @@ -872,6 +1149,7 @@ static void virtio_snd_unrealize(DeviceState *dev) if (stream) { virtio_snd_process_cmdq(stream->s); virtio_snd_pcm_close(stream); + qemu_mutex_destroy(&stream->queue_mutex); g_free(stream); } } |