[PATCH v6 0/9] ALSA: add virtio sound driver
This series implements a driver part of the virtio sound device specification v8 [1].
The driver supports PCM playback and capture substreams, jack and channel map controls. A message-based transport is used to write/read PCM frames to/from a device.
As a device part was used OpenSynergy proprietary implementation.
v6 changes: - Code readability improvements suggested by Takashi. - Added additional comments about PCM message lifetime. - Temporarily removed DEVICE_NEEDS_RESET device status handling. Some kind of assistance from the virtio bus/level is required. In turn, this requires a separate discussion of the possibility of implementing such handling for virtio devices whose configuration may change (which may require re-probing, like in case of the sound device).
[1] https://lists.oasis-open.org/archives/virtio-dev/202003/msg00185.html
Anton Yakovlev (9): uapi: virtio_ids: add a sound device type ID from OASIS spec ALSA: virtio: add virtio sound driver ALSA: virtio: handling control messages ALSA: virtio: build PCM devices and substream hardware descriptors ALSA: virtio: handling control and I/O messages for the PCM device ALSA: virtio: PCM substream operators ALSA: virtio: introduce jack support ALSA: virtio: introduce PCM channel map support ALSA: virtio: introduce device suspend/resume support
MAINTAINERS | 9 + include/uapi/linux/virtio_ids.h | 1 + include/uapi/linux/virtio_snd.h | 334 +++++++++++++++++++++ sound/Kconfig | 2 + sound/Makefile | 3 +- sound/virtio/Kconfig | 10 + sound/virtio/Makefile | 13 + sound/virtio/virtio_card.c | 410 ++++++++++++++++++++++++++ sound/virtio/virtio_card.h | 111 +++++++ sound/virtio/virtio_chmap.c | 219 ++++++++++++++ sound/virtio/virtio_ctl_msg.c | 310 ++++++++++++++++++++ sound/virtio/virtio_ctl_msg.h | 78 +++++ sound/virtio/virtio_jack.c | 233 +++++++++++++++ sound/virtio/virtio_pcm.c | 498 ++++++++++++++++++++++++++++++++ sound/virtio/virtio_pcm.h | 118 ++++++++ sound/virtio/virtio_pcm_msg.c | 417 ++++++++++++++++++++++++++ sound/virtio/virtio_pcm_ops.c | 478 ++++++++++++++++++++++++++++++ 17 files changed, 3243 insertions(+), 1 deletion(-) create mode 100644 include/uapi/linux/virtio_snd.h create mode 100644 sound/virtio/Kconfig create mode 100644 sound/virtio/Makefile create mode 100644 sound/virtio/virtio_card.c create mode 100644 sound/virtio/virtio_card.h create mode 100644 sound/virtio/virtio_chmap.c create mode 100644 sound/virtio/virtio_ctl_msg.c create mode 100644 sound/virtio/virtio_ctl_msg.h create mode 100644 sound/virtio/virtio_jack.c create mode 100644 sound/virtio/virtio_pcm.c create mode 100644 sound/virtio/virtio_pcm.h create mode 100644 sound/virtio/virtio_pcm_msg.c create mode 100644 sound/virtio/virtio_pcm_ops.c
The OASIS virtio spec defines a sound device type ID that is not present in the header yet.
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com --- include/uapi/linux/virtio_ids.h | 1 + 1 file changed, 1 insertion(+)
diff --git a/include/uapi/linux/virtio_ids.h b/include/uapi/linux/virtio_ids.h index bc1c0621f5ed..029a2e07a7f9 100644 --- a/include/uapi/linux/virtio_ids.h +++ b/include/uapi/linux/virtio_ids.h @@ -51,6 +51,7 @@ #define VIRTIO_ID_PSTORE 22 /* virtio pstore device */ #define VIRTIO_ID_IOMMU 23 /* virtio IOMMU */ #define VIRTIO_ID_MEM 24 /* virtio mem */ +#define VIRTIO_ID_SOUND 25 /* virtio sound */ #define VIRTIO_ID_FS 26 /* virtio filesystem */ #define VIRTIO_ID_PMEM 27 /* virtio pmem */ #define VIRTIO_ID_MAC80211_HWSIM 29 /* virtio mac80211-hwsim */
Introduce skeleton of the virtio sound driver. The driver implements the virtio sound device specification, which has become part of the virtio standard.
Initial initialization of the device, virtqueues and creation of an empty ALSA sound device.
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com --- MAINTAINERS | 9 + include/uapi/linux/virtio_snd.h | 334 ++++++++++++++++++++++++++++++++ sound/Kconfig | 2 + sound/Makefile | 3 +- sound/virtio/Kconfig | 10 + sound/virtio/Makefile | 7 + sound/virtio/virtio_card.c | 289 +++++++++++++++++++++++++++ sound/virtio/virtio_card.h | 65 +++++++ 8 files changed, 718 insertions(+), 1 deletion(-) create mode 100644 include/uapi/linux/virtio_snd.h create mode 100644 sound/virtio/Kconfig create mode 100644 sound/virtio/Makefile create mode 100644 sound/virtio/virtio_card.c create mode 100644 sound/virtio/virtio_card.h
diff --git a/MAINTAINERS b/MAINTAINERS index c71664ca8bfd..4369946434ad 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19049,6 +19049,15 @@ W: https://virtio-mem.gitlab.io/ F: drivers/virtio/virtio_mem.c F: include/uapi/linux/virtio_mem.h
+VIRTIO SOUND DRIVER +M: Anton Yakovlev anton.yakovlev@opensynergy.com +M: "Michael S. Tsirkin" mst@redhat.com +L: virtualization@lists.linux-foundation.org +L: alsa-devel@alsa-project.org (moderated for non-subscribers) +S: Maintained +F: include/uapi/linux/virtio_snd.h +F: sound/virtio/* + VIRTUAL BOX GUEST DEVICE DRIVER M: Hans de Goede hdegoede@redhat.com M: Arnd Bergmann arnd@arndb.de diff --git a/include/uapi/linux/virtio_snd.h b/include/uapi/linux/virtio_snd.h new file mode 100644 index 000000000000..dfe49547a7b0 --- /dev/null +++ b/include/uapi/linux/virtio_snd.h @@ -0,0 +1,334 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/* + * Copyright (C) 2021 OpenSynergy GmbH + */ +#ifndef VIRTIO_SND_IF_H +#define VIRTIO_SND_IF_H + +#include <linux/virtio_types.h> + +/******************************************************************************* + * CONFIGURATION SPACE + */ +struct virtio_snd_config { + /* # of available physical jacks */ + __le32 jacks; + /* # of available PCM streams */ + __le32 streams; + /* # of available channel maps */ + __le32 chmaps; +}; + +enum { + /* device virtqueue indexes */ + VIRTIO_SND_VQ_CONTROL = 0, + VIRTIO_SND_VQ_EVENT, + VIRTIO_SND_VQ_TX, + VIRTIO_SND_VQ_RX, + /* # of device virtqueues */ + VIRTIO_SND_VQ_MAX +}; + +/******************************************************************************* + * COMMON DEFINITIONS + */ + +/* supported dataflow directions */ +enum { + VIRTIO_SND_D_OUTPUT = 0, + VIRTIO_SND_D_INPUT +}; + +enum { + /* jack control request types */ + VIRTIO_SND_R_JACK_INFO = 1, + VIRTIO_SND_R_JACK_REMAP, + + /* 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 +}; + +/* common header */ +struct virtio_snd_hdr { + __le32 code; +}; + +/* event notification */ +struct virtio_snd_event { + /* VIRTIO_SND_EVT_XXX */ + struct virtio_snd_hdr hdr; + /* optional event data */ + __le32 data; +}; + +/* common control request to query an item information */ +struct virtio_snd_query_info { + /* VIRTIO_SND_R_XXX_INFO */ + struct virtio_snd_hdr hdr; + /* item start identifier */ + __le32 start_id; + /* item count to query */ + __le32 count; + /* item information size in bytes */ + __le32 size; +}; + +/* common item information header */ +struct virtio_snd_info { + /* function group node id (High Definition Audio Specification 7.1.2) */ + __le32 hda_fn_nid; +}; + +/******************************************************************************* + * JACK CONTROL MESSAGES + */ +struct virtio_snd_jack_hdr { + /* VIRTIO_SND_R_JACK_XXX */ + struct virtio_snd_hdr hdr; + /* 0 ... virtio_snd_config::jacks - 1 */ + __le32 jack_id; +}; + +/* supported jack features */ +enum { + VIRTIO_SND_JACK_F_REMAP = 0 +}; + +struct virtio_snd_jack_info { + /* common header */ + struct virtio_snd_info hdr; + /* supported feature bit map (1 << VIRTIO_SND_JACK_F_XXX) */ + __le32 features; + /* pin configuration (High Definition Audio Specification 7.3.3.31) */ + __le32 hda_reg_defconf; + /* pin capabilities (High Definition Audio Specification 7.3.4.9) */ + __le32 hda_reg_caps; + /* current jack connection status (0: disconnected, 1: connected) */ + __u8 connected; + + __u8 padding[7]; +}; + +/* jack remapping control request */ +struct virtio_snd_jack_remap { + /* .code = VIRTIO_SND_R_JACK_REMAP */ + struct virtio_snd_jack_hdr hdr; + /* selected association number */ + __le32 association; + /* selected sequence number */ + __le32 sequence; +}; + +/******************************************************************************* + * PCM CONTROL MESSAGES + */ +struct virtio_snd_pcm_hdr { + /* VIRTIO_SND_R_PCM_XXX */ + struct virtio_snd_hdr hdr; + /* 0 ... virtio_snd_config::streams - 1 */ + __le32 stream_id; +}; + +/* supported PCM stream features */ +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 +}; + +/* supported PCM sample formats */ +enum { + /* analog formats (width / physical width) */ + VIRTIO_SND_PCM_FMT_IMA_ADPCM = 0, /* 4 / 4 bits */ + VIRTIO_SND_PCM_FMT_MU_LAW, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_A_LAW, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_S8, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_U8, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_S16, /* 16 / 16 bits */ + VIRTIO_SND_PCM_FMT_U16, /* 16 / 16 bits */ + VIRTIO_SND_PCM_FMT_S18_3, /* 18 / 24 bits */ + VIRTIO_SND_PCM_FMT_U18_3, /* 18 / 24 bits */ + VIRTIO_SND_PCM_FMT_S20_3, /* 20 / 24 bits */ + VIRTIO_SND_PCM_FMT_U20_3, /* 20 / 24 bits */ + VIRTIO_SND_PCM_FMT_S24_3, /* 24 / 24 bits */ + VIRTIO_SND_PCM_FMT_U24_3, /* 24 / 24 bits */ + VIRTIO_SND_PCM_FMT_S20, /* 20 / 32 bits */ + VIRTIO_SND_PCM_FMT_U20, /* 20 / 32 bits */ + VIRTIO_SND_PCM_FMT_S24, /* 24 / 32 bits */ + VIRTIO_SND_PCM_FMT_U24, /* 24 / 32 bits */ + VIRTIO_SND_PCM_FMT_S32, /* 32 / 32 bits */ + VIRTIO_SND_PCM_FMT_U32, /* 32 / 32 bits */ + VIRTIO_SND_PCM_FMT_FLOAT, /* 32 / 32 bits */ + VIRTIO_SND_PCM_FMT_FLOAT64, /* 64 / 64 bits */ + /* digital formats (width / physical width) */ + VIRTIO_SND_PCM_FMT_DSD_U8, /* 8 / 8 bits */ + VIRTIO_SND_PCM_FMT_DSD_U16, /* 16 / 16 bits */ + VIRTIO_SND_PCM_FMT_DSD_U32, /* 32 / 32 bits */ + VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME /* 32 / 32 bits */ +}; + +/* supported PCM frame rates */ +enum { + VIRTIO_SND_PCM_RATE_5512 = 0, + VIRTIO_SND_PCM_RATE_8000, + VIRTIO_SND_PCM_RATE_11025, + VIRTIO_SND_PCM_RATE_16000, + VIRTIO_SND_PCM_RATE_22050, + VIRTIO_SND_PCM_RATE_32000, + VIRTIO_SND_PCM_RATE_44100, + VIRTIO_SND_PCM_RATE_48000, + VIRTIO_SND_PCM_RATE_64000, + VIRTIO_SND_PCM_RATE_88200, + VIRTIO_SND_PCM_RATE_96000, + VIRTIO_SND_PCM_RATE_176400, + VIRTIO_SND_PCM_RATE_192000, + VIRTIO_SND_PCM_RATE_384000 +}; + +struct virtio_snd_pcm_info { + /* common header */ + struct virtio_snd_info hdr; + /* supported feature bit map (1 << VIRTIO_SND_PCM_F_XXX) */ + __le32 features; + /* supported sample format bit map (1 << VIRTIO_SND_PCM_FMT_XXX) */ + __le64 formats; + /* supported frame rate bit map (1 << VIRTIO_SND_PCM_RATE_XXX) */ + __le64 rates; + /* dataflow direction (VIRTIO_SND_D_XXX) */ + __u8 direction; + /* minimum # of supported channels */ + __u8 channels_min; + /* maximum # of supported channels */ + __u8 channels_max; + + __u8 padding[5]; +}; + +/* set PCM stream format */ +struct virtio_snd_pcm_set_params { + /* .code = VIRTIO_SND_R_PCM_SET_PARAMS */ + struct virtio_snd_pcm_hdr hdr; + /* size of the hardware buffer */ + __le32 buffer_bytes; + /* size of the hardware period */ + __le32 period_bytes; + /* selected feature bit map (1 << VIRTIO_SND_PCM_F_XXX) */ + __le32 features; + /* selected # of channels */ + __u8 channels; + /* selected sample format (VIRTIO_SND_PCM_FMT_XXX) */ + __u8 format; + /* selected frame rate (VIRTIO_SND_PCM_RATE_XXX) */ + __u8 rate; + + __u8 padding; +}; + +/******************************************************************************* + * PCM I/O MESSAGES + */ + +/* I/O request header */ +struct virtio_snd_pcm_xfer { + /* 0 ... virtio_snd_config::streams - 1 */ + __le32 stream_id; +}; + +/* I/O request status */ +struct virtio_snd_pcm_status { + /* VIRTIO_SND_S_XXX */ + __le32 status; + /* current device latency */ + __le32 latency_bytes; +}; + +/******************************************************************************* + * CHANNEL MAP CONTROL MESSAGES + */ +struct virtio_snd_chmap_hdr { + /* VIRTIO_SND_R_CHMAP_XXX */ + struct virtio_snd_hdr hdr; + /* 0 ... virtio_snd_config::chmaps - 1 */ + __le32 chmap_id; +}; + +/* standard channel position definition */ +enum { + VIRTIO_SND_CHMAP_NONE = 0, /* undefined */ + VIRTIO_SND_CHMAP_NA, /* silent */ + VIRTIO_SND_CHMAP_MONO, /* mono stream */ + VIRTIO_SND_CHMAP_FL, /* front left */ + VIRTIO_SND_CHMAP_FR, /* front right */ + VIRTIO_SND_CHMAP_RL, /* rear left */ + VIRTIO_SND_CHMAP_RR, /* rear right */ + VIRTIO_SND_CHMAP_FC, /* front center */ + VIRTIO_SND_CHMAP_LFE, /* low frequency (LFE) */ + VIRTIO_SND_CHMAP_SL, /* side left */ + VIRTIO_SND_CHMAP_SR, /* side right */ + VIRTIO_SND_CHMAP_RC, /* rear center */ + VIRTIO_SND_CHMAP_FLC, /* front left center */ + VIRTIO_SND_CHMAP_FRC, /* front right center */ + VIRTIO_SND_CHMAP_RLC, /* rear left center */ + VIRTIO_SND_CHMAP_RRC, /* rear right center */ + VIRTIO_SND_CHMAP_FLW, /* front left wide */ + VIRTIO_SND_CHMAP_FRW, /* front right wide */ + VIRTIO_SND_CHMAP_FLH, /* front left high */ + VIRTIO_SND_CHMAP_FCH, /* front center high */ + VIRTIO_SND_CHMAP_FRH, /* front right high */ + VIRTIO_SND_CHMAP_TC, /* top center */ + VIRTIO_SND_CHMAP_TFL, /* top front left */ + VIRTIO_SND_CHMAP_TFR, /* top front right */ + VIRTIO_SND_CHMAP_TFC, /* top front center */ + VIRTIO_SND_CHMAP_TRL, /* top rear left */ + VIRTIO_SND_CHMAP_TRR, /* top rear right */ + VIRTIO_SND_CHMAP_TRC, /* top rear center */ + VIRTIO_SND_CHMAP_TFLC, /* top front left center */ + VIRTIO_SND_CHMAP_TFRC, /* top front right center */ + VIRTIO_SND_CHMAP_TSL, /* top side left */ + VIRTIO_SND_CHMAP_TSR, /* top side right */ + VIRTIO_SND_CHMAP_LLFE, /* left LFE */ + VIRTIO_SND_CHMAP_RLFE, /* right LFE */ + VIRTIO_SND_CHMAP_BC, /* bottom center */ + VIRTIO_SND_CHMAP_BLC, /* bottom left center */ + VIRTIO_SND_CHMAP_BRC /* bottom right center */ +}; + +/* maximum possible number of channels */ +#define VIRTIO_SND_CHMAP_MAX_SIZE 18 + +struct virtio_snd_chmap_info { + /* common header */ + struct virtio_snd_info hdr; + /* dataflow direction (VIRTIO_SND_D_XXX) */ + __u8 direction; + /* # of valid channel position values */ + __u8 channels; + /* channel position values (VIRTIO_SND_CHMAP_XXX) */ + __u8 positions[VIRTIO_SND_CHMAP_MAX_SIZE]; +}; + +#endif /* VIRTIO_SND_IF_H */ diff --git a/sound/Kconfig b/sound/Kconfig index 36785410fbe1..e56d96d2b11c 100644 --- a/sound/Kconfig +++ b/sound/Kconfig @@ -99,6 +99,8 @@ source "sound/synth/Kconfig"
source "sound/xen/Kconfig"
+source "sound/virtio/Kconfig" + endif # SND
endif # !UML diff --git a/sound/Makefile b/sound/Makefile index 797ecdcd35e2..04ef04b1168f 100644 --- a/sound/Makefile +++ b/sound/Makefile @@ -5,7 +5,8 @@ obj-$(CONFIG_SOUND) += soundcore.o obj-$(CONFIG_DMASOUND) += oss/dmasound/ obj-$(CONFIG_SND) += core/ i2c/ drivers/ isa/ pci/ ppc/ arm/ sh/ synth/ usb/ \ - firewire/ sparc/ spi/ parisc/ pcmcia/ mips/ soc/ atmel/ hda/ x86/ xen/ + firewire/ sparc/ spi/ parisc/ pcmcia/ mips/ soc/ atmel/ hda/ x86/ xen/ \ + virtio/ obj-$(CONFIG_SND_AOA) += aoa/
# This one must be compilable even if sound is configured out diff --git a/sound/virtio/Kconfig b/sound/virtio/Kconfig new file mode 100644 index 000000000000..094cba24ee5b --- /dev/null +++ b/sound/virtio/Kconfig @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Sound card driver for virtio + +config SND_VIRTIO + tristate "Virtio sound driver" + depends on VIRTIO + select SND_PCM + select SND_JACK + help + This is the virtual sound driver for virtio. Say Y or M. diff --git a/sound/virtio/Makefile b/sound/virtio/Makefile new file mode 100644 index 000000000000..8c87ebb9982b --- /dev/null +++ b/sound/virtio/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0+ + +obj-$(CONFIG_SND_VIRTIO) += virtio_snd.o + +virtio_snd-objs := \ + virtio_card.o + diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c new file mode 100644 index 000000000000..697cb5f16e4d --- /dev/null +++ b/sound/virtio/virtio_card.c @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * virtio-snd: Virtio sound device + * Copyright (C) 2021 OpenSynergy GmbH + */ +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/virtio_config.h> +#include <sound/initval.h> +#include <uapi/linux/virtio_ids.h> + +#include "virtio_card.h" + +static void virtsnd_remove(struct virtio_device *vdev); + +/** + * virtsnd_event_send() - Add an event to the event queue. + * @vqueue: Underlying event virtqueue. + * @event: Event. + * @notify: Indicates whether or not to send a notification to the device. + * @gfp: Kernel flags for memory allocation. + * + * Context: Any context. + */ +static void virtsnd_event_send(struct virtqueue *vqueue, + struct virtio_snd_event *event, bool notify, + gfp_t gfp) +{ + struct scatterlist sg; + struct scatterlist *psgs[1] = { &sg }; + + /* reset event content */ + memset(event, 0, sizeof(*event)); + + sg_init_one(&sg, event, sizeof(*event)); + + if (virtqueue_add_sgs(vqueue, psgs, 0, 1, event, gfp) || !notify) + return; + + if (virtqueue_kick_prepare(vqueue)) + virtqueue_notify(vqueue); +} + +/** + * virtsnd_event_notify_cb() - Dispatch all reported events from the event queue. + * @vqueue: Underlying event virtqueue. + * + * This callback function is called upon a vring interrupt request from the + * device. + * + * Context: Interrupt context. + */ +static void virtsnd_event_notify_cb(struct virtqueue *vqueue) +{ + struct virtio_snd *snd = vqueue->vdev->priv; + struct virtio_snd_queue *queue = virtsnd_event_queue(snd); + struct virtio_snd_event *event; + u32 length; + unsigned long flags; + + spin_lock_irqsave(&queue->lock, flags); + do { + virtqueue_disable_cb(vqueue); + while ((event = virtqueue_get_buf(vqueue, &length))) + virtsnd_event_send(vqueue, event, true, GFP_ATOMIC); + if (unlikely(virtqueue_is_broken(vqueue))) + break; + } while (!virtqueue_enable_cb(vqueue)); + spin_unlock_irqrestore(&queue->lock, flags); +} + +/** + * virtsnd_find_vqs() - Enumerate and initialize all virtqueues. + * @snd: VirtIO sound device. + * + * After calling this function, the event queue is disabled. + * + * Context: Any context. + * Return: 0 on success, -errno on failure. + */ +static int virtsnd_find_vqs(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + static vq_callback_t *callbacks[VIRTIO_SND_VQ_MAX] = { + [VIRTIO_SND_VQ_EVENT] = virtsnd_event_notify_cb + }; + static const char *names[VIRTIO_SND_VQ_MAX] = { + [VIRTIO_SND_VQ_CONTROL] = "virtsnd-ctl", + [VIRTIO_SND_VQ_EVENT] = "virtsnd-event", + [VIRTIO_SND_VQ_TX] = "virtsnd-tx", + [VIRTIO_SND_VQ_RX] = "virtsnd-rx" + }; + struct virtqueue *vqs[VIRTIO_SND_VQ_MAX] = { 0 }; + unsigned int i; + unsigned int n; + int rc; + + rc = virtio_find_vqs(vdev, VIRTIO_SND_VQ_MAX, vqs, callbacks, names, + NULL); + if (rc) { + dev_err(&vdev->dev, "failed to initialize virtqueues\n"); + return rc; + } + + for (i = 0; i < VIRTIO_SND_VQ_MAX; ++i) + snd->queues[i].vqueue = vqs[i]; + + /* Allocate events and populate the event queue */ + virtqueue_disable_cb(vqs[VIRTIO_SND_VQ_EVENT]); + + n = virtqueue_get_vring_size(vqs[VIRTIO_SND_VQ_EVENT]); + + snd->event_msgs = kmalloc_array(n, sizeof(*snd->event_msgs), + GFP_KERNEL); + if (!snd->event_msgs) + return -ENOMEM; + + for (i = 0; i < n; ++i) + virtsnd_event_send(vqs[VIRTIO_SND_VQ_EVENT], + &snd->event_msgs[i], false, GFP_KERNEL); + + return 0; +} + +/** + * virtsnd_enable_event_vq() - Enable the event virtqueue. + * @snd: VirtIO sound device. + * + * Context: Any context. + */ +static void virtsnd_enable_event_vq(struct virtio_snd *snd) +{ + struct virtio_snd_queue *queue = virtsnd_event_queue(snd); + + if (!virtqueue_enable_cb(queue->vqueue)) + virtsnd_event_notify_cb(queue->vqueue); +} + +/** + * virtsnd_build_devs() - Read configuration and build ALSA devices. + * @snd: VirtIO sound device. + * + * Context: Any context that permits to sleep. + * Return: 0 on success, -errno on failure. + */ +static int virtsnd_build_devs(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct device *dev = &vdev->dev; + int rc; + + rc = snd_card_new(dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1, + THIS_MODULE, 0, &snd->card); + if (rc < 0) + return rc; + + snd->card->private_data = snd; + + strscpy(snd->card->driver, VIRTIO_SND_CARD_DRIVER, + sizeof(snd->card->driver)); + strscpy(snd->card->shortname, VIRTIO_SND_CARD_NAME, + sizeof(snd->card->shortname)); + if (dev->parent->bus) + snprintf(snd->card->longname, sizeof(snd->card->longname), + VIRTIO_SND_CARD_NAME " at %s/%s/%s", + dev->parent->bus->name, dev_name(dev->parent), + dev_name(dev)); + else + snprintf(snd->card->longname, sizeof(snd->card->longname), + VIRTIO_SND_CARD_NAME " at %s/%s", + dev_name(dev->parent), dev_name(dev)); + + return snd_card_register(snd->card); +} + +/** + * virtsnd_validate() - Validate if the device can be started. + * @vdev: VirtIO parent device. + * + * Context: Any context. + * Return: 0 on success, -EINVAL on failure. + */ +static int virtsnd_validate(struct virtio_device *vdev) +{ + if (!vdev->config->get) { + dev_err(&vdev->dev, "configuration access disabled\n"); + return -EINVAL; + } + + if (!virtio_has_feature(vdev, VIRTIO_F_VERSION_1)) { + dev_err(&vdev->dev, + "device does not comply with spec version 1.x\n"); + return -EINVAL; + } + + return 0; +} + +/** + * virtsnd_probe() - Create and initialize the device. + * @vdev: VirtIO parent device. + * + * Context: Any context that permits to sleep. + * Return: 0 on success, -errno on failure. + */ +static int virtsnd_probe(struct virtio_device *vdev) +{ + struct virtio_snd *snd; + unsigned int i; + int rc; + + snd = devm_kzalloc(&vdev->dev, sizeof(*snd), GFP_KERNEL); + if (!snd) + return -ENOMEM; + + snd->vdev = vdev; + + vdev->priv = snd; + + for (i = 0; i < VIRTIO_SND_VQ_MAX; ++i) + spin_lock_init(&snd->queues[i].lock); + + rc = virtsnd_find_vqs(snd); + if (rc) + goto on_exit; + + virtio_device_ready(vdev); + + rc = virtsnd_build_devs(snd); + if (rc) + goto on_exit; + + virtsnd_enable_event_vq(snd); + +on_exit: + if (rc) + virtsnd_remove(vdev); + + return rc; +} + +/** + * virtsnd_remove() - Remove VirtIO and ALSA devices. + * @vdev: VirtIO parent device. + * + * Context: Any context that permits to sleep. + */ +static void virtsnd_remove(struct virtio_device *vdev) +{ + struct virtio_snd *snd = vdev->priv; + + if (snd->card) + snd_card_free(snd->card); + + vdev->config->del_vqs(vdev); + vdev->config->reset(vdev); + + kfree(snd->event_msgs); +} + +static const struct virtio_device_id id_table[] = { + { VIRTIO_ID_SOUND, VIRTIO_DEV_ANY_ID }, + { 0 }, +}; + +static struct virtio_driver virtsnd_driver = { + .driver.name = KBUILD_MODNAME, + .driver.owner = THIS_MODULE, + .id_table = id_table, + .validate = virtsnd_validate, + .probe = virtsnd_probe, + .remove = virtsnd_remove, +}; + +static int __init init(void) +{ + return register_virtio_driver(&virtsnd_driver); +} +module_init(init); + +static void __exit fini(void) +{ + unregister_virtio_driver(&virtsnd_driver); +} +module_exit(fini); + +MODULE_DEVICE_TABLE(virtio, id_table); +MODULE_DESCRIPTION("Virtio sound card driver"); +MODULE_LICENSE("GPL"); diff --git a/sound/virtio/virtio_card.h b/sound/virtio/virtio_card.h new file mode 100644 index 000000000000..b903b1b12e90 --- /dev/null +++ b/sound/virtio/virtio_card.h @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * virtio-snd: Virtio sound device + * Copyright (C) 2021 OpenSynergy GmbH + */ +#ifndef VIRTIO_SND_CARD_H +#define VIRTIO_SND_CARD_H + +#include <linux/slab.h> +#include <linux/virtio.h> +#include <sound/core.h> +#include <uapi/linux/virtio_snd.h> + +#define VIRTIO_SND_CARD_DRIVER "virtio-snd" +#define VIRTIO_SND_CARD_NAME "VirtIO SoundCard" + +/** + * struct virtio_snd_queue - Virtqueue wrapper structure. + * @lock: Used to synchronize access to a virtqueue. + * @vqueue: Underlying virtqueue. + */ +struct virtio_snd_queue { + spinlock_t lock; + struct virtqueue *vqueue; +}; + +/** + * struct virtio_snd - VirtIO sound card device. + * @vdev: Underlying virtio device. + * @queues: Virtqueue wrappers. + * @card: ALSA sound card. + * @event_msgs: Device events. + */ +struct virtio_snd { + struct virtio_device *vdev; + struct virtio_snd_queue queues[VIRTIO_SND_VQ_MAX]; + struct snd_card *card; + struct virtio_snd_event *event_msgs; +}; + +static inline struct virtio_snd_queue * +virtsnd_control_queue(struct virtio_snd *snd) +{ + return &snd->queues[VIRTIO_SND_VQ_CONTROL]; +} + +static inline struct virtio_snd_queue * +virtsnd_event_queue(struct virtio_snd *snd) +{ + return &snd->queues[VIRTIO_SND_VQ_EVENT]; +} + +static inline struct virtio_snd_queue * +virtsnd_tx_queue(struct virtio_snd *snd) +{ + return &snd->queues[VIRTIO_SND_VQ_TX]; +} + +static inline struct virtio_snd_queue * +virtsnd_rx_queue(struct virtio_snd *snd) +{ + return &snd->queues[VIRTIO_SND_VQ_RX]; +} + +#endif /* VIRTIO_SND_CARD_H */
The control queue can be used by different parts of the driver to send commands to the device. Control messages can be either synchronous or asynchronous. The lifetime of a message is controlled by a reference count.
Introduce a module parameter to set the message completion timeout: msg_timeout_ms [=1000]
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com --- sound/virtio/Makefile | 3 +- sound/virtio/virtio_card.c | 13 ++ sound/virtio/virtio_card.h | 7 + sound/virtio/virtio_ctl_msg.c | 310 ++++++++++++++++++++++++++++++++++ sound/virtio/virtio_ctl_msg.h | 78 +++++++++ 5 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 sound/virtio/virtio_ctl_msg.c create mode 100644 sound/virtio/virtio_ctl_msg.h
diff --git a/sound/virtio/Makefile b/sound/virtio/Makefile index 8c87ebb9982b..dc551e637441 100644 --- a/sound/virtio/Makefile +++ b/sound/virtio/Makefile @@ -3,5 +3,6 @@ obj-$(CONFIG_SND_VIRTIO) += virtio_snd.o
virtio_snd-objs := \ - virtio_card.o + virtio_card.o \ + virtio_ctl_msg.o
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c index 697cb5f16e4d..196cb97087b0 100644 --- a/sound/virtio/virtio_card.c +++ b/sound/virtio/virtio_card.c @@ -11,6 +11,10 @@
#include "virtio_card.h"
+int msg_timeout_ms = MSEC_PER_SEC; +module_param(msg_timeout_ms, int, 0644); +MODULE_PARM_DESC(msg_timeout_ms, "Message completion timeout in milliseconds"); + static void virtsnd_remove(struct virtio_device *vdev);
/** @@ -82,6 +86,7 @@ static int virtsnd_find_vqs(struct virtio_snd *snd) { struct virtio_device *vdev = snd->vdev; static vq_callback_t *callbacks[VIRTIO_SND_VQ_MAX] = { + [VIRTIO_SND_VQ_CONTROL] = virtsnd_ctl_notify_cb, [VIRTIO_SND_VQ_EVENT] = virtsnd_event_notify_cb }; static const char *names[VIRTIO_SND_VQ_MAX] = { @@ -193,6 +198,11 @@ static int virtsnd_validate(struct virtio_device *vdev) return -EINVAL; }
+ if (!msg_timeout_ms) { + dev_err(&vdev->dev, "msg_timeout_ms value cannot be zero\n"); + return -EINVAL; + } + return 0; }
@@ -214,6 +224,7 @@ static int virtsnd_probe(struct virtio_device *vdev) return -ENOMEM;
snd->vdev = vdev; + INIT_LIST_HEAD(&snd->ctl_msgs);
vdev->priv = snd;
@@ -249,6 +260,8 @@ static void virtsnd_remove(struct virtio_device *vdev) { struct virtio_snd *snd = vdev->priv;
+ virtsnd_ctl_msg_cancel_all(snd); + if (snd->card) snd_card_free(snd->card);
diff --git a/sound/virtio/virtio_card.h b/sound/virtio/virtio_card.h index b903b1b12e90..349311a30199 100644 --- a/sound/virtio/virtio_card.h +++ b/sound/virtio/virtio_card.h @@ -11,6 +11,8 @@ #include <sound/core.h> #include <uapi/linux/virtio_snd.h>
+#include "virtio_ctl_msg.h" + #define VIRTIO_SND_CARD_DRIVER "virtio-snd" #define VIRTIO_SND_CARD_NAME "VirtIO SoundCard"
@@ -29,15 +31,20 @@ struct virtio_snd_queue { * @vdev: Underlying virtio device. * @queues: Virtqueue wrappers. * @card: ALSA sound card. + * @ctl_msgs: Pending control request list. * @event_msgs: Device events. */ struct virtio_snd { struct virtio_device *vdev; struct virtio_snd_queue queues[VIRTIO_SND_VQ_MAX]; struct snd_card *card; + struct list_head ctl_msgs; struct virtio_snd_event *event_msgs; };
+/* Message completion timeout in milliseconds (module parameter). */ +extern int msg_timeout_ms; + static inline struct virtio_snd_queue * virtsnd_control_queue(struct virtio_snd *snd) { diff --git a/sound/virtio/virtio_ctl_msg.c b/sound/virtio/virtio_ctl_msg.c new file mode 100644 index 000000000000..67a5a0301ba3 --- /dev/null +++ b/sound/virtio/virtio_ctl_msg.c @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * virtio-snd: Virtio sound device + * Copyright (C) 2021 OpenSynergy GmbH + */ +#include <linux/moduleparam.h> +#include <linux/virtio_config.h> + +#include "virtio_card.h" + +/** + * struct virtio_snd_msg - Control message. + * @sg_request: Scattergather list containing a device request (header). + * @sg_response: Scattergather list containing a device response (status). + * @list: Pending message list entry. + * @notify: Request completed notification. + * @ref_count: Reference count used to manage a message lifetime. + */ +struct virtio_snd_msg { + struct scatterlist sg_request; + struct scatterlist sg_response; + struct list_head list; + struct completion notify; + refcount_t ref_count; +}; + +/** + * virtsnd_ctl_msg_ref() - Increment reference counter for the message. + * @msg: Control message. + * + * Context: Any context. + */ +void virtsnd_ctl_msg_ref(struct virtio_snd_msg *msg) +{ + refcount_inc(&msg->ref_count); +} + +/** + * virtsnd_ctl_msg_unref() - Decrement reference counter for the message. + * @msg: Control message. + * + * The message will be freed when the ref_count value is 0. + * + * Context: Any context. + */ +void virtsnd_ctl_msg_unref(struct virtio_snd_msg *msg) +{ + if (refcount_dec_and_test(&msg->ref_count)) + kfree(msg); +} + +/** + * virtsnd_ctl_msg_request() - Get a pointer to the request header. + * @msg: Control message. + * + * Context: Any context. + */ +void *virtsnd_ctl_msg_request(struct virtio_snd_msg *msg) +{ + return sg_virt(&msg->sg_request); +} + +/** + * virtsnd_ctl_msg_request() - Get a pointer to the response header. + * @msg: Control message. + * + * Context: Any context. + */ +void *virtsnd_ctl_msg_response(struct virtio_snd_msg *msg) +{ + return sg_virt(&msg->sg_response); +} + +/** + * virtsnd_ctl_msg_alloc() - Allocate and initialize a control message. + * @request_size: Size of request header. + * @response_size: Size of response header. + * @gfp: Kernel flags for memory allocation. + * + * The message will be automatically freed when the ref_count value is 0. + * + * Context: Any context. May sleep if @gfp flags permit. + * Return: Allocated message on success, NULL on failure. + */ +struct virtio_snd_msg *virtsnd_ctl_msg_alloc(size_t request_size, + size_t response_size, gfp_t gfp) +{ + struct virtio_snd_msg *msg; + + if (!request_size || !response_size) + return NULL; + + msg = kzalloc(sizeof(*msg) + request_size + response_size, gfp); + if (!msg) + return NULL; + + sg_init_one(&msg->sg_request, (u8 *)msg + sizeof(*msg), request_size); + sg_init_one(&msg->sg_response, (u8 *)msg + sizeof(*msg) + request_size, + response_size); + + INIT_LIST_HEAD(&msg->list); + init_completion(&msg->notify); + /* This reference is dropped in virtsnd_ctl_msg_complete(). */ + refcount_set(&msg->ref_count, 1); + + return msg; +} + +/** + * virtsnd_ctl_msg_send() - Send a control message. + * @snd: VirtIO sound device. + * @msg: Control message. + * @out_sgs: Additional sg-list to attach to the request header (may be NULL). + * @in_sgs: Additional sg-list to attach to the response header (may be NULL). + * @nowait: Flag indicating whether to wait for completion. + * + * Context: Any context. Takes and releases the control queue spinlock. + * May sleep if @nowait is false. + * Return: 0 on success, -errno on failure. + */ +int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg, + struct scatterlist *out_sgs, + struct scatterlist *in_sgs, bool nowait) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_queue *queue = virtsnd_control_queue(snd); + unsigned int js = msecs_to_jiffies(msg_timeout_ms); + struct virtio_snd_hdr *request = virtsnd_ctl_msg_request(msg); + struct virtio_snd_hdr *response = virtsnd_ctl_msg_response(msg); + unsigned int nouts = 0; + unsigned int nins = 0; + struct scatterlist *psgs[4]; + bool notify = false; + unsigned long flags; + int rc; + + virtsnd_ctl_msg_ref(msg); + + /* Set the default status in case the message was canceled. */ + response->code = cpu_to_le32(VIRTIO_SND_S_IO_ERR); + + psgs[nouts++] = &msg->sg_request; + if (out_sgs) + psgs[nouts++] = out_sgs; + + psgs[nouts + nins++] = &msg->sg_response; + if (in_sgs) + psgs[nouts + nins++] = in_sgs; + + spin_lock_irqsave(&queue->lock, flags); + rc = virtqueue_add_sgs(queue->vqueue, psgs, nouts, nins, msg, + GFP_ATOMIC); + if (!rc) { + notify = virtqueue_kick_prepare(queue->vqueue); + + list_add_tail(&msg->list, &snd->ctl_msgs); + } + spin_unlock_irqrestore(&queue->lock, flags); + + if (rc) { + dev_err(&vdev->dev, "failed to send control message (0x%08x)\n", + le32_to_cpu(request->code)); + + /* + * Since in this case virtsnd_ctl_msg_complete() will not be + * called, it is necessary to decrement the reference count. + */ + virtsnd_ctl_msg_unref(msg); + + goto on_exit; + } + + if (notify) + virtqueue_notify(queue->vqueue); + + if (nowait) + goto on_exit; + + rc = wait_for_completion_interruptible_timeout(&msg->notify, js); + if (rc <= 0) { + if (!rc) { + dev_err(&vdev->dev, + "control message (0x%08x) timeout\n", + le32_to_cpu(request->code)); + rc = -ETIMEDOUT; + } + + goto on_exit; + } + + switch (le32_to_cpu(response->code)) { + case VIRTIO_SND_S_OK: + rc = 0; + break; + case VIRTIO_SND_S_NOT_SUPP: + rc = -EOPNOTSUPP; + break; + case VIRTIO_SND_S_IO_ERR: + rc = -EIO; + break; + default: + rc = -EINVAL; + break; + } + +on_exit: + virtsnd_ctl_msg_unref(msg); + + return rc; +} + +/** + * virtsnd_ctl_msg_complete() - Complete a control message. + * @msg: Control message. + * + * Context: Any context. Expects the control queue spinlock to be held by + * caller. + */ +void virtsnd_ctl_msg_complete(struct virtio_snd_msg *msg) +{ + list_del(&msg->list); + complete(&msg->notify); + + virtsnd_ctl_msg_unref(msg); +} + +/** + * virtsnd_ctl_msg_cancel_all() - Cancel all pending control messages. + * @snd: VirtIO sound device. + * + * Context: Any context. + */ +void virtsnd_ctl_msg_cancel_all(struct virtio_snd *snd) +{ + struct virtio_snd_queue *queue = virtsnd_control_queue(snd); + unsigned long flags; + + spin_lock_irqsave(&queue->lock, flags); + while (!list_empty(&snd->ctl_msgs)) { + struct virtio_snd_msg *msg = + list_first_entry(&snd->ctl_msgs, struct virtio_snd_msg, + list); + + virtsnd_ctl_msg_complete(msg); + } + spin_unlock_irqrestore(&queue->lock, flags); +} + +/** + * virtsnd_ctl_query_info() - Query the item configuration from the device. + * @snd: VirtIO sound device. + * @command: Control request code (VIRTIO_SND_R_XXX_INFO). + * @start_id: Item start identifier. + * @count: Item count to query. + * @size: Item information size in bytes. + * @info: Buffer for storing item information. + * + * Context: Any context that permits to sleep. + * Return: 0 on success, -errno on failure. + */ +int virtsnd_ctl_query_info(struct virtio_snd *snd, int command, int start_id, + int count, size_t size, void *info) +{ + struct virtio_snd_msg *msg; + struct virtio_snd_query_info *query; + struct scatterlist sg; + + msg = virtsnd_ctl_msg_alloc(sizeof(*query), + sizeof(struct virtio_snd_hdr), GFP_KERNEL); + if (!msg) + return -ENOMEM; + + query = virtsnd_ctl_msg_request(msg); + query->hdr.code = cpu_to_le32(command); + query->start_id = cpu_to_le32(start_id); + query->count = cpu_to_le32(count); + query->size = cpu_to_le32(size); + + sg_init_one(&sg, info, count * size); + + return virtsnd_ctl_msg_send(snd, msg, NULL, &sg, false); +} + +/** + * virtsnd_ctl_notify_cb() - Process all completed control messages. + * @vqueue: Underlying control virtqueue. + * + * This callback function is called upon a vring interrupt request from the + * device. + * + * Context: Interrupt context. Takes and releases the control queue spinlock. + */ +void virtsnd_ctl_notify_cb(struct virtqueue *vqueue) +{ + struct virtio_snd *snd = vqueue->vdev->priv; + struct virtio_snd_queue *queue = virtsnd_control_queue(snd); + struct virtio_snd_msg *msg; + u32 length; + unsigned long flags; + + spin_lock_irqsave(&queue->lock, flags); + do { + virtqueue_disable_cb(vqueue); + while ((msg = virtqueue_get_buf(vqueue, &length))) + virtsnd_ctl_msg_complete(msg); + if (unlikely(virtqueue_is_broken(vqueue))) + break; + } while (!virtqueue_enable_cb(vqueue)); + spin_unlock_irqrestore(&queue->lock, flags); +} diff --git a/sound/virtio/virtio_ctl_msg.h b/sound/virtio/virtio_ctl_msg.h new file mode 100644 index 000000000000..7f4db044f28e --- /dev/null +++ b/sound/virtio/virtio_ctl_msg.h @@ -0,0 +1,78 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * virtio-snd: Virtio sound device + * Copyright (C) 2021 OpenSynergy GmbH + */ +#ifndef VIRTIO_SND_MSG_H +#define VIRTIO_SND_MSG_H + +#include <linux/atomic.h> +#include <linux/virtio.h> + +struct virtio_snd; +struct virtio_snd_msg; + +void virtsnd_ctl_msg_ref(struct virtio_snd_msg *msg); + +void virtsnd_ctl_msg_unref(struct virtio_snd_msg *msg); + +void *virtsnd_ctl_msg_request(struct virtio_snd_msg *msg); + +void *virtsnd_ctl_msg_response(struct virtio_snd_msg *msg); + +struct virtio_snd_msg *virtsnd_ctl_msg_alloc(size_t request_size, + size_t response_size, gfp_t gfp); + +int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg, + struct scatterlist *out_sgs, + struct scatterlist *in_sgs, bool nowait); + +/** + * virtsnd_ctl_msg_send_sync() - Simplified sending of synchronous message. + * @snd: VirtIO sound device. + * @msg: Control message. + * + * After returning from this function, the message will be deleted. If message + * content is still needed, the caller must additionally to + * virtsnd_ctl_msg_ref/unref() it. + * + * The msg_timeout_ms module parameter defines the message completion timeout. + * If the message is not completed within this time, the function will return an + * error. + * + * Context: Any context that permits to sleep. + * Return: 0 on success, -errno on failure. + * + * The return value is a message status code (VIRTIO_SND_S_XXX) converted to an + * appropriate -errno value. + */ +static inline int virtsnd_ctl_msg_send_sync(struct virtio_snd *snd, + struct virtio_snd_msg *msg) +{ + return virtsnd_ctl_msg_send(snd, msg, NULL, NULL, false); +} + +/** + * virtsnd_ctl_msg_send_async() - Simplified sending of asynchronous message. + * @snd: VirtIO sound device. + * @msg: Control message. + * + * Context: Any context. + * Return: 0 on success, -errno on failure. + */ +static inline int virtsnd_ctl_msg_send_async(struct virtio_snd *snd, + struct virtio_snd_msg *msg) +{ + return virtsnd_ctl_msg_send(snd, msg, NULL, NULL, true); +} + +void virtsnd_ctl_msg_cancel_all(struct virtio_snd *snd); + +void virtsnd_ctl_msg_complete(struct virtio_snd_msg *msg); + +int virtsnd_ctl_query_info(struct virtio_snd *snd, int command, int start_id, + int count, size_t size, void *info); + +void virtsnd_ctl_notify_cb(struct virtqueue *vqueue); + +#endif /* VIRTIO_SND_MSG_H */
On Sat, 27 Feb 2021 09:59:50 +0100, Anton Yakovlev wrote:
--- a/sound/virtio/virtio_card.c +++ b/sound/virtio/virtio_card.c @@ -11,6 +11,10 @@
#include "virtio_card.h"
+int msg_timeout_ms = MSEC_PER_SEC; +module_param(msg_timeout_ms, int, 0644); +MODULE_PARM_DESC(msg_timeout_ms, "Message completion timeout in milliseconds");
If it's a global variable, better to set a prefix to make it unique, and use module_param_named().
And, it should be unsigned int, no? (unless a negative value has any meaning) Otherwise...
- if (!msg_timeout_ms) {
dev_err(&vdev->dev, "msg_timeout_ms value cannot be zero\n");
return -EINVAL;
Here a negative value would pass.
(snip)
+int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
struct scatterlist *out_sgs,
struct scatterlist *in_sgs, bool nowait)
+{
- struct virtio_device *vdev = snd->vdev;
- struct virtio_snd_queue *queue = virtsnd_control_queue(snd);
- unsigned int js = msecs_to_jiffies(msg_timeout_ms);
- struct virtio_snd_hdr *request = virtsnd_ctl_msg_request(msg);
- struct virtio_snd_hdr *response = virtsnd_ctl_msg_response(msg);
- unsigned int nouts = 0;
- unsigned int nins = 0;
- struct scatterlist *psgs[4];
- bool notify = false;
- unsigned long flags;
- int rc;
- virtsnd_ctl_msg_ref(msg);
- /* Set the default status in case the message was canceled. */
- response->code = cpu_to_le32(VIRTIO_SND_S_IO_ERR);
- psgs[nouts++] = &msg->sg_request;
- if (out_sgs)
psgs[nouts++] = out_sgs;
- psgs[nouts + nins++] = &msg->sg_response;
- if (in_sgs)
psgs[nouts + nins++] = in_sgs;
- spin_lock_irqsave(&queue->lock, flags);
- rc = virtqueue_add_sgs(queue->vqueue, psgs, nouts, nins, msg,
GFP_ATOMIC);
It's a bit pity that we have to use GFP_ATOMIC always here... As long as it's in spinlock, it's the only way.
However, this reminds me of another question: may the virtio event be handled in an atomic context, e.g. the period elapsed or xrun events? If so, the current implementation with non-atomic PCM mode is wrong. Since the non-atomic PCM uses mutex instead of spinlock, it'll lead to a sleep-in-atomic in snd_pcm_period_elapsed() handling.
I suggested the non-atomic PCM *iff* the all contexts are sleepable; then the sync can be done in each callback, which makes the code much simpler usually. But you've already implemented the sync via sync_stop call, hence the non-atomic PCM has little benefit by its own.
thanks,
Takashi
On 28.02.2021 12:04, Takashi Iwai wrote:> On Sat, 27 Feb 2021 09:59:50 +0100,
Anton Yakovlev wrote:
--- a/sound/virtio/virtio_card.c +++ b/sound/virtio/virtio_card.c @@ -11,6 +11,10 @@
#include "virtio_card.h"
+int msg_timeout_ms = MSEC_PER_SEC; +module_param(msg_timeout_ms, int, 0644); +MODULE_PARM_DESC(msg_timeout_ms, "Message completion timeout in milliseconds");
If it's a global variable, better to set a prefix to make it unique, and use module_param_named().
Yes, it makes sense.
And, it should be unsigned int, no? (unless a negative value has any meaning) Otherwise...
And yes, it must be unsigned!
if (!msg_timeout_ms) {
dev_err(&vdev->dev, "msg_timeout_ms value cannot be zero\n");
return -EINVAL;
Here a negative value would pass.
(snip)
+int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
struct scatterlist *out_sgs,
struct scatterlist *in_sgs, bool nowait)
+{
struct virtio_device *vdev = snd->vdev;
struct virtio_snd_queue *queue = virtsnd_control_queue(snd);
unsigned int js = msecs_to_jiffies(msg_timeout_ms);
struct virtio_snd_hdr *request = virtsnd_ctl_msg_request(msg);
struct virtio_snd_hdr *response = virtsnd_ctl_msg_response(msg);
unsigned int nouts = 0;
unsigned int nins = 0;
struct scatterlist *psgs[4];
bool notify = false;
unsigned long flags;
int rc;
virtsnd_ctl_msg_ref(msg);
/* Set the default status in case the message was canceled. */
response->code = cpu_to_le32(VIRTIO_SND_S_IO_ERR);
psgs[nouts++] = &msg->sg_request;
if (out_sgs)
psgs[nouts++] = out_sgs;
psgs[nouts + nins++] = &msg->sg_response;
if (in_sgs)
psgs[nouts + nins++] = in_sgs;
spin_lock_irqsave(&queue->lock, flags);
rc = virtqueue_add_sgs(queue->vqueue, psgs, nouts, nins, msg,
GFP_ATOMIC);
It's a bit pity that we have to use GFP_ATOMIC always here... As long as it's in spinlock, it's the only way.
Well, here we have no other choices, since we share the queue with an interrupt handler.
However, this reminds me of another question: may the virtio event be handled in an atomic context, e.g. the period elapsed or xrun events? If so, the current implementation with non-atomic PCM mode is wrong. Since the non-atomic PCM uses mutex instead of spinlock, it'll lead to a sleep-in-atomic in snd_pcm_period_elapsed() handling.
I suggested the non-atomic PCM *iff* the all contexts are sleepable; then the sync can be done in each callback, which makes the code much simpler usually. But you've already implemented the sync via sync_stop call, hence the non-atomic PCM has little benefit by its own.
The device provides 4 separate queues for communication with the driver, and different data is transmitted over these queues:
The control queue (actually shared between all driver components) for sending commands to the device. These requests must be synchronous. For each request, the device must send a response, and we must wait for it. What you can see in PCM ops are exactly sending these commands (set params, prepare, start and so on). But since some ops could be called in atomic context, there was no other choice but to add asynchronous messages and return from the operator without waiting for a response from the device. Because of this, the START command was a headache. We could not say for sure if the substream started at all on the device side. Also, the virtualization overhead was not taken into account (application might think that the substream is already running, but actually it was not).
Then there are 2 queues for sending/receiving PCM frames. These contain i/o messages carrying actual buffer sliced into periods. Actually, snd_pcm_period_elapsed() is called from interrupt handlers here.
And then there is an additional queue for events.
All of these are handled in different ways.
thanks,
Takashi
Like the HDA specification, the virtio sound device specification links PCM substreams, jacks and PCM channel maps into functional groups. For each discovered group, a PCM device is created, the number of which coincides with the group number.
Introduce the module parameters for setting the hardware buffer parameters: pcm_buffer_ms [=160] pcm_periods_min [=2] pcm_periods_max [=16] pcm_period_ms_min [=10] pcm_period_ms_max [=80]
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com --- sound/virtio/Makefile | 3 +- sound/virtio/virtio_card.c | 14 ++ sound/virtio/virtio_card.h | 10 + sound/virtio/virtio_pcm.c | 463 +++++++++++++++++++++++++++++++++++++ sound/virtio/virtio_pcm.h | 70 ++++++ 5 files changed, 559 insertions(+), 1 deletion(-) create mode 100644 sound/virtio/virtio_pcm.c create mode 100644 sound/virtio/virtio_pcm.h
diff --git a/sound/virtio/Makefile b/sound/virtio/Makefile index dc551e637441..69162a545a41 100644 --- a/sound/virtio/Makefile +++ b/sound/virtio/Makefile @@ -4,5 +4,6 @@ obj-$(CONFIG_SND_VIRTIO) += virtio_snd.o
virtio_snd-objs := \ virtio_card.o \ - virtio_ctl_msg.o + virtio_ctl_msg.o \ + virtio_pcm.o
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c index 196cb97087b0..c5e9ceaaa8a0 100644 --- a/sound/virtio/virtio_card.c +++ b/sound/virtio/virtio_card.c @@ -175,6 +175,16 @@ static int virtsnd_build_devs(struct virtio_snd *snd) VIRTIO_SND_CARD_NAME " at %s/%s", dev_name(dev->parent), dev_name(dev));
+ rc = virtsnd_pcm_parse_cfg(snd); + if (rc) + return rc; + + if (snd->nsubstreams) { + rc = virtsnd_pcm_build_devs(snd); + if (rc) + return rc; + } + return snd_card_register(snd->card); }
@@ -203,6 +213,9 @@ static int virtsnd_validate(struct virtio_device *vdev) return -EINVAL; }
+ if (virtsnd_pcm_validate(vdev)) + return -EINVAL; + return 0; }
@@ -225,6 +238,7 @@ static int virtsnd_probe(struct virtio_device *vdev)
snd->vdev = vdev; INIT_LIST_HEAD(&snd->ctl_msgs); + INIT_LIST_HEAD(&snd->pcm_list);
vdev->priv = snd;
diff --git a/sound/virtio/virtio_card.h b/sound/virtio/virtio_card.h index 349311a30199..4431cc8f0445 100644 --- a/sound/virtio/virtio_card.h +++ b/sound/virtio/virtio_card.h @@ -12,9 +12,13 @@ #include <uapi/linux/virtio_snd.h>
#include "virtio_ctl_msg.h" +#include "virtio_pcm.h"
#define VIRTIO_SND_CARD_DRIVER "virtio-snd" #define VIRTIO_SND_CARD_NAME "VirtIO SoundCard" +#define VIRTIO_SND_PCM_NAME "VirtIO PCM" + +struct virtio_pcm_substream;
/** * struct virtio_snd_queue - Virtqueue wrapper structure. @@ -33,6 +37,9 @@ struct virtio_snd_queue { * @card: ALSA sound card. * @ctl_msgs: Pending control request list. * @event_msgs: Device events. + * @pcm_list: VirtIO PCM device list. + * @substreams: VirtIO PCM substreams. + * @nsubstreams: Number of PCM substreams. */ struct virtio_snd { struct virtio_device *vdev; @@ -40,6 +47,9 @@ struct virtio_snd { struct snd_card *card; struct list_head ctl_msgs; struct virtio_snd_event *event_msgs; + struct list_head pcm_list; + struct virtio_pcm_substream *substreams; + u32 nsubstreams; };
/* Message completion timeout in milliseconds (module parameter). */ diff --git a/sound/virtio/virtio_pcm.c b/sound/virtio/virtio_pcm.c new file mode 100644 index 000000000000..4348ead63c14 --- /dev/null +++ b/sound/virtio/virtio_pcm.c @@ -0,0 +1,463 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * virtio-snd: Virtio sound device + * Copyright (C) 2021 OpenSynergy GmbH + */ +#include <linux/moduleparam.h> +#include <linux/virtio_config.h> + +#include "virtio_card.h" + +static unsigned int pcm_buffer_ms = 160; +module_param(pcm_buffer_ms, uint, 0644); +MODULE_PARM_DESC(pcm_buffer_ms, "PCM substream buffer time in milliseconds"); + +static unsigned int pcm_periods_min = 2; +module_param(pcm_periods_min, uint, 0644); +MODULE_PARM_DESC(pcm_periods_min, "Minimum number of PCM periods"); + +static unsigned int pcm_periods_max = 16; +module_param(pcm_periods_max, uint, 0644); +MODULE_PARM_DESC(pcm_periods_max, "Maximum number of PCM periods"); + +static unsigned int pcm_period_ms_min = 10; +module_param(pcm_period_ms_min, uint, 0644); +MODULE_PARM_DESC(pcm_period_ms_min, "Minimum PCM period time in milliseconds"); + +static unsigned int pcm_period_ms_max = 80; +module_param(pcm_period_ms_max, uint, 0644); +MODULE_PARM_DESC(pcm_period_ms_max, "Maximum PCM period time in milliseconds"); + +/* Map for converting VirtIO format to ALSA format. */ +static const snd_pcm_format_t g_v2a_format_map[] = { + [VIRTIO_SND_PCM_FMT_IMA_ADPCM] = SNDRV_PCM_FORMAT_IMA_ADPCM, + [VIRTIO_SND_PCM_FMT_MU_LAW] = SNDRV_PCM_FORMAT_MU_LAW, + [VIRTIO_SND_PCM_FMT_A_LAW] = SNDRV_PCM_FORMAT_A_LAW, + [VIRTIO_SND_PCM_FMT_S8] = SNDRV_PCM_FORMAT_S8, + [VIRTIO_SND_PCM_FMT_U8] = SNDRV_PCM_FORMAT_U8, + [VIRTIO_SND_PCM_FMT_S16] = SNDRV_PCM_FORMAT_S16_LE, + [VIRTIO_SND_PCM_FMT_U16] = SNDRV_PCM_FORMAT_U16_LE, + [VIRTIO_SND_PCM_FMT_S18_3] = SNDRV_PCM_FORMAT_S18_3LE, + [VIRTIO_SND_PCM_FMT_U18_3] = SNDRV_PCM_FORMAT_U18_3LE, + [VIRTIO_SND_PCM_FMT_S20_3] = SNDRV_PCM_FORMAT_S20_3LE, + [VIRTIO_SND_PCM_FMT_U20_3] = SNDRV_PCM_FORMAT_U20_3LE, + [VIRTIO_SND_PCM_FMT_S24_3] = SNDRV_PCM_FORMAT_S24_3LE, + [VIRTIO_SND_PCM_FMT_U24_3] = SNDRV_PCM_FORMAT_U24_3LE, + [VIRTIO_SND_PCM_FMT_S20] = SNDRV_PCM_FORMAT_S20_LE, + [VIRTIO_SND_PCM_FMT_U20] = SNDRV_PCM_FORMAT_U20_LE, + [VIRTIO_SND_PCM_FMT_S24] = SNDRV_PCM_FORMAT_S24_LE, + [VIRTIO_SND_PCM_FMT_U24] = SNDRV_PCM_FORMAT_U24_LE, + [VIRTIO_SND_PCM_FMT_S32] = SNDRV_PCM_FORMAT_S32_LE, + [VIRTIO_SND_PCM_FMT_U32] = SNDRV_PCM_FORMAT_U32_LE, + [VIRTIO_SND_PCM_FMT_FLOAT] = SNDRV_PCM_FORMAT_FLOAT_LE, + [VIRTIO_SND_PCM_FMT_FLOAT64] = SNDRV_PCM_FORMAT_FLOAT64_LE, + [VIRTIO_SND_PCM_FMT_DSD_U8] = SNDRV_PCM_FORMAT_DSD_U8, + [VIRTIO_SND_PCM_FMT_DSD_U16] = SNDRV_PCM_FORMAT_DSD_U16_LE, + [VIRTIO_SND_PCM_FMT_DSD_U32] = SNDRV_PCM_FORMAT_DSD_U32_LE, + [VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME] = + SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE +}; + +/* Map for converting VirtIO frame rate to ALSA frame rate. */ +struct virtsnd_v2a_rate { + unsigned int alsa_bit; + unsigned int rate; +}; + +static const struct virtsnd_v2a_rate g_v2a_rate_map[] = { + [VIRTIO_SND_PCM_RATE_5512] = { SNDRV_PCM_RATE_5512, 5512 }, + [VIRTIO_SND_PCM_RATE_8000] = { SNDRV_PCM_RATE_8000, 8000 }, + [VIRTIO_SND_PCM_RATE_11025] = { SNDRV_PCM_RATE_11025, 11025 }, + [VIRTIO_SND_PCM_RATE_16000] = { SNDRV_PCM_RATE_16000, 16000 }, + [VIRTIO_SND_PCM_RATE_22050] = { SNDRV_PCM_RATE_22050, 22050 }, + [VIRTIO_SND_PCM_RATE_32000] = { SNDRV_PCM_RATE_32000, 32000 }, + [VIRTIO_SND_PCM_RATE_44100] = { SNDRV_PCM_RATE_44100, 44100 }, + [VIRTIO_SND_PCM_RATE_48000] = { SNDRV_PCM_RATE_48000, 48000 }, + [VIRTIO_SND_PCM_RATE_64000] = { SNDRV_PCM_RATE_64000, 64000 }, + [VIRTIO_SND_PCM_RATE_88200] = { SNDRV_PCM_RATE_88200, 88200 }, + [VIRTIO_SND_PCM_RATE_96000] = { SNDRV_PCM_RATE_96000, 96000 }, + [VIRTIO_SND_PCM_RATE_176400] = { SNDRV_PCM_RATE_176400, 176400 }, + [VIRTIO_SND_PCM_RATE_192000] = { SNDRV_PCM_RATE_192000, 192000 } +}; + +/** + * virtsnd_pcm_build_hw() - Parse substream config and build HW descriptor. + * @vss: VirtIO substream. + * @info: VirtIO substream information entry. + * + * Context: Any context. + * Return: 0 on success, -EINVAL if configuration is invalid. + */ +static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *vss, + struct virtio_snd_pcm_info *info) +{ + struct virtio_device *vdev = vss->snd->vdev; + unsigned int i; + u64 values; + size_t sample_max = 0; + size_t sample_min = 0; + + vss->features = le32_to_cpu(info->features); + + /* + * TODO: set SNDRV_PCM_INFO_{BATCH,BLOCK_TRANSFER} if device supports + * only message-based transport. + */ + vss->hw.info = + SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_BATCH | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_PAUSE; + + if (!info->channels_min || info->channels_min > info->channels_max) { + dev_err(&vdev->dev, + "SID %u: invalid channel range [%u %u]\n", + vss->sid, info->channels_min, info->channels_max); + return -EINVAL; + } + + vss->hw.channels_min = info->channels_min; + vss->hw.channels_max = info->channels_max; + + values = le64_to_cpu(info->formats); + + vss->hw.formats = 0; + + for (i = 0; i < ARRAY_SIZE(g_v2a_format_map); ++i) + if (values & (1ULL << i)) { + snd_pcm_format_t alsa_fmt = g_v2a_format_map[i]; + int bytes = snd_pcm_format_physical_width(alsa_fmt) / 8; + + if (!sample_min || sample_min > bytes) + sample_min = bytes; + + if (sample_max < bytes) + sample_max = bytes; + + vss->hw.formats |= (1ULL << (__force int)alsa_fmt); + } + + if (!vss->hw.formats) { + dev_err(&vdev->dev, + "SID %u: no supported PCM sample formats found\n", + vss->sid); + return -EINVAL; + } + + values = le64_to_cpu(info->rates); + + vss->hw.rates = 0; + + for (i = 0; i < ARRAY_SIZE(g_v2a_rate_map); ++i) + if (values & (1ULL << i)) { + if (!vss->hw.rate_min || + vss->hw.rate_min > g_v2a_rate_map[i].rate) + vss->hw.rate_min = g_v2a_rate_map[i].rate; + + if (vss->hw.rate_max < g_v2a_rate_map[i].rate) + vss->hw.rate_max = g_v2a_rate_map[i].rate; + + vss->hw.rates |= g_v2a_rate_map[i].alsa_bit; + } + + if (!vss->hw.rates) { + dev_err(&vdev->dev, + "SID %u: no supported PCM frame rates found\n", + vss->sid); + return -EINVAL; + } + + vss->hw.periods_min = pcm_periods_min; + vss->hw.periods_max = pcm_periods_max; + + /* + * We must ensure that there is enough space in the buffer to store + * pcm_buffer_ms ms for the combination (Cmax, Smax, Rmax), where: + * Cmax = maximum supported number of channels, + * Smax = maximum supported sample size in bytes, + * Rmax = maximum supported frame rate. + */ + vss->hw.buffer_bytes_max = + sample_max * vss->hw.channels_max * pcm_buffer_ms * + (vss->hw.rate_max / MSEC_PER_SEC); + + /* Align the buffer size to the page size */ + vss->hw.buffer_bytes_max = + (vss->hw.buffer_bytes_max + PAGE_SIZE - 1) & -PAGE_SIZE; + + /* + * We must ensure that the minimum period size is enough to store + * pcm_period_ms_min ms for the combination (Cmin, Smin, Rmin), where: + * Cmin = minimum supported number of channels, + * Smin = minimum supported sample size in bytes, + * Rmin = minimum supported frame rate. + */ + vss->hw.period_bytes_min = + sample_min * vss->hw.channels_min * pcm_period_ms_min * + (vss->hw.rate_min / MSEC_PER_SEC); + + /* + * We must ensure that the maximum period size is enough to store + * pcm_period_ms_max ms for the combination (Cmax, Smax, Rmax). + */ + vss->hw.period_bytes_max = + sample_max * vss->hw.channels_max * pcm_period_ms_max * + (vss->hw.rate_max / MSEC_PER_SEC); + + return 0; +} + +/** + * virtsnd_pcm_find() - Find the PCM device for the specified node ID. + * @snd: VirtIO sound device. + * @nid: Function node ID. + * + * Context: Any context. + * Return: a pointer to the PCM device or ERR_PTR(-ENOENT). + */ +struct virtio_pcm *virtsnd_pcm_find(struct virtio_snd *snd, u32 nid) +{ + struct virtio_pcm *vpcm; + + list_for_each_entry(vpcm, &snd->pcm_list, list) + if (vpcm->nid == nid) + return vpcm; + + return ERR_PTR(-ENOENT); +} + +/** + * virtsnd_pcm_find_or_create() - Find or create the PCM device for the + * specified node ID. + * @snd: VirtIO sound device. + * @nid: Function node ID. + * + * Context: Any context that permits to sleep. + * Return: a pointer to the PCM device or ERR_PTR(-errno). + */ +struct virtio_pcm *virtsnd_pcm_find_or_create(struct virtio_snd *snd, u32 nid) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_pcm *vpcm; + + vpcm = virtsnd_pcm_find(snd, nid); + if (!IS_ERR(vpcm)) + return vpcm; + + vpcm = devm_kzalloc(&vdev->dev, sizeof(*vpcm), GFP_KERNEL); + if (!vpcm) + return ERR_PTR(-ENOMEM); + + vpcm->nid = nid; + list_add_tail(&vpcm->list, &snd->pcm_list); + + return vpcm; +} + +/** + * virtsnd_pcm_validate() - Validate if the device can be started. + * @vdev: VirtIO parent device. + * + * Context: Any context. + * Return: 0 on success, -EINVAL on failure. + */ +int virtsnd_pcm_validate(struct virtio_device *vdev) +{ + if (pcm_periods_min < 2 || pcm_periods_min > pcm_periods_max) { + dev_err(&vdev->dev, + "invalid range [%u %u] of the number of PCM periods\n", + pcm_periods_min, pcm_periods_max); + return -EINVAL; + } + + if (!pcm_period_ms_min || pcm_period_ms_min > pcm_period_ms_max) { + dev_err(&vdev->dev, + "invalid range [%u %u] of the size of the PCM period\n", + pcm_period_ms_min, pcm_period_ms_max); + return -EINVAL; + } + + if (pcm_buffer_ms < pcm_periods_min * pcm_period_ms_min) { + dev_err(&vdev->dev, + "pcm_buffer_ms(=%u) value cannot be < %u ms\n", + pcm_buffer_ms, pcm_periods_min * pcm_period_ms_min); + return -EINVAL; + } + + if (pcm_period_ms_max > pcm_buffer_ms / 2) { + dev_err(&vdev->dev, + "pcm_period_ms_max(=%u) value cannot be > %u ms\n", + pcm_period_ms_max, pcm_buffer_ms / 2); + return -EINVAL; + } + + return 0; +} + +/** + * virtsnd_pcm_parse_cfg() - Parse the stream configuration. + * @snd: VirtIO sound device. + * + * This function is called during initial device initialization. + * + * Context: Any context that permits to sleep. + * Return: 0 on success, -errno on failure. + */ +int virtsnd_pcm_parse_cfg(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_pcm_info *info; + u32 i; + int rc; + + virtio_cread_le(vdev, struct virtio_snd_config, streams, + &snd->nsubstreams); + if (!snd->nsubstreams) + return 0; + + snd->substreams = devm_kcalloc(&vdev->dev, snd->nsubstreams, + sizeof(*snd->substreams), GFP_KERNEL); + if (!snd->substreams) + return -ENOMEM; + + info = kcalloc(snd->nsubstreams, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + rc = virtsnd_ctl_query_info(snd, VIRTIO_SND_R_PCM_INFO, 0, + snd->nsubstreams, sizeof(*info), info); + if (rc) + goto on_exit; + + for (i = 0; i < snd->nsubstreams; ++i) { + struct virtio_pcm_substream *vss = &snd->substreams[i]; + struct virtio_pcm *vpcm; + + vss->snd = snd; + vss->sid = i; + + rc = virtsnd_pcm_build_hw(vss, &info[i]); + if (rc) + goto on_exit; + + vss->nid = le32_to_cpu(info[i].hdr.hda_fn_nid); + + vpcm = virtsnd_pcm_find_or_create(snd, vss->nid); + if (IS_ERR(vpcm)) { + rc = PTR_ERR(vpcm); + goto on_exit; + } + + switch (info[i].direction) { + case VIRTIO_SND_D_OUTPUT: + vss->direction = SNDRV_PCM_STREAM_PLAYBACK; + break; + case VIRTIO_SND_D_INPUT: + vss->direction = SNDRV_PCM_STREAM_CAPTURE; + break; + default: + dev_err(&vdev->dev, "SID %u: unknown direction (%u)\n", + vss->sid, info[i].direction); + rc = -EINVAL; + goto on_exit; + } + + vpcm->streams[vss->direction].nsubstreams++; + } + +on_exit: + kfree(info); + + return rc; +} + +/** + * virtsnd_pcm_build_devs() - Build ALSA PCM devices. + * @snd: VirtIO sound device. + * + * Context: Any context that permits to sleep. + * Return: 0 on success, -errno on failure. + */ +int virtsnd_pcm_build_devs(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_pcm *vpcm; + u32 i; + int rc; + + list_for_each_entry(vpcm, &snd->pcm_list, list) { + unsigned int npbs = + vpcm->streams[SNDRV_PCM_STREAM_PLAYBACK].nsubstreams; + unsigned int ncps = + vpcm->streams[SNDRV_PCM_STREAM_CAPTURE].nsubstreams; + + if (!npbs && !ncps) + continue; + + rc = snd_pcm_new(snd->card, VIRTIO_SND_CARD_DRIVER, vpcm->nid, + npbs, ncps, &vpcm->pcm); + if (rc) { + dev_err(&vdev->dev, "snd_pcm_new[%u] failed: %d\n", + vpcm->nid, rc); + return rc; + } + + vpcm->pcm->info_flags = 0; + vpcm->pcm->dev_class = SNDRV_PCM_CLASS_GENERIC; + vpcm->pcm->dev_subclass = SNDRV_PCM_SUBCLASS_GENERIC_MIX; + snprintf(vpcm->pcm->name, sizeof(vpcm->pcm->name), + VIRTIO_SND_PCM_NAME " %u", vpcm->pcm->device); + vpcm->pcm->private_data = vpcm; + vpcm->pcm->nonatomic = true; + + for (i = 0; i < ARRAY_SIZE(vpcm->streams); ++i) { + struct virtio_pcm_stream *stream = &vpcm->streams[i]; + + if (!stream->nsubstreams) + continue; + + stream->substreams = + devm_kcalloc(&vdev->dev, stream->nsubstreams, + sizeof(*stream->substreams), + GFP_KERNEL); + if (!stream->substreams) + return -ENOMEM; + + stream->nsubstreams = 0; + } + } + + for (i = 0; i < snd->nsubstreams; ++i) { + struct virtio_pcm_stream *vs; + struct virtio_pcm_substream *vss = &snd->substreams[i]; + + vpcm = virtsnd_pcm_find(snd, vss->nid); + if (IS_ERR(vpcm)) + return PTR_ERR(vpcm); + + vs = &vpcm->streams[vss->direction]; + vs->substreams[vs->nsubstreams++] = vss; + } + + list_for_each_entry(vpcm, &snd->pcm_list, list) { + for (i = 0; i < ARRAY_SIZE(vpcm->streams); ++i) { + struct virtio_pcm_stream *vs = &vpcm->streams[i]; + struct snd_pcm_str *ks = &vpcm->pcm->streams[i]; + struct snd_pcm_substream *kss; + + if (!vs->nsubstreams) + continue; + + for (kss = ks->substream; kss; kss = kss->next) + vs->substreams[kss->number]->substream = kss; + } + + snd_pcm_set_managed_buffer_all(vpcm->pcm, + SNDRV_DMA_TYPE_VMALLOC, NULL, + 0, 0); + } + + return 0; +} diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h new file mode 100644 index 000000000000..a2aadc44dce9 --- /dev/null +++ b/sound/virtio/virtio_pcm.h @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * virtio-snd: Virtio sound device + * Copyright (C) 2021 OpenSynergy GmbH + */ +#ifndef VIRTIO_SND_PCM_H +#define VIRTIO_SND_PCM_H + +#include <linux/atomic.h> +#include <linux/virtio_config.h> +#include <sound/pcm.h> + +struct virtio_pcm; +struct virtio_pcm_msg; + +/** + * struct virtio_pcm_substream - VirtIO PCM substream. + * @snd: VirtIO sound device. + * @nid: Function group node identifier. + * @sid: Stream identifier. + * @direction: Stream data flow direction (SNDRV_PCM_STREAM_XXX). + * @features: Stream VirtIO feature bit map (1 << VIRTIO_SND_PCM_F_XXX). + * @substream: Kernel ALSA substream. + * @hw: Kernel ALSA substream hardware descriptor. + */ +struct virtio_pcm_substream { + struct virtio_snd *snd; + u32 nid; + u32 sid; + u32 direction; + u32 features; + struct snd_pcm_substream *substream; + struct snd_pcm_hardware hw; +}; + +/** + * struct virtio_pcm_stream - VirtIO PCM stream. + * @substreams: VirtIO substreams belonging to the stream. + * @nsubstreams: Number of substreams. + */ +struct virtio_pcm_stream { + struct virtio_pcm_substream **substreams; + u32 nsubstreams; +}; + +/** + * struct virtio_pcm - VirtIO PCM device. + * @list: VirtIO PCM list entry. + * @nid: Function group node identifier. + * @pcm: Kernel PCM device. + * @streams: VirtIO PCM streams (playback and capture). + */ +struct virtio_pcm { + struct list_head list; + u32 nid; + struct snd_pcm *pcm; + struct virtio_pcm_stream streams[SNDRV_PCM_STREAM_LAST + 1]; +}; + +int virtsnd_pcm_validate(struct virtio_device *vdev); + +int virtsnd_pcm_parse_cfg(struct virtio_snd *snd); + +int virtsnd_pcm_build_devs(struct virtio_snd *snd); + +struct virtio_pcm *virtsnd_pcm_find(struct virtio_snd *snd, u32 nid); + +struct virtio_pcm *virtsnd_pcm_find_or_create(struct virtio_snd *snd, u32 nid); + +#endif /* VIRTIO_SND_PCM_H */
On Sat, 27 Feb 2021 09:59:51 +0100, Anton Yakovlev wrote:
+static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *vss,
struct virtio_snd_pcm_info *info)
+{
....
- for (i = 0; i < ARRAY_SIZE(g_v2a_format_map); ++i)
if (values & (1ULL << i)) {
snd_pcm_format_t alsa_fmt = g_v2a_format_map[i];
int bytes = snd_pcm_format_physical_width(alsa_fmt) / 8;
if (!sample_min || sample_min > bytes)
sample_min = bytes;
if (sample_max < bytes)
sample_max = bytes;
vss->hw.formats |= (1ULL << (__force int)alsa_fmt);
Please use pcm_format_to_bits().
- /* Align the buffer size to the page size */
- vss->hw.buffer_bytes_max =
(vss->hw.buffer_bytes_max + PAGE_SIZE - 1) & -PAGE_SIZE;
You can use PAGE_ALIGN() macro.
thanks,
Takashi
The driver implements a message-based transport for I/O substream operations. Before the start of the substream, the hardware buffer is sliced into I/O messages, the number of which is equal to the current number of periods. The size of each message is equal to the current size of one period.
I/O messages are organized in an ordered queue. The completion of the I/O message indicates an elapsed period (the only exception is the end of the stream for the capture substream). Upon completion, the message is automatically re-added to the end of the queue.
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com --- sound/virtio/Makefile | 3 +- sound/virtio/virtio_card.c | 18 +- sound/virtio/virtio_card.h | 9 + sound/virtio/virtio_pcm.c | 32 +++ sound/virtio/virtio_pcm.h | 40 ++++ sound/virtio/virtio_pcm_msg.c | 417 ++++++++++++++++++++++++++++++++++ 6 files changed, 516 insertions(+), 3 deletions(-) create mode 100644 sound/virtio/virtio_pcm_msg.c
diff --git a/sound/virtio/Makefile b/sound/virtio/Makefile index 69162a545a41..626af3cc3ed7 100644 --- a/sound/virtio/Makefile +++ b/sound/virtio/Makefile @@ -5,5 +5,6 @@ obj-$(CONFIG_SND_VIRTIO) += virtio_snd.o virtio_snd-objs := \ virtio_card.o \ virtio_ctl_msg.o \ - virtio_pcm.o + virtio_pcm.o \ + virtio_pcm_msg.o
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c index c5e9ceaaa8a0..9f74261b234c 100644 --- a/sound/virtio/virtio_card.c +++ b/sound/virtio/virtio_card.c @@ -65,8 +65,16 @@ static void virtsnd_event_notify_cb(struct virtqueue *vqueue) spin_lock_irqsave(&queue->lock, flags); do { virtqueue_disable_cb(vqueue); - while ((event = virtqueue_get_buf(vqueue, &length))) + while ((event = virtqueue_get_buf(vqueue, &length))) { + switch (le32_to_cpu(event->hdr.code)) { + case VIRTIO_SND_EVT_PCM_PERIOD_ELAPSED: + case VIRTIO_SND_EVT_PCM_XRUN: + virtsnd_pcm_event(snd, event); + break; + } + virtsnd_event_send(vqueue, event, true, GFP_ATOMIC); + } if (unlikely(virtqueue_is_broken(vqueue))) break; } while (!virtqueue_enable_cb(vqueue)); @@ -87,7 +95,9 @@ static int virtsnd_find_vqs(struct virtio_snd *snd) struct virtio_device *vdev = snd->vdev; static vq_callback_t *callbacks[VIRTIO_SND_VQ_MAX] = { [VIRTIO_SND_VQ_CONTROL] = virtsnd_ctl_notify_cb, - [VIRTIO_SND_VQ_EVENT] = virtsnd_event_notify_cb + [VIRTIO_SND_VQ_EVENT] = virtsnd_event_notify_cb, + [VIRTIO_SND_VQ_TX] = virtsnd_pcm_tx_notify_cb, + [VIRTIO_SND_VQ_RX] = virtsnd_pcm_rx_notify_cb }; static const char *names[VIRTIO_SND_VQ_MAX] = { [VIRTIO_SND_VQ_CONTROL] = "virtsnd-ctl", @@ -273,6 +283,7 @@ static int virtsnd_probe(struct virtio_device *vdev) static void virtsnd_remove(struct virtio_device *vdev) { struct virtio_snd *snd = vdev->priv; + unsigned int i;
virtsnd_ctl_msg_cancel_all(snd);
@@ -282,6 +293,9 @@ static void virtsnd_remove(struct virtio_device *vdev) vdev->config->del_vqs(vdev); vdev->config->reset(vdev);
+ for (i = 0; snd->substreams && i < snd->nsubstreams; ++i) + virtsnd_pcm_msg_free(&snd->substreams[i]); + kfree(snd->event_msgs); }
diff --git a/sound/virtio/virtio_card.h b/sound/virtio/virtio_card.h index 4431cc8f0445..7e50f3835ab1 100644 --- a/sound/virtio/virtio_card.h +++ b/sound/virtio/virtio_card.h @@ -79,4 +79,13 @@ virtsnd_rx_queue(struct virtio_snd *snd) return &snd->queues[VIRTIO_SND_VQ_RX]; }
+static inline struct virtio_snd_queue * +virtsnd_pcm_queue(struct virtio_pcm_substream *vss) +{ + if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK) + return virtsnd_tx_queue(vss->snd); + else + return virtsnd_rx_queue(vss->snd); +} + #endif /* VIRTIO_SND_CARD_H */ diff --git a/sound/virtio/virtio_pcm.c b/sound/virtio/virtio_pcm.c index 4348ead63c14..3cfd3520a9c0 100644 --- a/sound/virtio/virtio_pcm.c +++ b/sound/virtio/virtio_pcm.c @@ -337,6 +337,8 @@ int virtsnd_pcm_parse_cfg(struct virtio_snd *snd)
vss->snd = snd; vss->sid = i; + init_waitqueue_head(&vss->msg_empty); + spin_lock_init(&vss->lock);
rc = virtsnd_pcm_build_hw(vss, &info[i]); if (rc) @@ -461,3 +463,33 @@ int virtsnd_pcm_build_devs(struct virtio_snd *snd)
return 0; } + +/** + * virtsnd_pcm_event() - Handle the PCM device event notification. + * @snd: VirtIO sound device. + * @event: VirtIO sound event. + * + * Context: Interrupt context. + */ +void virtsnd_pcm_event(struct virtio_snd *snd, struct virtio_snd_event *event) +{ + struct virtio_pcm_substream *vss; + u32 sid = le32_to_cpu(event->data); + + if (sid >= snd->nsubstreams) + return; + + vss = &snd->substreams[sid]; + + switch (le32_to_cpu(event->hdr.code)) { + case VIRTIO_SND_EVT_PCM_PERIOD_ELAPSED: + /* TODO: deal with shmem elapsed period */ + break; + case VIRTIO_SND_EVT_PCM_XRUN: + spin_lock(&vss->lock); + if (vss->xfer_enabled) + vss->xfer_xrun = true; + spin_unlock(&vss->lock); + break; + } +} diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h index a2aadc44dce9..7af41cad4e8f 100644 --- a/sound/virtio/virtio_pcm.h +++ b/sound/virtio/virtio_pcm.h @@ -22,6 +22,17 @@ struct virtio_pcm_msg; * @features: Stream VirtIO feature bit map (1 << VIRTIO_SND_PCM_F_XXX). * @substream: Kernel ALSA substream. * @hw: Kernel ALSA substream hardware descriptor. + * @lock: Spinlock that protects fields shared by interrupt handlers and + * substream operators. + * @buffer_bytes: Current buffer size in bytes. + * @hw_ptr: Substream hardware pointer value in bytes [0 ... buffer_bytes). + * @xfer_enabled: Data transfer state (0 - off, 1 - on). + * @xfer_xrun: Data underflow/overflow state (0 - no xrun, 1 - xrun). + * @msgs: Allocated I/O messages. + * @nmsgs: Number of allocated I/O messages. + * @msg_last_enqueued: Index of the last I/O message added to the virtqueue. + * @msg_count: Number of pending I/O messages in the virtqueue. + * @msg_empty: Notify when msg_count is zero. */ struct virtio_pcm_substream { struct virtio_snd *snd; @@ -31,6 +42,16 @@ struct virtio_pcm_substream { u32 features; struct snd_pcm_substream *substream; struct snd_pcm_hardware hw; + spinlock_t lock; + size_t buffer_bytes; + size_t hw_ptr; + bool xfer_enabled; + bool xfer_xrun; + struct virtio_pcm_msg **msgs; + unsigned int nmsgs; + int msg_last_enqueued; + unsigned int msg_count; + wait_queue_head_t msg_empty; };
/** @@ -63,8 +84,27 @@ int virtsnd_pcm_parse_cfg(struct virtio_snd *snd);
int virtsnd_pcm_build_devs(struct virtio_snd *snd);
+void virtsnd_pcm_event(struct virtio_snd *snd, struct virtio_snd_event *event); + +void virtsnd_pcm_tx_notify_cb(struct virtqueue *vqueue); + +void virtsnd_pcm_rx_notify_cb(struct virtqueue *vqueue); + struct virtio_pcm *virtsnd_pcm_find(struct virtio_snd *snd, u32 nid);
struct virtio_pcm *virtsnd_pcm_find_or_create(struct virtio_snd *snd, u32 nid);
+struct virtio_snd_msg * +virtsnd_pcm_ctl_msg_alloc(struct virtio_pcm_substream *vss, + unsigned int command, gfp_t gfp); + +int virtsnd_pcm_msg_alloc(struct virtio_pcm_substream *vss, + unsigned int periods, unsigned int period_bytes); + +void virtsnd_pcm_msg_free(struct virtio_pcm_substream *vss); + +int virtsnd_pcm_msg_send(struct virtio_pcm_substream *vss); + +unsigned int virtsnd_pcm_msg_pending_num(struct virtio_pcm_substream *vss); + #endif /* VIRTIO_SND_PCM_H */ diff --git a/sound/virtio/virtio_pcm_msg.c b/sound/virtio/virtio_pcm_msg.c new file mode 100644 index 000000000000..549bf8deb14c --- /dev/null +++ b/sound/virtio/virtio_pcm_msg.c @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * virtio-snd: Virtio sound device + * Copyright (C) 2021 OpenSynergy GmbH + */ +#include <sound/pcm_params.h> + +#include "virtio_card.h" + +/** + * struct virtio_pcm_msg - VirtIO I/O message. + * @substream: VirtIO PCM substream. + * @xfer: Request header payload. + * @status: Response header payload. + * @sgs: Payload scatter-gather table. + */ +struct virtio_pcm_msg { + struct virtio_pcm_substream *substream; + struct virtio_snd_pcm_xfer xfer; + struct virtio_snd_pcm_status status; + struct scatterlist sgs[0]; +}; + +/** + * enum pcm_msg_sg_index - Index values for the virtio_pcm_msg->sgs field in + * an I/O message. + * @PCM_MSG_SG_XFER: Element containing a virtio_snd_pcm_xfer structure. + * @PCM_MSG_SG_STATUS: Element containing a virtio_snd_pcm_status structure. + * @PCM_MSG_SG_DATA: The first element containing a data buffer. + */ +enum pcm_msg_sg_index { + PCM_MSG_SG_XFER = 0, + PCM_MSG_SG_STATUS, + PCM_MSG_SG_DATA +}; + +/** + * virtsnd_pcm_sg_num() - Count the number of sg-elements required to represent + * vmalloc'ed buffer. + * @data: Pointer to vmalloc'ed buffer. + * @length: Buffer size. + * + * Context: Any context. + * Return: Number of physically contiguous parts in the @data. + */ +static int virtsnd_pcm_sg_num(u8 *data, unsigned int length) +{ + phys_addr_t sg_address; + unsigned int sg_length; + int num = 0; + + while (length) { + struct page *pg = vmalloc_to_page(data); + phys_addr_t pg_address = page_to_phys(pg); + size_t pg_length; + + pg_length = PAGE_SIZE - offset_in_page(data); + if (pg_length > length) + pg_length = length; + + if (!num || sg_address + sg_length != pg_address) { + sg_address = pg_address; + sg_length = pg_length; + num++; + } else { + sg_length += pg_length; + } + + data += pg_length; + length -= pg_length; + } + + return num; +} + +/** + * virtsnd_pcm_sg_from() - Build sg-list from vmalloc'ed buffer. + * @sgs: Preallocated sg-list to populate. + * @nsgs: The maximum number of elements in the @sgs. + * @data: Pointer to vmalloc'ed buffer. + * @length: Buffer size. + * + * Splits the buffer into physically contiguous parts and makes an sg-list of + * such parts. + * + * Context: Any context. + */ +static void virtsnd_pcm_sg_from(struct scatterlist *sgs, int nsgs, u8 *data, + unsigned int length) +{ + int idx = -1; + + while (length) { + struct page *pg = vmalloc_to_page(data); + size_t pg_length; + + pg_length = PAGE_SIZE - offset_in_page(data); + if (pg_length > length) + pg_length = length; + + if (idx == -1 || + sg_phys(&sgs[idx]) + sgs[idx].length != page_to_phys(pg)) { + if (idx + 1 == nsgs) + break; + sg_set_page(&sgs[++idx], pg, pg_length, + offset_in_page(data)); + } else { + sgs[idx].length += pg_length; + } + + data += pg_length; + length -= pg_length; + } + + sg_mark_end(&sgs[idx]); +} + +/** + * virtsnd_pcm_msg_alloc() - Allocate I/O messages. + * @vss: VirtIO PCM substream. + * @periods: Current number of periods. + * @period_bytes: Current period size in bytes. + * + * The function slices the buffer into @periods parts (each with the size of + * @period_bytes), and creates @periods corresponding I/O messages. + * + * Context: Any context that permits to sleep. + * Return: 0 on success, -ENOMEM on failure. + */ +int virtsnd_pcm_msg_alloc(struct virtio_pcm_substream *vss, + unsigned int periods, unsigned int period_bytes) +{ + struct snd_pcm_runtime *runtime = vss->substream->runtime; + unsigned int i; + + vss->msgs = kcalloc(periods, sizeof(*vss->msgs), GFP_KERNEL); + if (!vss->msgs) + return -ENOMEM; + + vss->nmsgs = periods; + + for (i = 0; i < periods; ++i) { + u8 *data = runtime->dma_area + period_bytes * i; + int sg_num = virtsnd_pcm_sg_num(data, period_bytes); + struct virtio_pcm_msg *msg; + + msg = kzalloc(sizeof(*msg) + sizeof(*msg->sgs) * (sg_num + 2), + GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->substream = vss; + sg_init_one(&msg->sgs[PCM_MSG_SG_XFER], &msg->xfer, + sizeof(msg->xfer)); + sg_init_one(&msg->sgs[PCM_MSG_SG_STATUS], &msg->status, + sizeof(msg->status)); + virtsnd_pcm_sg_from(&msg->sgs[PCM_MSG_SG_DATA], sg_num, data, + period_bytes); + + vss->msgs[i] = msg; + } + + return 0; +} + +/** + * virtsnd_pcm_msg_free() - Free all allocated I/O messages. + * @vss: VirtIO PCM substream. + * + * Context: Any context. + */ +void virtsnd_pcm_msg_free(struct virtio_pcm_substream *vss) +{ + unsigned int i; + + for (i = 0; vss->msgs && i < vss->nmsgs; ++i) + kfree(vss->msgs[i]); + kfree(vss->msgs); + + vss->msgs = NULL; + vss->nmsgs = 0; +} + +/** + * virtsnd_pcm_msg_send() - Send asynchronous I/O messages. + * @vss: VirtIO PCM substream. + * + * All messages are organized in an ordered circular list. Each time the + * function is called, all currently non-enqueued messages are added to the + * virtqueue. For this, the function keeps track of two values: + * + * msg_last_enqueued = index of the last enqueued message, + * msg_count = # of pending messages in the virtqueue. + * + * Context: Any context. Expects the tx/rx queue and the VirtIO substream + * spinlocks to be held by caller. + * Return: 0 on success, -errno on failure. + */ +int virtsnd_pcm_msg_send(struct virtio_pcm_substream *vss) +{ + struct snd_pcm_runtime *runtime = vss->substream->runtime; + struct virtio_snd *snd = vss->snd; + struct virtio_device *vdev = snd->vdev; + struct virtqueue *vqueue = virtsnd_pcm_queue(vss)->vqueue; + int i; + int n; + bool notify = false; + + i = (vss->msg_last_enqueued + 1) % runtime->periods; + n = runtime->periods - vss->msg_count; + + for (; n; --n, i = (i + 1) % runtime->periods) { + struct virtio_pcm_msg *msg = vss->msgs[i]; + struct scatterlist *psgs[] = { + &msg->sgs[PCM_MSG_SG_XFER], + &msg->sgs[PCM_MSG_SG_DATA], + &msg->sgs[PCM_MSG_SG_STATUS] + }; + int rc; + + msg->xfer.stream_id = cpu_to_le32(vss->sid); + memset(&msg->status, 0, sizeof(msg->status)); + + if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK) + rc = virtqueue_add_sgs(vqueue, psgs, 2, 1, msg, + GFP_ATOMIC); + else + rc = virtqueue_add_sgs(vqueue, psgs, 1, 2, msg, + GFP_ATOMIC); + + if (rc) { + dev_err(&vdev->dev, + "SID %u: failed to send I/O message\n", + vss->sid); + return rc; + } + + vss->msg_last_enqueued = i; + vss->msg_count++; + } + + if (!(vss->features & (1U << VIRTIO_SND_PCM_F_MSG_POLLING))) + notify = virtqueue_kick_prepare(vqueue); + + if (notify) + virtqueue_notify(vqueue); + + return 0; +} + +/** + * virtsnd_pcm_msg_pending_num() - Returns the number of pending I/O messages. + * @vss: VirtIO substream. + * + * Context: Any context. + * Return: Number of messages. + */ +unsigned int virtsnd_pcm_msg_pending_num(struct virtio_pcm_substream *vss) +{ + unsigned int num; + unsigned long flags; + + spin_lock_irqsave(&vss->lock, flags); + num = vss->msg_count; + spin_unlock_irqrestore(&vss->lock, flags); + + return num; +} + +/** + * virtsnd_pcm_msg_complete() - Complete an I/O message. + * @msg: I/O message. + * @written_bytes: Number of bytes written to the message. + * + * Completion of the message means the elapsed period. If transmission is + * allowed, then each completed message is immediately placed back at the end + * of the queue. + * + * For the playback substream, @written_bytes is equal to sizeof(msg->status). + * + * For the capture substream, @written_bytes is equal to sizeof(msg->status) + * plus the number of captured bytes. + * + * Context: Interrupt context. Takes and releases the VirtIO substream spinlock. + */ +static void virtsnd_pcm_msg_complete(struct virtio_pcm_msg *msg, + size_t written_bytes) +{ + struct virtio_pcm_substream *vss = msg->substream; + + /* + * hw_ptr always indicates the buffer position of the first I/O message + * in the virtqueue. Therefore, on each completion of an I/O message, + * the hw_ptr value is unconditionally advanced. + */ + spin_lock(&vss->lock); + /* + * If the capture substream returned an incorrect status, then just + * increase the hw_ptr by the message size. + */ + if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK || + written_bytes <= sizeof(msg->status)) { + struct scatterlist *sg; + + for (sg = &msg->sgs[PCM_MSG_SG_DATA]; sg; sg = sg_next(sg)) + vss->hw_ptr += sg->length; + } else { + vss->hw_ptr += written_bytes - sizeof(msg->status); + } + + if (vss->hw_ptr >= vss->buffer_bytes) + vss->hw_ptr -= vss->buffer_bytes; + + vss->xfer_xrun = false; + vss->msg_count--; + + if (vss->xfer_enabled) { + struct snd_pcm_runtime *runtime = vss->substream->runtime; + + runtime->delay = + bytes_to_frames(runtime, + le32_to_cpu(msg->status.latency_bytes)); + + spin_unlock(&vss->lock); + snd_pcm_period_elapsed(vss->substream); + spin_lock(&vss->lock); + + virtsnd_pcm_msg_send(vss); + } else if (!vss->msg_count) { + wake_up_all(&vss->msg_empty); + } + spin_unlock(&vss->lock); +} + +/** + * virtsnd_pcm_notify_cb() - Process all completed I/O messages. + * @queue: Underlying tx/rx virtqueue. + * + * Context: Interrupt context. Takes and releases the tx/rx queue spinlock. + */ +static inline void virtsnd_pcm_notify_cb(struct virtio_snd_queue *queue) +{ + struct virtio_pcm_msg *msg; + u32 written_bytes; + unsigned long flags; + + spin_lock_irqsave(&queue->lock, flags); + do { + virtqueue_disable_cb(queue->vqueue); + while ((msg = virtqueue_get_buf(queue->vqueue, &written_bytes))) + virtsnd_pcm_msg_complete(msg, written_bytes); + if (unlikely(virtqueue_is_broken(queue->vqueue))) + break; + } while (!virtqueue_enable_cb(queue->vqueue)); + spin_unlock_irqrestore(&queue->lock, flags); +} + +/** + * virtsnd_pcm_tx_notify_cb() - Process all completed TX messages. + * @vqueue: Underlying tx virtqueue. + * + * Context: Interrupt context. + */ +void virtsnd_pcm_tx_notify_cb(struct virtqueue *vqueue) +{ + struct virtio_snd *snd = vqueue->vdev->priv; + + virtsnd_pcm_notify_cb(virtsnd_tx_queue(snd)); +} + +/** + * virtsnd_pcm_rx_notify_cb() - Process all completed RX messages. + * @vqueue: Underlying rx virtqueue. + * + * Context: Interrupt context. + */ +void virtsnd_pcm_rx_notify_cb(struct virtqueue *vqueue) +{ + struct virtio_snd *snd = vqueue->vdev->priv; + + virtsnd_pcm_notify_cb(virtsnd_rx_queue(snd)); +} + +/** + * virtsnd_pcm_ctl_msg_alloc() - Allocate and initialize the PCM device control + * message for the specified substream. + * @vss: VirtIO PCM substream. + * @command: Control request code (VIRTIO_SND_R_PCM_XXX). + * @gfp: Kernel flags for memory allocation. + * + * Context: Any context. May sleep if @gfp flags permit. + * Return: Allocated message on success, NULL on failure. + */ +struct virtio_snd_msg * +virtsnd_pcm_ctl_msg_alloc(struct virtio_pcm_substream *vss, + unsigned int command, gfp_t gfp) +{ + size_t request_size = sizeof(struct virtio_snd_pcm_hdr); + size_t response_size = sizeof(struct virtio_snd_hdr); + struct virtio_snd_msg *msg; + + switch (command) { + case VIRTIO_SND_R_PCM_SET_PARAMS: + request_size = sizeof(struct virtio_snd_pcm_set_params); + break; + } + + msg = virtsnd_ctl_msg_alloc(request_size, response_size, gfp); + if (msg) { + struct virtio_snd_pcm_hdr *hdr = virtsnd_ctl_msg_request(msg); + + hdr->hdr.code = cpu_to_le32(command); + hdr->stream_id = cpu_to_le32(vss->sid); + } + + return msg; +}
On Sat, 27 Feb 2021 09:59:52 +0100, Anton Yakovlev wrote:
+/**
- virtsnd_pcm_event() - Handle the PCM device event notification.
- @snd: VirtIO sound device.
- @event: VirtIO sound event.
- Context: Interrupt context.
OK, then nonatomic PCM flag is invalid...
+/**
- virtsnd_pcm_sg_num() - Count the number of sg-elements required to represent
vmalloc'ed buffer.
- @data: Pointer to vmalloc'ed buffer.
- @length: Buffer size.
- Context: Any context.
- Return: Number of physically contiguous parts in the @data.
- */
+static int virtsnd_pcm_sg_num(u8 *data, unsigned int length) +{
- phys_addr_t sg_address;
- unsigned int sg_length;
- int num = 0;
- while (length) {
struct page *pg = vmalloc_to_page(data);
phys_addr_t pg_address = page_to_phys(pg);
size_t pg_length;
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
pg_length = length;
if (!num || sg_address + sg_length != pg_address) {
sg_address = pg_address;
sg_length = pg_length;
num++;
} else {
sg_length += pg_length;
}
data += pg_length;
length -= pg_length;
- }
- return num;
+}
+/**
- virtsnd_pcm_sg_from() - Build sg-list from vmalloc'ed buffer.
- @sgs: Preallocated sg-list to populate.
- @nsgs: The maximum number of elements in the @sgs.
- @data: Pointer to vmalloc'ed buffer.
- @length: Buffer size.
- Splits the buffer into physically contiguous parts and makes an sg-list of
- such parts.
- Context: Any context.
- */
+static void virtsnd_pcm_sg_from(struct scatterlist *sgs, int nsgs, u8 *data,
unsigned int length)
+{
- int idx = -1;
- while (length) {
struct page *pg = vmalloc_to_page(data);
size_t pg_length;
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
pg_length = length;
if (idx == -1 ||
sg_phys(&sgs[idx]) + sgs[idx].length != page_to_phys(pg)) {
if (idx + 1 == nsgs)
break;
sg_set_page(&sgs[++idx], pg, pg_length,
offset_in_page(data));
} else {
sgs[idx].length += pg_length;
}
data += pg_length;
length -= pg_length;
- }
- sg_mark_end(&sgs[idx]);
+}
Hmm, I thought there can be already a handy helper to convert vmalloc to sglist, but apparently not. It should have been trivial to get the page list from vmalloc, e.g.
int vmalloc_to_page_list(void *p, struct page **page_ret) { struct vmap_area *va;
va = find_vmap_area((unsigned long)p); if (!va) return 0; *page_ret = va->vm->pages; return va->vm->nr_pages; }
Then you can set up the sg list in a single call from the given page list.
But it's just a cleanup, and let's mark it as a room for improvements.
(snip)
+/**
- virtsnd_pcm_msg_complete() - Complete an I/O message.
- @msg: I/O message.
- @written_bytes: Number of bytes written to the message.
- Completion of the message means the elapsed period. If transmission is
- allowed, then each completed message is immediately placed back at the end
- of the queue.
- For the playback substream, @written_bytes is equal to sizeof(msg->status).
- For the capture substream, @written_bytes is equal to sizeof(msg->status)
- plus the number of captured bytes.
- Context: Interrupt context. Takes and releases the VirtIO substream spinlock.
- */
+static void virtsnd_pcm_msg_complete(struct virtio_pcm_msg *msg,
size_t written_bytes)
+{
- struct virtio_pcm_substream *vss = msg->substream;
- /*
* hw_ptr always indicates the buffer position of the first I/O message
* in the virtqueue. Therefore, on each completion of an I/O message,
* the hw_ptr value is unconditionally advanced.
*/
- spin_lock(&vss->lock);
- /*
* If the capture substream returned an incorrect status, then just
* increase the hw_ptr by the message size.
*/
- if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK ||
written_bytes <= sizeof(msg->status)) {
struct scatterlist *sg;
for (sg = &msg->sgs[PCM_MSG_SG_DATA]; sg; sg = sg_next(sg))
vss->hw_ptr += sg->length;
So the sg list entries are supposed to be updated? Or if the length there are constant, we don't need to iterate the sg entries but keep the total length beforehand?
thanks,
Takashi
On 28.02.2021 12:27, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:52 +0100, Anton Yakovlev wrote:
+/**
- virtsnd_pcm_event() - Handle the PCM device event notification.
- @snd: VirtIO sound device.
- @event: VirtIO sound event.
- Context: Interrupt context.
OK, then nonatomic PCM flag is invalid...
Well, no. Here, events are kind of independent entities. PCM-related events are just a special case of more generic events, which can carry any kind of notification/payload. (And at the moment, only XRUN notification is supported for PCM substreams.) So it has nothing to do with the atomicity of the PCM device itself.
+/**
- virtsnd_pcm_sg_num() - Count the number of sg-elements required to represent
vmalloc'ed buffer.
- @data: Pointer to vmalloc'ed buffer.
- @length: Buffer size.
- Context: Any context.
- Return: Number of physically contiguous parts in the @data.
- */
+static int virtsnd_pcm_sg_num(u8 *data, unsigned int length) +{
phys_addr_t sg_address;
unsigned int sg_length;
int num = 0;
while (length) {
struct page *pg = vmalloc_to_page(data);
phys_addr_t pg_address = page_to_phys(pg);
size_t pg_length;
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
pg_length = length;
if (!num || sg_address + sg_length != pg_address) {
sg_address = pg_address;
sg_length = pg_length;
num++;
} else {
sg_length += pg_length;
}
data += pg_length;
length -= pg_length;
}
return num;
+}
+/**
- virtsnd_pcm_sg_from() - Build sg-list from vmalloc'ed buffer.
- @sgs: Preallocated sg-list to populate.
- @nsgs: The maximum number of elements in the @sgs.
- @data: Pointer to vmalloc'ed buffer.
- @length: Buffer size.
- Splits the buffer into physically contiguous parts and makes an sg-list of
- such parts.
- Context: Any context.
- */
+static void virtsnd_pcm_sg_from(struct scatterlist *sgs, int nsgs, u8 *data,
unsigned int length)
+{
int idx = -1;
while (length) {
struct page *pg = vmalloc_to_page(data);
size_t pg_length;
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
pg_length = length;
if (idx == -1 ||
sg_phys(&sgs[idx]) + sgs[idx].length != page_to_phys(pg)) {
if (idx + 1 == nsgs)
break;
sg_set_page(&sgs[++idx], pg, pg_length,
offset_in_page(data));
} else {
sgs[idx].length += pg_length;
}
data += pg_length;
length -= pg_length;
}
sg_mark_end(&sgs[idx]);
+}
Hmm, I thought there can be already a handy helper to convert vmalloc to sglist, but apparently not. It should have been trivial to get the page list from vmalloc, e.g.
int vmalloc_to_page_list(void *p, struct page **page_ret) { struct vmap_area *va;
va = find_vmap_area((unsigned long)p); if (!va) return 0; *page_ret = va->vm->pages; return va->vm->nr_pages;
}
Then you can set up the sg list in a single call from the given page list.
But it's just a cleanup, and let's mark it as a room for improvements.
Yeah, we can take a look into some kind of optimizations here. But I suspect, the overall code will look similar. It is not enough just to get a list of pages, you also need to build a list of physically contiguous regions from it. Because the sg-elements are put into a virtqueue that has a limited size. And each sg-element consumes one item in the virtqueue. And since the virtqueue itself is shared between all substreams, the items of the virtqueue become a scarce resource.
(snip)
+/**
- virtsnd_pcm_msg_complete() - Complete an I/O message.
- @msg: I/O message.
- @written_bytes: Number of bytes written to the message.
- Completion of the message means the elapsed period. If transmission is
- allowed, then each completed message is immediately placed back at the end
- of the queue.
- For the playback substream, @written_bytes is equal to sizeof(msg->status).
- For the capture substream, @written_bytes is equal to sizeof(msg->status)
- plus the number of captured bytes.
- Context: Interrupt context. Takes and releases the VirtIO substream spinlock.
- */
+static void virtsnd_pcm_msg_complete(struct virtio_pcm_msg *msg,
size_t written_bytes)
+{
struct virtio_pcm_substream *vss = msg->substream;
/*
* hw_ptr always indicates the buffer position of the first I/O message
* in the virtqueue. Therefore, on each completion of an I/O message,
* the hw_ptr value is unconditionally advanced.
*/
spin_lock(&vss->lock);
/*
* If the capture substream returned an incorrect status, then just
* increase the hw_ptr by the message size.
*/
if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK ||
written_bytes <= sizeof(msg->status)) {
struct scatterlist *sg;
for (sg = &msg->sgs[PCM_MSG_SG_DATA]; sg; sg = sg_next(sg))
vss->hw_ptr += sg->length;
So the sg list entries are supposed to be updated? Or if the length there are constant, we don't need to iterate the sg entries but keep the total length beforehand?
That's one of options. Since the same info can be derived from sg-list, I thought it might be not necessary to keep it in some additional field. But probably it makes sense to keep total length in the message structure itself. Then it will be more flexible (if we will need to create non-period sized messages in the future).
thanks,
Takashi
On Mon, 01 Mar 2021 10:25:05 +0100, Anton Yakovlev wrote:
On 28.02.2021 12:27, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:52 +0100, Anton Yakovlev wrote:
+/**
- virtsnd_pcm_event() - Handle the PCM device event notification.
- @snd: VirtIO sound device.
- @event: VirtIO sound event.
- Context: Interrupt context.
OK, then nonatomic PCM flag is invalid...
Well, no. Here, events are kind of independent entities. PCM-related events are just a special case of more generic events, which can carry any kind of notification/payload. (And at the moment, only XRUN notification is supported for PCM substreams.) So it has nothing to do with the atomicity of the PCM device itself.
OK, thanks.
Basically the only question is how snd_pcm_period_elapsed() is called. And I see that it's called inside queue->lock, and this already invalidates the nonatomic PCM mode. So the code needs the fix: either fix this locking (and the context is guaranteed not to be an irq context), or change to the normal PCM mode without nonatomic flag. Both would bring some side-effect, and we need further changes, I suppose...
+/**
- virtsnd_pcm_sg_num() - Count the number of sg-elements required to represent
vmalloc'ed buffer.
- @data: Pointer to vmalloc'ed buffer.
- @length: Buffer size.
- Context: Any context.
- Return: Number of physically contiguous parts in the @data.
- */
+static int virtsnd_pcm_sg_num(u8 *data, unsigned int length) +{
phys_addr_t sg_address;
unsigned int sg_length;
int num = 0;
while (length) {
struct page *pg = vmalloc_to_page(data);
phys_addr_t pg_address = page_to_phys(pg);
size_t pg_length;
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
pg_length = length;
if (!num || sg_address + sg_length != pg_address) {
sg_address = pg_address;
sg_length = pg_length;
num++;
} else {
sg_length += pg_length;
}
data += pg_length;
length -= pg_length;
}
return num;
+}
+/**
- virtsnd_pcm_sg_from() - Build sg-list from vmalloc'ed buffer.
- @sgs: Preallocated sg-list to populate.
- @nsgs: The maximum number of elements in the @sgs.
- @data: Pointer to vmalloc'ed buffer.
- @length: Buffer size.
- Splits the buffer into physically contiguous parts and makes an sg-list of
- such parts.
- Context: Any context.
- */
+static void virtsnd_pcm_sg_from(struct scatterlist *sgs, int nsgs, u8 *data,
unsigned int length)
+{
int idx = -1;
while (length) {
struct page *pg = vmalloc_to_page(data);
size_t pg_length;
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
pg_length = length;
if (idx == -1 ||
sg_phys(&sgs[idx]) + sgs[idx].length != page_to_phys(pg)) {
if (idx + 1 == nsgs)
break;
sg_set_page(&sgs[++idx], pg, pg_length,
offset_in_page(data));
} else {
sgs[idx].length += pg_length;
}
data += pg_length;
length -= pg_length;
}
sg_mark_end(&sgs[idx]);
+}
Hmm, I thought there can be already a handy helper to convert vmalloc to sglist, but apparently not. It should have been trivial to get the page list from vmalloc, e.g.
int vmalloc_to_page_list(void *p, struct page **page_ret) { struct vmap_area *va;
va = find_vmap_area((unsigned long)p); if (!va) return 0; *page_ret = va->vm->pages; return va->vm->nr_pages;
}
Then you can set up the sg list in a single call from the given page list.
But it's just a cleanup, and let's mark it as a room for improvements.
Yeah, we can take a look into some kind of optimizations here. But I suspect, the overall code will look similar. It is not enough just to get a list of pages, you also need to build a list of physically contiguous regions from it.
I believe the standard helper does it. But it's for sg_table, hence the plain scatterlist needs to be extracted from there, but most of complex things are in the standard code.
But it's merely an optimization and something for future.
Takashi
On 01.03.2021 14:32, Takashi Iwai wrote:
On Mon, 01 Mar 2021 10:25:05 +0100, Anton Yakovlev wrote:
On 28.02.2021 12:27, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:52 +0100, Anton Yakovlev wrote:
+/**
- virtsnd_pcm_event() - Handle the PCM device event notification.
- @snd: VirtIO sound device.
- @event: VirtIO sound event.
- Context: Interrupt context.
OK, then nonatomic PCM flag is invalid...
Well, no. Here, events are kind of independent entities. PCM-related events are just a special case of more generic events, which can carry any kind of notification/payload. (And at the moment, only XRUN notification is supported for PCM substreams.) So it has nothing to do with the atomicity of the PCM device itself.
OK, thanks.
Basically the only question is how snd_pcm_period_elapsed() is called. And I see that it's called inside queue->lock, and this already invalidates the nonatomic PCM mode. So the code needs the fix: either fix this locking (and the context is guaranteed not to be an irq context), or change to the normal PCM mode without nonatomic flag. Both would bring some side-effect, and we need further changes, I suppose...
Ok, I understood the problem. Well, I would say the nonatomic PCM mode is more important option, since in this mode we can guarantee the correct operation of the device. And if you say, that we need to get rid of irq context here, then probably workqueue for calling snd_pcm_period_elapsed() should be fine (of course, it should be shared between all available substreams).
+/**
- virtsnd_pcm_sg_num() - Count the number of sg-elements required to represent
vmalloc'ed buffer.
- @data: Pointer to vmalloc'ed buffer.
- @length: Buffer size.
- Context: Any context.
- Return: Number of physically contiguous parts in the @data.
- */
+static int virtsnd_pcm_sg_num(u8 *data, unsigned int length) +{
phys_addr_t sg_address;
unsigned int sg_length;
int num = 0;
while (length) {
struct page *pg = vmalloc_to_page(data);
phys_addr_t pg_address = page_to_phys(pg);
size_t pg_length;
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
pg_length = length;
if (!num || sg_address + sg_length != pg_address) {
sg_address = pg_address;
sg_length = pg_length;
num++;
} else {
sg_length += pg_length;
}
data += pg_length;
length -= pg_length;
}
return num;
+}
+/**
- virtsnd_pcm_sg_from() - Build sg-list from vmalloc'ed buffer.
- @sgs: Preallocated sg-list to populate.
- @nsgs: The maximum number of elements in the @sgs.
- @data: Pointer to vmalloc'ed buffer.
- @length: Buffer size.
- Splits the buffer into physically contiguous parts and makes an sg-list of
- such parts.
- Context: Any context.
- */
+static void virtsnd_pcm_sg_from(struct scatterlist *sgs, int nsgs, u8 *data,
unsigned int length)
+{
int idx = -1;
while (length) {
struct page *pg = vmalloc_to_page(data);
size_t pg_length;
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
pg_length = length;
if (idx == -1 ||
sg_phys(&sgs[idx]) + sgs[idx].length != page_to_phys(pg)) {
if (idx + 1 == nsgs)
break;
sg_set_page(&sgs[++idx], pg, pg_length,
offset_in_page(data));
} else {
sgs[idx].length += pg_length;
}
data += pg_length;
length -= pg_length;
}
sg_mark_end(&sgs[idx]);
+}
Hmm, I thought there can be already a handy helper to convert vmalloc to sglist, but apparently not. It should have been trivial to get the page list from vmalloc, e.g.
int vmalloc_to_page_list(void *p, struct page **page_ret) { struct vmap_area *va;
va = find_vmap_area((unsigned long)p); if (!va) return 0; *page_ret = va->vm->pages; return va->vm->nr_pages;
}
Then you can set up the sg list in a single call from the given page list.
But it's just a cleanup, and let's mark it as a room for improvements.
Yeah, we can take a look into some kind of optimizations here. But I suspect, the overall code will look similar. It is not enough just to get a list of pages, you also need to build a list of physically contiguous regions from it.
I believe the standard helper does it. But it's for sg_table, hence the plain scatterlist needs to be extracted from there, but most of complex things are in the standard code.
But it's merely an optimization and something for future.
I quickly checked it. I think it's hardly possible to do anything here. These functions to deal with vmalloced areas are not exported. And, according to comments, require some proper locking on top of that. At least, it does not look like a trivial things.
Takashi
On Mon, 01 Mar 2021 15:47:46 +0100, Anton Yakovlev wrote:
On 01.03.2021 14:32, Takashi Iwai wrote:
On Mon, 01 Mar 2021 10:25:05 +0100, Anton Yakovlev wrote:
On 28.02.2021 12:27, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:52 +0100, Anton Yakovlev wrote:
+/**
- virtsnd_pcm_event() - Handle the PCM device event notification.
- @snd: VirtIO sound device.
- @event: VirtIO sound event.
- Context: Interrupt context.
OK, then nonatomic PCM flag is invalid...
Well, no. Here, events are kind of independent entities. PCM-related events are just a special case of more generic events, which can carry any kind of notification/payload. (And at the moment, only XRUN notification is supported for PCM substreams.) So it has nothing to do with the atomicity of the PCM device itself.
OK, thanks.
Basically the only question is how snd_pcm_period_elapsed() is called. And I see that it's called inside queue->lock, and this already invalidates the nonatomic PCM mode. So the code needs the fix: either fix this locking (and the context is guaranteed not to be an irq context), or change to the normal PCM mode without nonatomic flag. Both would bring some side-effect, and we need further changes, I suppose...
Ok, I understood the problem. Well, I would say the nonatomic PCM mode is more important option, since in this mode we can guarantee the correct operation of the device.
Which operation (except for the resume) in the trigger and the pointer callbacks need the action that need to sleep? I thought the sync with the command queue is done in the sync_stop. And most of others seem already taking the spinlock in themselves, so the non-atomic operation has little merit for them.
And if you say, that we need to get rid of irq context here, then probably workqueue for calling snd_pcm_period_elapsed() should be fine (of course, it should be shared between all available substreams).
That would work, but it's of course just papering over it :)
+/**
- virtsnd_pcm_sg_num() - Count the number of sg-elements required to represent
vmalloc'ed buffer.
- @data: Pointer to vmalloc'ed buffer.
- @length: Buffer size.
- Context: Any context.
- Return: Number of physically contiguous parts in the @data.
- */
+static int virtsnd_pcm_sg_num(u8 *data, unsigned int length) +{
phys_addr_t sg_address;
unsigned int sg_length;
int num = 0;
while (length) {
struct page *pg = vmalloc_to_page(data);
phys_addr_t pg_address = page_to_phys(pg);
size_t pg_length;
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
pg_length = length;
if (!num || sg_address + sg_length != pg_address) {
sg_address = pg_address;
sg_length = pg_length;
num++;
} else {
sg_length += pg_length;
}
data += pg_length;
length -= pg_length;
}
return num;
+}
+/**
- virtsnd_pcm_sg_from() - Build sg-list from vmalloc'ed buffer.
- @sgs: Preallocated sg-list to populate.
- @nsgs: The maximum number of elements in the @sgs.
- @data: Pointer to vmalloc'ed buffer.
- @length: Buffer size.
- Splits the buffer into physically contiguous parts and makes an sg-list of
- such parts.
- Context: Any context.
- */
+static void virtsnd_pcm_sg_from(struct scatterlist *sgs, int nsgs, u8 *data,
unsigned int length)
+{
int idx = -1;
while (length) {
struct page *pg = vmalloc_to_page(data);
size_t pg_length;
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
pg_length = length;
if (idx == -1 ||
sg_phys(&sgs[idx]) + sgs[idx].length != page_to_phys(pg)) {
if (idx + 1 == nsgs)
break;
sg_set_page(&sgs[++idx], pg, pg_length,
offset_in_page(data));
} else {
sgs[idx].length += pg_length;
}
data += pg_length;
length -= pg_length;
}
sg_mark_end(&sgs[idx]);
+}
Hmm, I thought there can be already a handy helper to convert vmalloc to sglist, but apparently not. It should have been trivial to get the page list from vmalloc, e.g.
int vmalloc_to_page_list(void *p, struct page **page_ret) { struct vmap_area *va;
va = find_vmap_area((unsigned long)p); if (!va) return 0; *page_ret = va->vm->pages; return va->vm->nr_pages;
}
Then you can set up the sg list in a single call from the given page list.
But it's just a cleanup, and let's mark it as a room for improvements.
Yeah, we can take a look into some kind of optimizations here. But I suspect, the overall code will look similar. It is not enough just to get a list of pages, you also need to build a list of physically contiguous regions from it.
I believe the standard helper does it. But it's for sg_table, hence the plain scatterlist needs to be extracted from there, but most of complex things are in the standard code.
But it's merely an optimization and something for future.
I quickly checked it. I think it's hardly possible to do anything here. These functions to deal with vmalloced areas are not exported. And, according to comments, require some proper locking on top of that. At least, it does not look like a trivial things.
Sure, it needs a function exposed from vmalloc.c. But I don't think the locking is the problem as find_vmap_area() already takes care of it, and we don't release the vmalloced pages while invoking this function. Of course I might overlook something, but my point is that this kind of work should be rather in the core (or at least most of the important steps in the code should be in the core code).
Takashi
On 01.03.2021 15:56, Takashi Iwai wrote:
On Mon, 01 Mar 2021 15:47:46 +0100, Anton Yakovlev wrote:
On 01.03.2021 14:32, Takashi Iwai wrote:
On Mon, 01 Mar 2021 10:25:05 +0100, Anton Yakovlev wrote:
On 28.02.2021 12:27, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:52 +0100, Anton Yakovlev wrote:
+/**
- virtsnd_pcm_event() - Handle the PCM device event notification.
- @snd: VirtIO sound device.
- @event: VirtIO sound event.
- Context: Interrupt context.
OK, then nonatomic PCM flag is invalid...
Well, no. Here, events are kind of independent entities. PCM-related events are just a special case of more generic events, which can carry any kind of notification/payload. (And at the moment, only XRUN notification is supported for PCM substreams.) So it has nothing to do with the atomicity of the PCM device itself.
OK, thanks.
Basically the only question is how snd_pcm_period_elapsed() is called. And I see that it's called inside queue->lock, and this already invalidates the nonatomic PCM mode. So the code needs the fix: either fix this locking (and the context is guaranteed not to be an irq context), or change to the normal PCM mode without nonatomic flag. Both would bring some side-effect, and we need further changes, I suppose...
Ok, I understood the problem. Well, I would say the nonatomic PCM mode is more important option, since in this mode we can guarantee the correct operation of the device.
Which operation (except for the resume) in the trigger and the pointer callbacks need the action that need to sleep? I thought the sync with the command queue is done in the sync_stop. And most of others seem already taking the spinlock in themselves, so the non-atomic operation has little merit for them.
All three commands from the beginning of that switch in trigger op: ops->trigger(START) ops->trigger(PAUSE_RELEASE) ops->trigger(RESUME)
They all result in virtsnd_ctl_msg_send_sync(VIRTIO_SND_R_PCM_START)
And that command must be sync, i.e. we need to wait/sleep until device sent response. There are two major reasons for that: we need to be sure, that substream has been actually started (i.e. device said OK). And we need to take into account the virtualization overhead as well. Since substream starting on device side may take some time, we can not return from the trigger op until it actually happened. In atomic mode all these nuances are lost, and it may lead to incorrect application behavior.
And if you say, that we need to get rid of irq context here, then probably workqueue for calling snd_pcm_period_elapsed() should be fine (of course, it should be shared between all available substreams).
That would work, but it's of course just papering over it :)
+/**
- virtsnd_pcm_sg_num() - Count the number of sg-elements required to represent
vmalloc'ed buffer.
- @data: Pointer to vmalloc'ed buffer.
- @length: Buffer size.
- Context: Any context.
- Return: Number of physically contiguous parts in the @data.
- */
+static int virtsnd_pcm_sg_num(u8 *data, unsigned int length) +{
phys_addr_t sg_address;
unsigned int sg_length;
int num = 0;
while (length) {
struct page *pg = vmalloc_to_page(data);
phys_addr_t pg_address = page_to_phys(pg);
size_t pg_length;
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
pg_length = length;
if (!num || sg_address + sg_length != pg_address) {
sg_address = pg_address;
sg_length = pg_length;
num++;
} else {
sg_length += pg_length;
}
data += pg_length;
length -= pg_length;
}
return num;
+}
+/**
- virtsnd_pcm_sg_from() - Build sg-list from vmalloc'ed buffer.
- @sgs: Preallocated sg-list to populate.
- @nsgs: The maximum number of elements in the @sgs.
- @data: Pointer to vmalloc'ed buffer.
- @length: Buffer size.
- Splits the buffer into physically contiguous parts and makes an sg-list of
- such parts.
- Context: Any context.
- */
+static void virtsnd_pcm_sg_from(struct scatterlist *sgs, int nsgs, u8 *data,
unsigned int length)
+{
int idx = -1;
while (length) {
struct page *pg = vmalloc_to_page(data);
size_t pg_length;
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
pg_length = length;
if (idx == -1 ||
sg_phys(&sgs[idx]) + sgs[idx].length != page_to_phys(pg)) {
if (idx + 1 == nsgs)
break;
sg_set_page(&sgs[++idx], pg, pg_length,
offset_in_page(data));
} else {
sgs[idx].length += pg_length;
}
data += pg_length;
length -= pg_length;
}
sg_mark_end(&sgs[idx]);
+}
Hmm, I thought there can be already a handy helper to convert vmalloc to sglist, but apparently not. It should have been trivial to get the page list from vmalloc, e.g.
int vmalloc_to_page_list(void *p, struct page **page_ret) { struct vmap_area *va;
va = find_vmap_area((unsigned long)p); if (!va) return 0; *page_ret = va->vm->pages; return va->vm->nr_pages;
}
Then you can set up the sg list in a single call from the given page list.
But it's just a cleanup, and let's mark it as a room for improvements.
Yeah, we can take a look into some kind of optimizations here. But I suspect, the overall code will look similar. It is not enough just to get a list of pages, you also need to build a list of physically contiguous regions from it.
I believe the standard helper does it. But it's for sg_table, hence the plain scatterlist needs to be extracted from there, but most of complex things are in the standard code.
But it's merely an optimization and something for future.
I quickly checked it. I think it's hardly possible to do anything here. These functions to deal with vmalloced areas are not exported. And, according to comments, require some proper locking on top of that. At least, it does not look like a trivial things.
Sure, it needs a function exposed from vmalloc.c. But I don't think the locking is the problem as find_vmap_area() already takes care of it, and we don't release the vmalloced pages while invoking this function. Of course I might overlook something, but my point is that this kind of work should be rather in the core (or at least most of the important steps in the code should be in the core code).
Then it's definitely some work for future. :)
Takashi
On Mon, 01 Mar 2021 16:24:00 +0100, Anton Yakovlev wrote:
On 01.03.2021 15:56, Takashi Iwai wrote:
On Mon, 01 Mar 2021 15:47:46 +0100, Anton Yakovlev wrote:
On 01.03.2021 14:32, Takashi Iwai wrote:
On Mon, 01 Mar 2021 10:25:05 +0100, Anton Yakovlev wrote:
On 28.02.2021 12:27, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:52 +0100, Anton Yakovlev wrote: > +/** > + * virtsnd_pcm_event() - Handle the PCM device event notification. > + * @snd: VirtIO sound device. > + * @event: VirtIO sound event. > + * > + * Context: Interrupt context.
OK, then nonatomic PCM flag is invalid...
Well, no. Here, events are kind of independent entities. PCM-related events are just a special case of more generic events, which can carry any kind of notification/payload. (And at the moment, only XRUN notification is supported for PCM substreams.) So it has nothing to do with the atomicity of the PCM device itself.
OK, thanks.
Basically the only question is how snd_pcm_period_elapsed() is called. And I see that it's called inside queue->lock, and this already invalidates the nonatomic PCM mode. So the code needs the fix: either fix this locking (and the context is guaranteed not to be an irq context), or change to the normal PCM mode without nonatomic flag. Both would bring some side-effect, and we need further changes, I suppose...
Ok, I understood the problem. Well, I would say the nonatomic PCM mode is more important option, since in this mode we can guarantee the correct operation of the device.
Which operation (except for the resume) in the trigger and the pointer callbacks need the action that need to sleep? I thought the sync with the command queue is done in the sync_stop. And most of others seem already taking the spinlock in themselves, so the non-atomic operation has little merit for them.
All three commands from the beginning of that switch in trigger op: ops->trigger(START) ops->trigger(PAUSE_RELEASE) ops->trigger(RESUME)
They all result in virtsnd_ctl_msg_send_sync(VIRTIO_SND_R_PCM_START)
And that command must be sync, i.e. we need to wait/sleep until device sent response. There are two major reasons for that: we need to be sure, that substream has been actually started (i.e. device said OK). And we need to take into account the virtualization overhead as well. Since substream starting on device side may take some time, we can not return from the trigger op until it actually happened. In atomic mode all these nuances are lost, and it may lead to incorrect application behavior.
I see, then the nonatomic mode is the only way to go, indeed.
Takashi
Introduce the operators required for the operation of substreams.
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com --- sound/virtio/Makefile | 3 +- sound/virtio/virtio_pcm.c | 2 + sound/virtio/virtio_pcm.h | 4 + sound/virtio/virtio_pcm_ops.c | 453 ++++++++++++++++++++++++++++++++++ 4 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 sound/virtio/virtio_pcm_ops.c
diff --git a/sound/virtio/Makefile b/sound/virtio/Makefile index 626af3cc3ed7..34493226793f 100644 --- a/sound/virtio/Makefile +++ b/sound/virtio/Makefile @@ -6,5 +6,6 @@ virtio_snd-objs := \ virtio_card.o \ virtio_ctl_msg.o \ virtio_pcm.o \ - virtio_pcm_msg.o + virtio_pcm_msg.o \ + virtio_pcm_ops.o
diff --git a/sound/virtio/virtio_pcm.c b/sound/virtio/virtio_pcm.c index 3cfd3520a9c0..3605151860f2 100644 --- a/sound/virtio/virtio_pcm.c +++ b/sound/virtio/virtio_pcm.c @@ -454,6 +454,8 @@ int virtsnd_pcm_build_devs(struct virtio_snd *snd)
for (kss = ks->substream; kss; kss = kss->next) vs->substreams[kss->number]->substream = kss; + + snd_pcm_set_ops(vpcm->pcm, i, &virtsnd_pcm_ops); }
snd_pcm_set_managed_buffer_all(vpcm->pcm, diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h index 7af41cad4e8f..55dfb19d14b3 100644 --- a/sound/virtio/virtio_pcm.h +++ b/sound/virtio/virtio_pcm.h @@ -28,6 +28,7 @@ struct virtio_pcm_msg; * @hw_ptr: Substream hardware pointer value in bytes [0 ... buffer_bytes). * @xfer_enabled: Data transfer state (0 - off, 1 - on). * @xfer_xrun: Data underflow/overflow state (0 - no xrun, 1 - xrun). + * @release: True if the substream must be released on the device side. * @msgs: Allocated I/O messages. * @nmsgs: Number of allocated I/O messages. * @msg_last_enqueued: Index of the last I/O message added to the virtqueue. @@ -47,6 +48,7 @@ struct virtio_pcm_substream { size_t hw_ptr; bool xfer_enabled; bool xfer_xrun; + bool release; struct virtio_pcm_msg **msgs; unsigned int nmsgs; int msg_last_enqueued; @@ -78,6 +80,8 @@ struct virtio_pcm { struct virtio_pcm_stream streams[SNDRV_PCM_STREAM_LAST + 1]; };
+extern const struct snd_pcm_ops virtsnd_pcm_ops; + int virtsnd_pcm_validate(struct virtio_device *vdev);
int virtsnd_pcm_parse_cfg(struct virtio_snd *snd); diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c new file mode 100644 index 000000000000..d02d79bd94f3 --- /dev/null +++ b/sound/virtio/virtio_pcm_ops.c @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * virtio-snd: Virtio sound device + * Copyright (C) 2021 OpenSynergy GmbH + */ +#include <sound/pcm_params.h> + +#include "virtio_card.h" + +/* + * I/O messages lifetime + * --------------------- + * + * Allocation: + * Messages are initially allocated in the ops->hw_params() after the size and + * number of periods have been successfully negotiated. + * + * Freeing: + * Messages can be safely freed after the queue has been successfully flushed + * (RELEASE command in the ops->sync_stop()) and the ops->hw_free() has been + * called. + * + * When the substream stops, the ops->sync_stop() waits until the device has + * completed all pending messages. This wait can be interrupted either by a + * signal or due to a timeout. In this case, the device can still access + * messages even after calling ops->hw_free(). It can also issue an interrupt, + * and the interrupt handler will also try to access message structures. + * + * Therefore, freeing of already allocated messages occurs: + * + * - in ops->hw_params(), if this operator was called several times in a row, + * or if ops->hw_free() failed to free messages previously; + * + * - in ops->hw_free(), if the queue has been successfully flushed; + * + * - in dev->release(). + */ + +/* Map for converting ALSA format to VirtIO format. */ +struct virtsnd_a2v_format { + snd_pcm_format_t alsa_bit; + unsigned int vio_bit; +}; + +static const struct virtsnd_a2v_format g_a2v_format_map[] = { + { SNDRV_PCM_FORMAT_IMA_ADPCM, VIRTIO_SND_PCM_FMT_IMA_ADPCM }, + { SNDRV_PCM_FORMAT_MU_LAW, VIRTIO_SND_PCM_FMT_MU_LAW }, + { SNDRV_PCM_FORMAT_A_LAW, VIRTIO_SND_PCM_FMT_A_LAW }, + { SNDRV_PCM_FORMAT_S8, VIRTIO_SND_PCM_FMT_S8 }, + { SNDRV_PCM_FORMAT_U8, VIRTIO_SND_PCM_FMT_U8 }, + { SNDRV_PCM_FORMAT_S16_LE, VIRTIO_SND_PCM_FMT_S16 }, + { SNDRV_PCM_FORMAT_U16_LE, VIRTIO_SND_PCM_FMT_U16 }, + { SNDRV_PCM_FORMAT_S18_3LE, VIRTIO_SND_PCM_FMT_S18_3 }, + { SNDRV_PCM_FORMAT_U18_3LE, VIRTIO_SND_PCM_FMT_U18_3 }, + { SNDRV_PCM_FORMAT_S20_3LE, VIRTIO_SND_PCM_FMT_S20_3 }, + { SNDRV_PCM_FORMAT_U20_3LE, VIRTIO_SND_PCM_FMT_U20_3 }, + { SNDRV_PCM_FORMAT_S24_3LE, VIRTIO_SND_PCM_FMT_S24_3 }, + { SNDRV_PCM_FORMAT_U24_3LE, VIRTIO_SND_PCM_FMT_U24_3 }, + { SNDRV_PCM_FORMAT_S20_LE, VIRTIO_SND_PCM_FMT_S20 }, + { SNDRV_PCM_FORMAT_U20_LE, VIRTIO_SND_PCM_FMT_U20 }, + { SNDRV_PCM_FORMAT_S24_LE, VIRTIO_SND_PCM_FMT_S24 }, + { SNDRV_PCM_FORMAT_U24_LE, VIRTIO_SND_PCM_FMT_U24 }, + { SNDRV_PCM_FORMAT_S32_LE, VIRTIO_SND_PCM_FMT_S32 }, + { SNDRV_PCM_FORMAT_U32_LE, VIRTIO_SND_PCM_FMT_U32 }, + { SNDRV_PCM_FORMAT_FLOAT_LE, VIRTIO_SND_PCM_FMT_FLOAT }, + { SNDRV_PCM_FORMAT_FLOAT64_LE, VIRTIO_SND_PCM_FMT_FLOAT64 }, + { SNDRV_PCM_FORMAT_DSD_U8, VIRTIO_SND_PCM_FMT_DSD_U8 }, + { SNDRV_PCM_FORMAT_DSD_U16_LE, VIRTIO_SND_PCM_FMT_DSD_U16 }, + { SNDRV_PCM_FORMAT_DSD_U32_LE, VIRTIO_SND_PCM_FMT_DSD_U32 }, + { SNDRV_PCM_FORMAT_IEC958_SUBFRAME_LE, + VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME } +}; + +/* Map for converting ALSA frame rate to VirtIO frame rate. */ +struct virtsnd_a2v_rate { + unsigned int rate; + unsigned int vio_bit; +}; + +static const struct virtsnd_a2v_rate g_a2v_rate_map[] = { + { 5512, VIRTIO_SND_PCM_RATE_5512 }, + { 8000, VIRTIO_SND_PCM_RATE_8000 }, + { 11025, VIRTIO_SND_PCM_RATE_11025 }, + { 16000, VIRTIO_SND_PCM_RATE_16000 }, + { 22050, VIRTIO_SND_PCM_RATE_22050 }, + { 32000, VIRTIO_SND_PCM_RATE_32000 }, + { 44100, VIRTIO_SND_PCM_RATE_44100 }, + { 48000, VIRTIO_SND_PCM_RATE_48000 }, + { 64000, VIRTIO_SND_PCM_RATE_64000 }, + { 88200, VIRTIO_SND_PCM_RATE_88200 }, + { 96000, VIRTIO_SND_PCM_RATE_96000 }, + { 176400, VIRTIO_SND_PCM_RATE_176400 }, + { 192000, VIRTIO_SND_PCM_RATE_192000 } +}; + +static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream); + +/** + * virtsnd_pcm_open() - Open the PCM substream. + * @substream: Kernel ALSA substream. + * + * Context: Process context. + * Return: 0 on success, -errno on failure. + */ +static int virtsnd_pcm_open(struct snd_pcm_substream *substream) +{ + struct virtio_pcm *vpcm = snd_pcm_substream_chip(substream); + struct virtio_pcm_stream *vs = &vpcm->streams[substream->stream]; + struct virtio_pcm_substream *vss = vs->substreams[substream->number]; + + substream->runtime->hw = vss->hw; + substream->private_data = vss; + + snd_pcm_hw_constraint_integer(substream->runtime, + SNDRV_PCM_HW_PARAM_PERIODS); + + /* + * If the substream has already been used, then the I/O queue may be in + * an invalid state. Just in case, we do a check and try to return the + * queue to its original state, if necessary. + */ + vss->release = !!virtsnd_pcm_msg_pending_num(vss); + + return virtsnd_pcm_sync_stop(substream); +} + +/** + * virtsnd_pcm_close() - Close the PCM substream. + * @substream: Kernel ALSA substream. + * + * Context: Process context. + * Return: 0. + */ +static int virtsnd_pcm_close(struct snd_pcm_substream *substream) +{ + return 0; +} + +/** + * virtsnd_pcm_hw_params() - Set the parameters of the PCM substream. + * @substream: Kernel ALSA substream. + * @hw_params: Hardware parameters (can be NULL). + * + * The function can be called both from the upper level (in this case, + * @hw_params is not NULL) or from the driver itself (in this case, @hw_params + * is NULL, and the parameter values are taken from the runtime structure). + * + * Context: Process context. + * Return: 0 on success, -errno on failure. + */ +static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *hw_params) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream); + struct virtio_device *vdev = vss->snd->vdev; + struct virtio_snd_msg *msg; + struct virtio_snd_pcm_set_params *request; + snd_pcm_format_t format; + unsigned int channels; + unsigned int rate; + unsigned int buffer_bytes; + unsigned int period_bytes; + unsigned int periods; + unsigned int i; + int vformat = -1; + int vrate = -1; + int rc; + + if (virtsnd_pcm_msg_pending_num(vss)) { + dev_err(&vdev->dev, "SID %u: invalid I/O queue state\n", + vss->sid); + return -EBADFD; + } + + /* Set hardware parameters in device */ + if (hw_params) { + format = params_format(hw_params); + channels = params_channels(hw_params); + rate = params_rate(hw_params); + buffer_bytes = params_buffer_bytes(hw_params); + period_bytes = params_period_bytes(hw_params); + periods = params_periods(hw_params); + } else { + format = runtime->format; + channels = runtime->channels; + rate = runtime->rate; + buffer_bytes = frames_to_bytes(runtime, runtime->buffer_size); + period_bytes = frames_to_bytes(runtime, runtime->period_size); + periods = runtime->periods; + } + + for (i = 0; i < ARRAY_SIZE(g_a2v_format_map); ++i) + if (g_a2v_format_map[i].alsa_bit == format) { + vformat = g_a2v_format_map[i].vio_bit; + + break; + } + + for (i = 0; i < ARRAY_SIZE(g_a2v_rate_map); ++i) + if (g_a2v_rate_map[i].rate == rate) { + vrate = g_a2v_rate_map[i].vio_bit; + + break; + } + + if (vformat == -1 || vrate == -1) + return -EINVAL; + + msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_SET_PARAMS, + GFP_KERNEL); + if (!msg) + return -ENOMEM; + + request = virtsnd_ctl_msg_request(msg); + request->buffer_bytes = cpu_to_le32(buffer_bytes); + request->period_bytes = cpu_to_le32(period_bytes); + request->channels = channels; + request->format = vformat; + request->rate = vrate; + + if (vss->features & (1U << VIRTIO_SND_PCM_F_MSG_POLLING)) + request->features |= + cpu_to_le32(1U << VIRTIO_SND_PCM_F_MSG_POLLING); + + if (vss->features & (1U << VIRTIO_SND_PCM_F_EVT_XRUNS)) + request->features |= + cpu_to_le32(1U << VIRTIO_SND_PCM_F_EVT_XRUNS); + + rc = virtsnd_ctl_msg_send_sync(vss->snd, msg); + if (rc) + return rc; + + /* Free previously allocated messages (if any). */ + virtsnd_pcm_msg_free(vss); + + return virtsnd_pcm_msg_alloc(vss, periods, period_bytes); +} + +/** + * virtsnd_pcm_hw_free() - Reset the parameters of the PCM substream. + * @substream: Kernel ALSA substream. + * + * Context: Process context. + * Return: 0 + */ +static int virtsnd_pcm_hw_free(struct snd_pcm_substream *substream) +{ + struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream); + + /* If the queue is flushed, we can safely free the messages here. */ + if (!virtsnd_pcm_msg_pending_num(vss)) + virtsnd_pcm_msg_free(vss); + + return 0; +} + +/** + * virtsnd_pcm_prepare() - Prepare the PCM substream. + * @substream: Kernel ALSA substream. + * + * The function can be called both from the upper level or from the driver + * itself. + * + * Context: Process context. + * Return: 0 on success, -errno on failure. + */ +static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream) +{ + struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream); + struct virtio_device *vdev = vss->snd->vdev; + struct virtio_snd_msg *msg; + + if (virtsnd_pcm_msg_pending_num(vss)) { + dev_err(&vdev->dev, "SID %u: invalid I/O queue state\n", + vss->sid); + return -EBADFD; + } + + vss->buffer_bytes = snd_pcm_lib_buffer_bytes(substream); + vss->hw_ptr = 0; + vss->xfer_xrun = false; + vss->msg_last_enqueued = -1; + vss->msg_count = 0; + + msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_PREPARE, + GFP_KERNEL); + if (!msg) + return -ENOMEM; + + return virtsnd_ctl_msg_send_sync(vss->snd, msg); +} + +/** + * virtsnd_pcm_trigger() - Process command for the PCM substream. + * @substream: Kernel ALSA substream. + * @command: Substream command (SNDRV_PCM_TRIGGER_XXX). + * + * Context: Any context. Takes and releases the VirtIO substream spinlock. + * May take and release the tx/rx queue spinlock. + * Return: 0 on success, -errno on failure. + */ +static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command) +{ + struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream); + struct virtio_snd *snd = vss->snd; + struct virtio_snd_msg *msg; + unsigned long flags; + int rc; + + switch (command) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: { + struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss); + + spin_lock_irqsave(&queue->lock, flags); + spin_lock(&vss->lock); + rc = virtsnd_pcm_msg_send(vss); + if (!rc) + vss->xfer_enabled = true; + spin_unlock(&vss->lock); + spin_unlock_irqrestore(&queue->lock, flags); + if (rc) + return rc; + + msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_START, + GFP_KERNEL); + if (!msg) { + spin_lock_irqsave(&vss->lock, flags); + vss->xfer_enabled = false; + spin_unlock_irqrestore(&vss->lock, flags); + + return -ENOMEM; + } + + return virtsnd_ctl_msg_send_sync(snd, msg); + } + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: { + spin_lock_irqsave(&vss->lock, flags); + vss->xfer_enabled = false; + spin_unlock_irqrestore(&vss->lock, flags); + + /* + * The substream needs to be released on the device side only + * when it is completely stopped. + */ + vss->release = (command == SNDRV_PCM_TRIGGER_STOP); + + /* + * The STOP command can be issued in an atomic context after + * the drain is complete. Therefore, in general, we cannot sleep + * here. + */ + msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_STOP, + GFP_ATOMIC); + if (!msg) + return -ENOMEM; + + return virtsnd_ctl_msg_send_async(snd, msg); + } + default: { + return -EINVAL; + } + } +} + +/** + * virtsnd_pcm_sync_stop() - Synchronous PCM substream stop. + * @substream: Kernel ALSA substream. + * + * The function can be called both from the upper level or from the driver + * itself. + * + * Context: Process context. Takes and releases the VirtIO substream spinlock. + * Return: 0 on success, -errno on failure. + */ +static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream) +{ + struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream); + struct virtio_snd *snd = vss->snd; + struct virtio_snd_msg *msg; + unsigned int js = msecs_to_jiffies(msg_timeout_ms); + int rc; + + if (!vss->release) + return 0; + + msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_RELEASE, + GFP_KERNEL); + if (!msg) + return -ENOMEM; + + rc = virtsnd_ctl_msg_send_sync(snd, msg); + if (rc) + return rc; + + /* + * The spec states that upon receipt of the RELEASE command "the device + * MUST complete all pending I/O messages for the specified stream ID". + * Thus, we consider the absence of I/O messages in the queue as an + * indication that the substream has been released. + */ + rc = wait_event_interruptible_timeout(vss->msg_empty, + !virtsnd_pcm_msg_pending_num(vss), + js); + if (rc <= 0) { + dev_warn(&snd->vdev->dev, "SID %u: failed to flush I/O queue\n", + vss->sid); + + return !rc ? -ETIMEDOUT : rc; + } + + vss->release = false; + + return 0; +} + +/** + * virtsnd_pcm_pointer() - Get the current hardware position for the PCM + * substream. + * @substream: Kernel ALSA substream. + * + * Context: Any context. Takes and releases the VirtIO substream spinlock. + * Return: Hardware position in frames inside [0 ... buffer_size) range. + */ +static snd_pcm_uframes_t +virtsnd_pcm_pointer(struct snd_pcm_substream *substream) +{ + struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream); + snd_pcm_uframes_t hw_ptr = SNDRV_PCM_POS_XRUN; + unsigned long flags; + + spin_lock_irqsave(&vss->lock, flags); + if (!vss->xfer_xrun) + hw_ptr = bytes_to_frames(substream->runtime, vss->hw_ptr); + spin_unlock_irqrestore(&vss->lock, flags); + + return hw_ptr; +} + +/* PCM substream operators map. */ +const struct snd_pcm_ops virtsnd_pcm_ops = { + .open = virtsnd_pcm_open, + .close = virtsnd_pcm_close, + .ioctl = snd_pcm_lib_ioctl, + .hw_params = virtsnd_pcm_hw_params, + .hw_free = virtsnd_pcm_hw_free, + .prepare = virtsnd_pcm_prepare, + .trigger = virtsnd_pcm_trigger, + .sync_stop = virtsnd_pcm_sync_stop, + .pointer = virtsnd_pcm_pointer, +};
On Sat, 27 Feb 2021 09:59:53 +0100, Anton Yakovlev wrote:
+static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command) +{
- struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
- struct virtio_snd *snd = vss->snd;
- struct virtio_snd_msg *msg;
- unsigned long flags;
- int rc;
- switch (command) {
- case SNDRV_PCM_TRIGGER_START:
- case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: {
struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
spin_lock_irqsave(&queue->lock, flags);
spin_lock(&vss->lock);
rc = virtsnd_pcm_msg_send(vss);
if (!rc)
vss->xfer_enabled = true;
spin_unlock(&vss->lock);
spin_unlock_irqrestore(&queue->lock, flags);
if (rc)
return rc;
msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_START,
GFP_KERNEL);
If we drop nonatomic PCM, this has to be changed: GFP_KERNEL is no longer valid in the trigger and the pointer callbacks. I wonder, though, the code below uses GFP_ATOMIC inconsistently...
- case SNDRV_PCM_TRIGGER_STOP:
- case SNDRV_PCM_TRIGGER_PAUSE_PUSH: {
spin_lock_irqsave(&vss->lock, flags);
vss->xfer_enabled = false;
spin_unlock_irqrestore(&vss->lock, flags);
/*
* The substream needs to be released on the device side only
* when it is completely stopped.
*/
vss->release = (command == SNDRV_PCM_TRIGGER_STOP);
/*
* The STOP command can be issued in an atomic context after
* the drain is complete. Therefore, in general, we cannot sleep
* here.
*/
msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_STOP,
GFP_ATOMIC);
BTW...
- default: {
return -EINVAL;
- }
Let's avoid braces inside the switch() unless it's inevitably needed. It makes the code harder to read.
+static snd_pcm_uframes_t +virtsnd_pcm_pointer(struct snd_pcm_substream *substream) +{
- struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
- snd_pcm_uframes_t hw_ptr = SNDRV_PCM_POS_XRUN;
- unsigned long flags;
- spin_lock_irqsave(&vss->lock, flags);
- if (!vss->xfer_xrun)
hw_ptr = bytes_to_frames(substream->runtime, vss->hw_ptr);
- spin_unlock_irqrestore(&vss->lock, flags);
Oh, and if we drop nonatomc PCM, both trigger and pointer callbacks are guaranteed to be called inside the spinlock, hence you can remove *_irqsave() but just us spin_lock() in those two callbacks.
thanks,
Takashi
On 28.02.2021 12:32, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:53 +0100, Anton Yakovlev wrote:
[snip]
+static snd_pcm_uframes_t +virtsnd_pcm_pointer(struct snd_pcm_substream *substream) +{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
snd_pcm_uframes_t hw_ptr = SNDRV_PCM_POS_XRUN;
unsigned long flags;
spin_lock_irqsave(&vss->lock, flags);
if (!vss->xfer_xrun)
hw_ptr = bytes_to_frames(substream->runtime, vss->hw_ptr);
spin_unlock_irqrestore(&vss->lock, flags);
Oh, and if we drop nonatomc PCM, both trigger and pointer callbacks are guaranteed to be called inside the spinlock, hence you can remove *_irqsave() but just us spin_lock() in those two callbacks.
As I understand, spin_lock_irqsave() disables only local interrupts. But what about other CPU cores?
thanks,
Takashi
On Mon, 01 Mar 2021 10:29:24 +0100, Anton Yakovlev wrote:
On 28.02.2021 12:32, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:53 +0100, Anton Yakovlev wrote:
[snip]
+static snd_pcm_uframes_t +virtsnd_pcm_pointer(struct snd_pcm_substream *substream) +{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
snd_pcm_uframes_t hw_ptr = SNDRV_PCM_POS_XRUN;
unsigned long flags;
spin_lock_irqsave(&vss->lock, flags);
if (!vss->xfer_xrun)
hw_ptr = bytes_to_frames(substream->runtime, vss->hw_ptr);
spin_unlock_irqrestore(&vss->lock, flags);
Oh, and if we drop nonatomc PCM, both trigger and pointer callbacks are guaranteed to be called inside the spinlock, hence you can remove *_irqsave() but just us spin_lock() in those two callbacks.
As I understand, spin_lock_irqsave() disables only local interrupts. But what about other CPU cores?
Those are already under spin_lock_irqsave() of the PCM substream lock (in the case of normal PCM operation without nonatomic flag).
Takashi
Enumerate all available jacks and create ALSA controls.
At the moment jacks have a simple implementation and can only be used to receive notifications about a plugged in/out device.
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com --- sound/virtio/Makefile | 1 + sound/virtio/virtio_card.c | 14 +++ sound/virtio/virtio_card.h | 12 ++ sound/virtio/virtio_jack.c | 233 +++++++++++++++++++++++++++++++++++++ 4 files changed, 260 insertions(+) create mode 100644 sound/virtio/virtio_jack.c
diff --git a/sound/virtio/Makefile b/sound/virtio/Makefile index 34493226793f..09f485291285 100644 --- a/sound/virtio/Makefile +++ b/sound/virtio/Makefile @@ -5,6 +5,7 @@ obj-$(CONFIG_SND_VIRTIO) += virtio_snd.o virtio_snd-objs := \ virtio_card.o \ virtio_ctl_msg.o \ + virtio_jack.o \ virtio_pcm.o \ virtio_pcm_msg.o \ virtio_pcm_ops.o diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c index 9f74261b234c..c852afda5712 100644 --- a/sound/virtio/virtio_card.c +++ b/sound/virtio/virtio_card.c @@ -67,6 +67,10 @@ static void virtsnd_event_notify_cb(struct virtqueue *vqueue) virtqueue_disable_cb(vqueue); while ((event = virtqueue_get_buf(vqueue, &length))) { switch (le32_to_cpu(event->hdr.code)) { + case VIRTIO_SND_EVT_JACK_CONNECTED: + case VIRTIO_SND_EVT_JACK_DISCONNECTED: + virtsnd_jack_event(snd, event); + break; case VIRTIO_SND_EVT_PCM_PERIOD_ELAPSED: case VIRTIO_SND_EVT_PCM_XRUN: virtsnd_pcm_event(snd, event); @@ -185,10 +189,20 @@ static int virtsnd_build_devs(struct virtio_snd *snd) VIRTIO_SND_CARD_NAME " at %s/%s", dev_name(dev->parent), dev_name(dev));
+ rc = virtsnd_jack_parse_cfg(snd); + if (rc) + return rc; + rc = virtsnd_pcm_parse_cfg(snd); if (rc) return rc;
+ if (snd->njacks) { + rc = virtsnd_jack_build_devs(snd); + if (rc) + return rc; + } + if (snd->nsubstreams) { rc = virtsnd_pcm_build_devs(snd); if (rc) diff --git a/sound/virtio/virtio_card.h b/sound/virtio/virtio_card.h index 7e50f3835ab1..2092da297a9d 100644 --- a/sound/virtio/virtio_card.h +++ b/sound/virtio/virtio_card.h @@ -18,6 +18,7 @@ #define VIRTIO_SND_CARD_NAME "VirtIO SoundCard" #define VIRTIO_SND_PCM_NAME "VirtIO PCM"
+struct virtio_jack; struct virtio_pcm_substream;
/** @@ -38,6 +39,8 @@ struct virtio_snd_queue { * @ctl_msgs: Pending control request list. * @event_msgs: Device events. * @pcm_list: VirtIO PCM device list. + * @jacks: VirtIO jacks. + * @njacks: Number of jacks. * @substreams: VirtIO PCM substreams. * @nsubstreams: Number of PCM substreams. */ @@ -48,6 +51,8 @@ struct virtio_snd { struct list_head ctl_msgs; struct virtio_snd_event *event_msgs; struct list_head pcm_list; + struct virtio_jack *jacks; + u32 njacks; struct virtio_pcm_substream *substreams; u32 nsubstreams; }; @@ -88,4 +93,11 @@ virtsnd_pcm_queue(struct virtio_pcm_substream *vss) return virtsnd_rx_queue(vss->snd); }
+int virtsnd_jack_parse_cfg(struct virtio_snd *snd); + +int virtsnd_jack_build_devs(struct virtio_snd *snd); + +void virtsnd_jack_event(struct virtio_snd *snd, + struct virtio_snd_event *event); + #endif /* VIRTIO_SND_CARD_H */ diff --git a/sound/virtio/virtio_jack.c b/sound/virtio/virtio_jack.c new file mode 100644 index 000000000000..c69f1dcdcc84 --- /dev/null +++ b/sound/virtio/virtio_jack.c @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * virtio-snd: Virtio sound device + * Copyright (C) 2021 OpenSynergy GmbH + */ +#include <linux/virtio_config.h> +#include <sound/jack.h> +#include <sound/hda_verbs.h> + +#include "virtio_card.h" + +/** + * DOC: Implementation Status + * + * At the moment jacks have a simple implementation and can only be used to + * receive notifications about a plugged in/out device. + * + * VIRTIO_SND_R_JACK_REMAP + * is not supported + */ + +/** + * struct virtio_jack - VirtIO jack. + * @jack: Kernel jack control. + * @nid: Functional group node identifier. + * @features: Jack virtio feature bit map (1 << VIRTIO_SND_JACK_F_XXX). + * @defconf: Pin default configuration value. + * @caps: Pin capabilities value. + * @connected: Current jack connection status. + * @type: Kernel jack type (SND_JACK_XXX). + */ +struct virtio_jack { + struct snd_jack *jack; + u32 nid; + u32 features; + u32 defconf; + u32 caps; + bool connected; + int type; +}; + +/** + * virtsnd_jack_get_label() - Get the name string for the jack. + * @vjack: VirtIO jack. + * + * Returns the jack name based on the default pin configuration value (see HDA + * specification). + * + * Context: Any context. + * Return: Name string. + */ +static const char *virtsnd_jack_get_label(struct virtio_jack *vjack) +{ + unsigned int defconf = vjack->defconf; + unsigned int device = + (defconf & AC_DEFCFG_DEVICE) >> AC_DEFCFG_DEVICE_SHIFT; + unsigned int location = + (defconf & AC_DEFCFG_LOCATION) >> AC_DEFCFG_LOCATION_SHIFT; + + switch (device) { + case AC_JACK_LINE_OUT: + return "Line Out"; + case AC_JACK_SPEAKER: + return "Speaker"; + case AC_JACK_HP_OUT: + return "Headphone"; + case AC_JACK_CD: + return "CD"; + case AC_JACK_SPDIF_OUT: + case AC_JACK_DIG_OTHER_OUT: + if (location == AC_JACK_LOC_HDMI) + return "HDMI Out"; + else + return "SPDIF Out"; + case AC_JACK_LINE_IN: + return "Line"; + case AC_JACK_AUX: + return "Aux"; + case AC_JACK_MIC_IN: + return "Mic"; + case AC_JACK_SPDIF_IN: + return "SPDIF In"; + case AC_JACK_DIG_OTHER_IN: + return "Digital In"; + default: + return "Misc"; + } +} + +/** + * virtsnd_jack_get_type() - Get the type for the jack. + * @vjack: VirtIO jack. + * + * Returns the jack type based on the default pin configuration value (see HDA + * specification). + * + * Context: Any context. + * Return: SND_JACK_XXX value. + */ +static int virtsnd_jack_get_type(struct virtio_jack *vjack) +{ + unsigned int defconf = vjack->defconf; + unsigned int device = + (defconf & AC_DEFCFG_DEVICE) >> AC_DEFCFG_DEVICE_SHIFT; + + switch (device) { + case AC_JACK_LINE_OUT: + case AC_JACK_SPEAKER: + return SND_JACK_LINEOUT; + case AC_JACK_HP_OUT: + return SND_JACK_HEADPHONE; + case AC_JACK_SPDIF_OUT: + case AC_JACK_DIG_OTHER_OUT: + return SND_JACK_AVOUT; + case AC_JACK_MIC_IN: + return SND_JACK_MICROPHONE; + default: + return SND_JACK_LINEIN; + } +} + +/** + * virtsnd_jack_parse_cfg() - Parse the jack configuration. + * @snd: VirtIO sound device. + * + * This function is called during initial device initialization. + * + * Context: Any context that permits to sleep. + * Return: 0 on success, -errno on failure. + */ +int virtsnd_jack_parse_cfg(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_snd_jack_info *info; + u32 i; + int rc; + + virtio_cread_le(vdev, struct virtio_snd_config, jacks, &snd->njacks); + if (!snd->njacks) + return 0; + + snd->jacks = devm_kcalloc(&vdev->dev, snd->njacks, sizeof(*snd->jacks), + GFP_KERNEL); + if (!snd->jacks) + return -ENOMEM; + + info = kcalloc(snd->njacks, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + rc = virtsnd_ctl_query_info(snd, VIRTIO_SND_R_JACK_INFO, 0, snd->njacks, + sizeof(*info), info); + if (rc) + goto on_exit; + + for (i = 0; i < snd->njacks; ++i) { + struct virtio_jack *vjack = &snd->jacks[i]; + + vjack->nid = le32_to_cpu(info[i].hdr.hda_fn_nid); + vjack->features = le32_to_cpu(info[i].features); + vjack->defconf = le32_to_cpu(info[i].hda_reg_defconf); + vjack->caps = le32_to_cpu(info[i].hda_reg_caps); + vjack->connected = info[i].connected; + } + +on_exit: + kfree(info); + + return rc; +} + +/** + * virtsnd_jack_build_devs() - Build ALSA controls for jacks. + * @snd: VirtIO sound device. + * + * Context: Any context that permits to sleep. + * Return: 0 on success, -errno on failure. + */ +int virtsnd_jack_build_devs(struct virtio_snd *snd) +{ + u32 i; + int rc; + + for (i = 0; i < snd->njacks; ++i) { + struct virtio_jack *vjack = &snd->jacks[i]; + + vjack->type = virtsnd_jack_get_type(vjack); + + rc = snd_jack_new(snd->card, virtsnd_jack_get_label(vjack), + vjack->type, &vjack->jack, true, true); + if (rc) + return rc; + + if (vjack->jack) + vjack->jack->private_data = vjack; + + snd_jack_report(vjack->jack, + vjack->connected ? vjack->type : 0); + } + + return 0; +} + +/** + * virtsnd_jack_event() - Handle the jack event notification. + * @snd: VirtIO sound device. + * @event: VirtIO sound event. + * + * Context: Interrupt context. + */ +void virtsnd_jack_event(struct virtio_snd *snd, struct virtio_snd_event *event) +{ + u32 jack_id = le32_to_cpu(event->data); + struct virtio_jack *vjack; + + if (jack_id >= snd->njacks) + return; + + vjack = &snd->jacks[jack_id]; + + switch (le32_to_cpu(event->hdr.code)) { + case VIRTIO_SND_EVT_JACK_CONNECTED: + vjack->connected = true; + break; + case VIRTIO_SND_EVT_JACK_DISCONNECTED: + vjack->connected = false; + break; + default: + return; + } + + snd_jack_report(vjack->jack, vjack->connected ? vjack->type : 0); +}
Enumerate all available PCM channel maps and create ALSA controls.
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com --- sound/virtio/Makefile | 1 + sound/virtio/virtio_card.c | 10 ++ sound/virtio/virtio_card.h | 8 ++ sound/virtio/virtio_chmap.c | 219 ++++++++++++++++++++++++++++++++++++ sound/virtio/virtio_pcm.h | 4 + 5 files changed, 242 insertions(+) create mode 100644 sound/virtio/virtio_chmap.c
diff --git a/sound/virtio/Makefile b/sound/virtio/Makefile index 09f485291285..2742bddb8874 100644 --- a/sound/virtio/Makefile +++ b/sound/virtio/Makefile @@ -4,6 +4,7 @@ obj-$(CONFIG_SND_VIRTIO) += virtio_snd.o
virtio_snd-objs := \ virtio_card.o \ + virtio_chmap.o \ virtio_ctl_msg.o \ virtio_jack.o \ virtio_pcm.o \ diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c index c852afda5712..59455a562018 100644 --- a/sound/virtio/virtio_card.c +++ b/sound/virtio/virtio_card.c @@ -197,6 +197,10 @@ static int virtsnd_build_devs(struct virtio_snd *snd) if (rc) return rc;
+ rc = virtsnd_chmap_parse_cfg(snd); + if (rc) + return rc; + if (snd->njacks) { rc = virtsnd_jack_build_devs(snd); if (rc) @@ -209,6 +213,12 @@ static int virtsnd_build_devs(struct virtio_snd *snd) return rc; }
+ if (snd->nchmaps) { + rc = virtsnd_chmap_build_devs(snd); + if (rc) + return rc; + } + return snd_card_register(snd->card); }
diff --git a/sound/virtio/virtio_card.h b/sound/virtio/virtio_card.h index 2092da297a9d..6af3d4816f57 100644 --- a/sound/virtio/virtio_card.h +++ b/sound/virtio/virtio_card.h @@ -43,6 +43,8 @@ struct virtio_snd_queue { * @njacks: Number of jacks. * @substreams: VirtIO PCM substreams. * @nsubstreams: Number of PCM substreams. + * @chmaps: VirtIO channel maps. + * @nchmaps: Number of channel maps. */ struct virtio_snd { struct virtio_device *vdev; @@ -55,6 +57,8 @@ struct virtio_snd { u32 njacks; struct virtio_pcm_substream *substreams; u32 nsubstreams; + struct virtio_snd_chmap_info *chmaps; + u32 nchmaps; };
/* Message completion timeout in milliseconds (module parameter). */ @@ -100,4 +104,8 @@ int virtsnd_jack_build_devs(struct virtio_snd *snd); void virtsnd_jack_event(struct virtio_snd *snd, struct virtio_snd_event *event);
+int virtsnd_chmap_parse_cfg(struct virtio_snd *snd); + +int virtsnd_chmap_build_devs(struct virtio_snd *snd); + #endif /* VIRTIO_SND_CARD_H */ diff --git a/sound/virtio/virtio_chmap.c b/sound/virtio/virtio_chmap.c new file mode 100644 index 000000000000..5bc924933a59 --- /dev/null +++ b/sound/virtio/virtio_chmap.c @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * virtio-snd: Virtio sound device + * Copyright (C) 2021 OpenSynergy GmbH + */ +#include <linux/virtio_config.h> + +#include "virtio_card.h" + +/* VirtIO->ALSA channel position map */ +static const u8 g_v2a_position_map[] = { + [VIRTIO_SND_CHMAP_NONE] = SNDRV_CHMAP_UNKNOWN, + [VIRTIO_SND_CHMAP_NA] = SNDRV_CHMAP_NA, + [VIRTIO_SND_CHMAP_MONO] = SNDRV_CHMAP_MONO, + [VIRTIO_SND_CHMAP_FL] = SNDRV_CHMAP_FL, + [VIRTIO_SND_CHMAP_FR] = SNDRV_CHMAP_FR, + [VIRTIO_SND_CHMAP_RL] = SNDRV_CHMAP_RL, + [VIRTIO_SND_CHMAP_RR] = SNDRV_CHMAP_RR, + [VIRTIO_SND_CHMAP_FC] = SNDRV_CHMAP_FC, + [VIRTIO_SND_CHMAP_LFE] = SNDRV_CHMAP_LFE, + [VIRTIO_SND_CHMAP_SL] = SNDRV_CHMAP_SL, + [VIRTIO_SND_CHMAP_SR] = SNDRV_CHMAP_SR, + [VIRTIO_SND_CHMAP_RC] = SNDRV_CHMAP_RC, + [VIRTIO_SND_CHMAP_FLC] = SNDRV_CHMAP_FLC, + [VIRTIO_SND_CHMAP_FRC] = SNDRV_CHMAP_FRC, + [VIRTIO_SND_CHMAP_RLC] = SNDRV_CHMAP_RLC, + [VIRTIO_SND_CHMAP_RRC] = SNDRV_CHMAP_RRC, + [VIRTIO_SND_CHMAP_FLW] = SNDRV_CHMAP_FLW, + [VIRTIO_SND_CHMAP_FRW] = SNDRV_CHMAP_FRW, + [VIRTIO_SND_CHMAP_FLH] = SNDRV_CHMAP_FLH, + [VIRTIO_SND_CHMAP_FCH] = SNDRV_CHMAP_FCH, + [VIRTIO_SND_CHMAP_FRH] = SNDRV_CHMAP_FRH, + [VIRTIO_SND_CHMAP_TC] = SNDRV_CHMAP_TC, + [VIRTIO_SND_CHMAP_TFL] = SNDRV_CHMAP_TFL, + [VIRTIO_SND_CHMAP_TFR] = SNDRV_CHMAP_TFR, + [VIRTIO_SND_CHMAP_TFC] = SNDRV_CHMAP_TFC, + [VIRTIO_SND_CHMAP_TRL] = SNDRV_CHMAP_TRL, + [VIRTIO_SND_CHMAP_TRR] = SNDRV_CHMAP_TRR, + [VIRTIO_SND_CHMAP_TRC] = SNDRV_CHMAP_TRC, + [VIRTIO_SND_CHMAP_TFLC] = SNDRV_CHMAP_TFLC, + [VIRTIO_SND_CHMAP_TFRC] = SNDRV_CHMAP_TFRC, + [VIRTIO_SND_CHMAP_TSL] = SNDRV_CHMAP_TSL, + [VIRTIO_SND_CHMAP_TSR] = SNDRV_CHMAP_TSR, + [VIRTIO_SND_CHMAP_LLFE] = SNDRV_CHMAP_LLFE, + [VIRTIO_SND_CHMAP_RLFE] = SNDRV_CHMAP_RLFE, + [VIRTIO_SND_CHMAP_BC] = SNDRV_CHMAP_BC, + [VIRTIO_SND_CHMAP_BLC] = SNDRV_CHMAP_BLC, + [VIRTIO_SND_CHMAP_BRC] = SNDRV_CHMAP_BRC +}; + +/** + * virtsnd_chmap_parse_cfg() - Parse the channel map configuration. + * @snd: VirtIO sound device. + * + * This function is called during initial device initialization. + * + * Context: Any context that permits to sleep. + * Return: 0 on success, -errno on failure. + */ +int virtsnd_chmap_parse_cfg(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + u32 i; + int rc; + + virtio_cread_le(vdev, struct virtio_snd_config, chmaps, &snd->nchmaps); + if (!snd->nchmaps) + return 0; + + snd->chmaps = devm_kcalloc(&vdev->dev, snd->nchmaps, + sizeof(*snd->chmaps), GFP_KERNEL); + if (!snd->chmaps) + return -ENOMEM; + + rc = virtsnd_ctl_query_info(snd, VIRTIO_SND_R_CHMAP_INFO, 0, + snd->nchmaps, sizeof(*snd->chmaps), + snd->chmaps); + if (rc) + return rc; + + /* Count the number of channel maps per each PCM device/stream. */ + for (i = 0; i < snd->nchmaps; ++i) { + struct virtio_snd_chmap_info *info = &snd->chmaps[i]; + u32 nid = le32_to_cpu(info->hdr.hda_fn_nid); + struct virtio_pcm *vpcm; + struct virtio_pcm_stream *vs; + + vpcm = virtsnd_pcm_find_or_create(snd, nid); + if (IS_ERR(vpcm)) + return PTR_ERR(vpcm); + + switch (info->direction) { + case VIRTIO_SND_D_OUTPUT: + vs = &vpcm->streams[SNDRV_PCM_STREAM_PLAYBACK]; + break; + case VIRTIO_SND_D_INPUT: + vs = &vpcm->streams[SNDRV_PCM_STREAM_CAPTURE]; + break; + default: + dev_err(&vdev->dev, + "chmap #%u: unknown direction (%u)\n", i, + info->direction); + return -EINVAL; + } + + vs->nchmaps++; + } + + return 0; +} + +/** + * virtsnd_chmap_add_ctls() - Create an ALSA control for channel maps. + * @pcm: ALSA PCM device. + * @direction: PCM stream direction (SNDRV_PCM_STREAM_XXX). + * @vs: VirtIO PCM stream. + * + * Context: Any context. + * Return: 0 on success, -errno on failure. + */ +static int virtsnd_chmap_add_ctls(struct snd_pcm *pcm, int direction, + struct virtio_pcm_stream *vs) +{ + u32 i; + int max_channels = 0; + + for (i = 0; i < vs->nchmaps; i++) + if (max_channels < vs->chmaps[i].channels) + max_channels = vs->chmaps[i].channels; + + return snd_pcm_add_chmap_ctls(pcm, direction, vs->chmaps, max_channels, + 0, NULL); +} + +/** + * virtsnd_chmap_build_devs() - Build ALSA controls for channel maps. + * @snd: VirtIO sound device. + * + * Context: Any context. + * Return: 0 on success, -errno on failure. + */ +int virtsnd_chmap_build_devs(struct virtio_snd *snd) +{ + struct virtio_device *vdev = snd->vdev; + struct virtio_pcm *vpcm; + struct virtio_pcm_stream *vs; + u32 i; + int rc; + + /* Allocate channel map elements per each PCM device/stream. */ + list_for_each_entry(vpcm, &snd->pcm_list, list) { + for (i = 0; i < ARRAY_SIZE(vpcm->streams); ++i) { + vs = &vpcm->streams[i]; + + if (!vs->nchmaps) + continue; + + vs->chmaps = devm_kcalloc(&vdev->dev, vs->nchmaps + 1, + sizeof(*vs->chmaps), + GFP_KERNEL); + if (!vs->chmaps) + return -ENOMEM; + + vs->nchmaps = 0; + } + } + + /* Initialize channel maps per each PCM device/stream. */ + for (i = 0; i < snd->nchmaps; ++i) { + struct virtio_snd_chmap_info *info = &snd->chmaps[i]; + unsigned int channels = info->channels; + unsigned int ch; + struct snd_pcm_chmap_elem *chmap; + + vpcm = virtsnd_pcm_find(snd, le32_to_cpu(info->hdr.hda_fn_nid)); + if (IS_ERR(vpcm)) + return PTR_ERR(vpcm); + + if (info->direction == VIRTIO_SND_D_OUTPUT) + vs = &vpcm->streams[SNDRV_PCM_STREAM_PLAYBACK]; + else + vs = &vpcm->streams[SNDRV_PCM_STREAM_CAPTURE]; + + chmap = &vs->chmaps[vs->nchmaps++]; + + if (channels > ARRAY_SIZE(chmap->map)) + channels = ARRAY_SIZE(chmap->map); + + chmap->channels = channels; + + for (ch = 0; ch < channels; ++ch) { + u8 position = info->positions[ch]; + + if (position >= ARRAY_SIZE(g_v2a_position_map)) + return -EINVAL; + + chmap->map[ch] = g_v2a_position_map[position]; + } + } + + /* Create an ALSA control per each PCM device/stream. */ + list_for_each_entry(vpcm, &snd->pcm_list, list) { + if (!vpcm->pcm) + continue; + + for (i = 0; i < ARRAY_SIZE(vpcm->streams); ++i) { + vs = &vpcm->streams[i]; + + if (!vs->nchmaps) + continue; + + rc = virtsnd_chmap_add_ctls(vpcm->pcm, i, vs); + if (rc) + return rc; + } + } + + return 0; +} diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h index 55dfb19d14b3..1e683aeadde9 100644 --- a/sound/virtio/virtio_pcm.h +++ b/sound/virtio/virtio_pcm.h @@ -60,10 +60,14 @@ struct virtio_pcm_substream { * struct virtio_pcm_stream - VirtIO PCM stream. * @substreams: VirtIO substreams belonging to the stream. * @nsubstreams: Number of substreams. + * @chmaps: Kernel channel maps belonging to the stream. + * @nchmaps: Number of channel maps. */ struct virtio_pcm_stream { struct virtio_pcm_substream **substreams; u32 nsubstreams; + struct snd_pcm_chmap_elem *chmaps; + u32 nchmaps; };
/**
All running PCM substreams are stopped on device suspend and restarted on device resume.
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com --- sound/virtio/virtio_card.c | 56 +++++++++++++++++++++++++++++++++++ sound/virtio/virtio_pcm.c | 1 + sound/virtio/virtio_pcm_ops.c | 41 ++++++++++++++++++++----- 3 files changed, 90 insertions(+), 8 deletions(-)
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c index 59455a562018..c7ae8801991d 100644 --- a/sound/virtio/virtio_card.c +++ b/sound/virtio/virtio_card.c @@ -323,6 +323,58 @@ static void virtsnd_remove(struct virtio_device *vdev) kfree(snd->event_msgs); }
+#ifdef CONFIG_PM_SLEEP +/** + * virtsnd_freeze() - Suspend device. + * @vdev: VirtIO parent device. + * + * Context: Any context. + * Return: 0 on success, -errno on failure. + */ +static int virtsnd_freeze(struct virtio_device *vdev) +{ + struct virtio_snd *snd = vdev->priv; + + virtsnd_ctl_msg_cancel_all(snd); + + vdev->config->del_vqs(vdev); + vdev->config->reset(vdev); + + kfree(snd->event_msgs); + + /* + * If the virtsnd_restore() fails before re-allocating events, then we + * get a dangling pointer here. + */ + snd->event_msgs = NULL; + + return 0; +} + +/** + * virtsnd_restore() - Resume device. + * @vdev: VirtIO parent device. + * + * Context: Any context. + * Return: 0 on success, -errno on failure. + */ +static int virtsnd_restore(struct virtio_device *vdev) +{ + struct virtio_snd *snd = vdev->priv; + int rc; + + rc = virtsnd_find_vqs(snd); + if (rc) + return rc; + + virtio_device_ready(vdev); + + virtsnd_enable_event_vq(snd); + + return 0; +} +#endif /* CONFIG_PM_SLEEP */ + static const struct virtio_device_id id_table[] = { { VIRTIO_ID_SOUND, VIRTIO_DEV_ANY_ID }, { 0 }, @@ -335,6 +387,10 @@ static struct virtio_driver virtsnd_driver = { .validate = virtsnd_validate, .probe = virtsnd_probe, .remove = virtsnd_remove, +#ifdef CONFIG_PM_SLEEP + .freeze = virtsnd_freeze, + .restore = virtsnd_restore, +#endif };
static int __init init(void) diff --git a/sound/virtio/virtio_pcm.c b/sound/virtio/virtio_pcm.c index 3605151860f2..4a4a6583b002 100644 --- a/sound/virtio/virtio_pcm.c +++ b/sound/virtio/virtio_pcm.c @@ -109,6 +109,7 @@ static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *vss, SNDRV_PCM_INFO_BATCH | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_RESUME | SNDRV_PCM_INFO_PAUSE;
if (!info->channels_min || info->channels_min > info->channels_max) { diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c index d02d79bd94f3..03a7e2666cfb 100644 --- a/sound/virtio/virtio_pcm_ops.c +++ b/sound/virtio/virtio_pcm_ops.c @@ -167,7 +167,8 @@ static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream, int vrate = -1; int rc;
- if (virtsnd_pcm_msg_pending_num(vss)) { + if (runtime->status->state != SNDRV_PCM_STATE_SUSPENDED && + virtsnd_pcm_msg_pending_num(vss)) { dev_err(&vdev->dev, "SID %u: invalid I/O queue state\n", vss->sid); return -EBADFD; @@ -231,6 +232,10 @@ static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream, if (rc) return rc;
+ /* If messages have already been allocated before, do nothing. */ + if (runtime->status->state == SNDRV_PCM_STATE_SUSPENDED) + return 0; + /* Free previously allocated messages (if any). */ virtsnd_pcm_msg_free(vss);
@@ -267,20 +272,24 @@ static int virtsnd_pcm_hw_free(struct snd_pcm_substream *substream) */ static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream) { + struct snd_pcm_runtime *runtime = substream->runtime; struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream); struct virtio_device *vdev = vss->snd->vdev; struct virtio_snd_msg *msg;
- if (virtsnd_pcm_msg_pending_num(vss)) { - dev_err(&vdev->dev, "SID %u: invalid I/O queue state\n", - vss->sid); - return -EBADFD; + if (runtime->status->state != SNDRV_PCM_STATE_SUSPENDED) { + if (virtsnd_pcm_msg_pending_num(vss)) { + dev_err(&vdev->dev, "SID %u: invalid I/O queue state\n", + vss->sid); + return -EBADFD; + } + + vss->buffer_bytes = snd_pcm_lib_buffer_bytes(substream); + vss->hw_ptr = 0; + vss->msg_last_enqueued = -1; }
- vss->buffer_bytes = snd_pcm_lib_buffer_bytes(substream); - vss->hw_ptr = 0; vss->xfer_xrun = false; - vss->msg_last_enqueued = -1; vss->msg_count = 0;
msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_PREPARE, @@ -309,6 +318,21 @@ static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command) int rc;
switch (command) { + case SNDRV_PCM_TRIGGER_RESUME: { + /* + * We restart the substream by executing the standard command + * sequence. + */ + rc = virtsnd_pcm_hw_params(substream, NULL); + if (rc) + return rc; + + rc = virtsnd_pcm_prepare(substream); + if (rc) + return rc; + + fallthrough; + } case SNDRV_PCM_TRIGGER_START: case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: { struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss); @@ -335,6 +359,7 @@ static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command)
return virtsnd_ctl_msg_send_sync(snd, msg); } + case SNDRV_PCM_TRIGGER_SUSPEND: case SNDRV_PCM_TRIGGER_STOP: case SNDRV_PCM_TRIGGER_PAUSE_PUSH: { spin_lock_irqsave(&vss->lock, flags);
On Sat, 27 Feb 2021 09:59:56 +0100, Anton Yakovlev wrote:
All running PCM substreams are stopped on device suspend and restarted on device resume.
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com
sound/virtio/virtio_card.c | 56 +++++++++++++++++++++++++++++++++++ sound/virtio/virtio_pcm.c | 1 + sound/virtio/virtio_pcm_ops.c | 41 ++++++++++++++++++++----- 3 files changed, 90 insertions(+), 8 deletions(-)
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c index 59455a562018..c7ae8801991d 100644 --- a/sound/virtio/virtio_card.c +++ b/sound/virtio/virtio_card.c @@ -323,6 +323,58 @@ static void virtsnd_remove(struct virtio_device *vdev) kfree(snd->event_msgs); }
+#ifdef CONFIG_PM_SLEEP +/**
- virtsnd_freeze() - Suspend device.
- @vdev: VirtIO parent device.
- Context: Any context.
- Return: 0 on success, -errno on failure.
- */
+static int virtsnd_freeze(struct virtio_device *vdev) +{
- struct virtio_snd *snd = vdev->priv;
- virtsnd_ctl_msg_cancel_all(snd);
- vdev->config->del_vqs(vdev);
- vdev->config->reset(vdev);
- kfree(snd->event_msgs);
- /*
* If the virtsnd_restore() fails before re-allocating events, then we
* get a dangling pointer here.
*/
- snd->event_msgs = NULL;
- return 0;
I suppose some cancel of inflight works is needed? Ditto for the device removal, too.
--- a/sound/virtio/virtio_pcm.c +++ b/sound/virtio/virtio_pcm.c @@ -109,6 +109,7 @@ static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *vss, SNDRV_PCM_INFO_BATCH | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_PAUSE;SNDRV_PCM_INFO_RESUME |
Actually you don't need to set SNDRV_PCM_INFO_RESUME. This flag means that the driver supports the full resume procedure, which isn't often the case; with this, the driver is supposed to resume the stream exactly from the suspended position.
Most drivers don't set this but implement only the suspend-stop action. Then the application (or the sound backend) will re-setup the stream and restart accordingly.
@@ -309,6 +318,21 @@ static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command) int rc;
switch (command) {
- case SNDRV_PCM_TRIGGER_RESUME: {
/*
* We restart the substream by executing the standard command
* sequence.
*/
rc = virtsnd_pcm_hw_params(substream, NULL);
if (rc)
return rc;
rc = virtsnd_pcm_prepare(substream);
if (rc)
return rc;
And this won't work at all unless nonatomic PCM ops.
thanks,
Takashi
On 28.02.2021 13:05, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:56 +0100, Anton Yakovlev wrote:
All running PCM substreams are stopped on device suspend and restarted on device resume.
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com
sound/virtio/virtio_card.c | 56 +++++++++++++++++++++++++++++++++++ sound/virtio/virtio_pcm.c | 1 + sound/virtio/virtio_pcm_ops.c | 41 ++++++++++++++++++++----- 3 files changed, 90 insertions(+), 8 deletions(-)
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c index 59455a562018..c7ae8801991d 100644 --- a/sound/virtio/virtio_card.c +++ b/sound/virtio/virtio_card.c @@ -323,6 +323,58 @@ static void virtsnd_remove(struct virtio_device *vdev) kfree(snd->event_msgs); }
+#ifdef CONFIG_PM_SLEEP +/**
- virtsnd_freeze() - Suspend device.
- @vdev: VirtIO parent device.
- Context: Any context.
- Return: 0 on success, -errno on failure.
- */
+static int virtsnd_freeze(struct virtio_device *vdev) +{
struct virtio_snd *snd = vdev->priv;
virtsnd_ctl_msg_cancel_all(snd);
vdev->config->del_vqs(vdev);
vdev->config->reset(vdev);
kfree(snd->event_msgs);
/*
* If the virtsnd_restore() fails before re-allocating events, then we
* get a dangling pointer here.
*/
snd->event_msgs = NULL;
return 0;
I suppose some cancel of inflight works is needed? Ditto for the device removal, too.
It's not necessary here, since the device is reset and all of this are happened automatically. But in the device remove it makes sense also to disable events before calling snd_card_free(), since the device is still able to send notifications at that moment. Thanks!
--- a/sound/virtio/virtio_pcm.c +++ b/sound/virtio/virtio_pcm.c @@ -109,6 +109,7 @@ static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *vss, SNDRV_PCM_INFO_BATCH | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_RESUME | SNDRV_PCM_INFO_PAUSE;
Actually you don't need to set SNDRV_PCM_INFO_RESUME. This flag means that the driver supports the full resume procedure, which isn't often the case; with this, the driver is supposed to resume the stream exactly from the suspended position.
If I understood you right, that's exactly how resume is implemented now in the driver. Although we fully restart substream on the device side, from an application point of view it is resumed exactly at the same position.
Most drivers don't set this but implement only the suspend-stop action. Then the application (or the sound backend) will re-setup the stream and restart accordingly.
And an application must be aware of such possible situation? Since I have no doubt in alsa-lib, but I don't think that for example tinyalsa can handle this right.
@@ -309,6 +318,21 @@ static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command) int rc;
switch (command) {
case SNDRV_PCM_TRIGGER_RESUME: {
/*
* We restart the substream by executing the standard command
* sequence.
*/
rc = virtsnd_pcm_hw_params(substream, NULL);
if (rc)
return rc;
rc = virtsnd_pcm_prepare(substream);
if (rc)
return rc;
And this won't work at all unless nonatomic PCM ops.
thanks,
Takashi
On Mon, 01 Mar 2021 11:03:04 +0100, Anton Yakovlev wrote:
On 28.02.2021 13:05, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:56 +0100, Anton Yakovlev wrote:
All running PCM substreams are stopped on device suspend and restarted on device resume.
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com
sound/virtio/virtio_card.c | 56 +++++++++++++++++++++++++++++++++++ sound/virtio/virtio_pcm.c | 1 + sound/virtio/virtio_pcm_ops.c | 41 ++++++++++++++++++++----- 3 files changed, 90 insertions(+), 8 deletions(-)
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c index 59455a562018..c7ae8801991d 100644 --- a/sound/virtio/virtio_card.c +++ b/sound/virtio/virtio_card.c @@ -323,6 +323,58 @@ static void virtsnd_remove(struct virtio_device *vdev) kfree(snd->event_msgs); }
+#ifdef CONFIG_PM_SLEEP +/**
- virtsnd_freeze() - Suspend device.
- @vdev: VirtIO parent device.
- Context: Any context.
- Return: 0 on success, -errno on failure.
- */
+static int virtsnd_freeze(struct virtio_device *vdev) +{
struct virtio_snd *snd = vdev->priv;
virtsnd_ctl_msg_cancel_all(snd);
vdev->config->del_vqs(vdev);
vdev->config->reset(vdev);
kfree(snd->event_msgs);
/*
* If the virtsnd_restore() fails before re-allocating events, then we
* get a dangling pointer here.
*/
snd->event_msgs = NULL;
return 0;
I suppose some cancel of inflight works is needed? Ditto for the device removal, too.
It's not necessary here, since the device is reset and all of this are happened automatically.
Hrm, but the reset call itself might conflict with the inflight reset work? I haven't see any work canceling or flushing, so...
But in the device remove it makes sense also to disable events before calling snd_card_free(), since the device is still able to send notifications at that moment. Thanks!
--- a/sound/virtio/virtio_pcm.c +++ b/sound/virtio/virtio_pcm.c @@ -109,6 +109,7 @@ static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *vss, SNDRV_PCM_INFO_BATCH | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_RESUME | SNDRV_PCM_INFO_PAUSE;
Actually you don't need to set SNDRV_PCM_INFO_RESUME. This flag means that the driver supports the full resume procedure, which isn't often the case; with this, the driver is supposed to resume the stream exactly from the suspended position.
If I understood you right, that's exactly how resume is implemented now in the driver. Although we fully restart substream on the device side, from an application point of view it is resumed exactly at the same position.
Most drivers don't set this but implement only the suspend-stop action. Then the application (or the sound backend) will re-setup the stream and restart accordingly.
And an application must be aware of such possible situation? Since I have no doubt in alsa-lib, but I don't think that for example tinyalsa can handle this right.
Tiny ALSA should work, too. Actually there are only few drivers that have the full PCM resume. The majority of drivers are without the resume support (including a large one like HD-audio).
And, with the resume implementation, I'm worried by the style like:
@@ -309,6 +318,21 @@ static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command) int rc;
switch (command) {
case SNDRV_PCM_TRIGGER_RESUME: {
/*
* We restart the substream by executing the standard command
* sequence.
*/
rc = virtsnd_pcm_hw_params(substream, NULL);
if (rc)
return rc;
rc = virtsnd_pcm_prepare(substream);
if (rc)
return rc;
... and this is rather what the core code should do, and it's exactly the same procedure that would be done without RESUME flag.
Takashi
On 01.03.2021 14:38, Takashi Iwai wrote:
On Mon, 01 Mar 2021 11:03:04 +0100, Anton Yakovlev wrote:
On 28.02.2021 13:05, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:56 +0100, Anton Yakovlev wrote:
All running PCM substreams are stopped on device suspend and restarted on device resume.
Signed-off-by: Anton Yakovlev anton.yakovlev@opensynergy.com
sound/virtio/virtio_card.c | 56 +++++++++++++++++++++++++++++++++++ sound/virtio/virtio_pcm.c | 1 + sound/virtio/virtio_pcm_ops.c | 41 ++++++++++++++++++++----- 3 files changed, 90 insertions(+), 8 deletions(-)
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c index 59455a562018..c7ae8801991d 100644 --- a/sound/virtio/virtio_card.c +++ b/sound/virtio/virtio_card.c @@ -323,6 +323,58 @@ static void virtsnd_remove(struct virtio_device *vdev) kfree(snd->event_msgs); }
+#ifdef CONFIG_PM_SLEEP +/**
- virtsnd_freeze() - Suspend device.
- @vdev: VirtIO parent device.
- Context: Any context.
- Return: 0 on success, -errno on failure.
- */
+static int virtsnd_freeze(struct virtio_device *vdev) +{
struct virtio_snd *snd = vdev->priv;
virtsnd_ctl_msg_cancel_all(snd);
vdev->config->del_vqs(vdev);
vdev->config->reset(vdev);
kfree(snd->event_msgs);
/*
* If the virtsnd_restore() fails before re-allocating events, then we
* get a dangling pointer here.
*/
snd->event_msgs = NULL;
return 0;
I suppose some cancel of inflight works is needed? Ditto for the device removal, too.
It's not necessary here, since the device is reset and all of this are happened automatically.
Hrm, but the reset call itself might conflict with the inflight reset work? I haven't see any work canceling or flushing, so...
There maybe the following:
1. Some pending control requests -> these are cancelled in the virtsnd_ctl_msg_cancel_all() call.
2. PCM messages -> these must not be cancelled, since they will be requeued by driver on resume (starting with suspended position).
3. Some pending events from the device. These will be lost. Yeah, I think we can process all pending events before destroying virtqueue.
Other that these, there are no other inflight works or so.
But in the device remove it makes sense also to disable events before calling snd_card_free(), since the device is still able to send notifications at that moment. Thanks!
--- a/sound/virtio/virtio_pcm.c +++ b/sound/virtio/virtio_pcm.c @@ -109,6 +109,7 @@ static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *vss, SNDRV_PCM_INFO_BATCH | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_RESUME | SNDRV_PCM_INFO_PAUSE;
Actually you don't need to set SNDRV_PCM_INFO_RESUME. This flag means that the driver supports the full resume procedure, which isn't often the case; with this, the driver is supposed to resume the stream exactly from the suspended position.
If I understood you right, that's exactly how resume is implemented now in the driver. Although we fully restart substream on the device side, from an application point of view it is resumed exactly at the same position.
Most drivers don't set this but implement only the suspend-stop action. Then the application (or the sound backend) will re-setup the stream and restart accordingly.
And an application must be aware of such possible situation? Since I have no doubt in alsa-lib, but I don't think that for example tinyalsa can handle this right.
Tiny ALSA should work, too. Actually there are only few drivers that have the full PCM resume. The majority of drivers are without the resume support (including a large one like HD-audio).
Then it's a great news! Since we can simplify code a lot.
And, with the resume implementation, I'm worried by the style like:
@@ -309,6 +318,21 @@ static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command) int rc;
switch (command) {
case SNDRV_PCM_TRIGGER_RESUME: {
/*
* We restart the substream by executing the standard command
* sequence.
*/
rc = virtsnd_pcm_hw_params(substream, NULL);
if (rc)
return rc;
rc = virtsnd_pcm_prepare(substream);
if (rc)
return rc;
... and this is rather what the core code should do, and it's exactly the same procedure that would be done without RESUME flag.
Takashi
On 28.02.2021 13:05, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:56 +0100, Anton Yakovlev wrote:
[snip]
--- a/sound/virtio/virtio_pcm.c +++ b/sound/virtio/virtio_pcm.c @@ -109,6 +109,7 @@ static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *vss, SNDRV_PCM_INFO_BATCH | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_RESUME | SNDRV_PCM_INFO_PAUSE;
Actually you don't need to set SNDRV_PCM_INFO_RESUME. This flag means that the driver supports the full resume procedure, which isn't often the case; with this, the driver is supposed to resume the stream exactly from the suspended position.
Most drivers don't set this but implement only the suspend-stop action. Then the application (or the sound backend) will re-setup the stream and restart accordingly.
I tried to resume driver without SNDRV_PCM_INFO_RESUME, and alsa-lib called only ops->prepare(). It makes sense for a typical hw, but we have "clean" unconfigured device on resume. And we must set hw parameters as a first step. It means, that code should be more or less the same. And maybe it's better to set SNDRV_PCM_INFO_RESUME, since it allows us to resume substream in any situation (regardless of application behavior). I can refactor code to only send requests from trigger(RESUME) path and not to call ops itself. It should make code more straitforward. What do you say?
On Tue, 02 Mar 2021 07:29:20 +0100, Anton Yakovlev wrote:
On 28.02.2021 13:05, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:56 +0100, Anton Yakovlev wrote:
[snip]
--- a/sound/virtio/virtio_pcm.c +++ b/sound/virtio/virtio_pcm.c @@ -109,6 +109,7 @@ static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *vss, SNDRV_PCM_INFO_BATCH | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_RESUME | SNDRV_PCM_INFO_PAUSE;
Actually you don't need to set SNDRV_PCM_INFO_RESUME. This flag means that the driver supports the full resume procedure, which isn't often the case; with this, the driver is supposed to resume the stream exactly from the suspended position.
Most drivers don't set this but implement only the suspend-stop action. Then the application (or the sound backend) will re-setup the stream and restart accordingly.
I tried to resume driver without SNDRV_PCM_INFO_RESUME, and alsa-lib called only ops->prepare(). It makes sense for a typical hw, but we have "clean" unconfigured device on resume. And we must set hw parameters as a first step. It means, that code should be more or less the same. And maybe it's better to set SNDRV_PCM_INFO_RESUME, since it allows us to resume substream in any situation (regardless of application behavior). I can refactor code to only send requests from trigger(RESUME) path and not to call ops itself. It should make code more straitforward. What do you say?
How about calling hw_params(NULL) conditionally in the prepare? Doing the full stack work in the trigger callback is bad from the API design POV; in general the trigger callback is supposed to be as short as possible.
Takashi
On 02.03.2021 07:48, Takashi Iwai wrote:
On Tue, 02 Mar 2021 07:29:20 +0100, Anton Yakovlev wrote:
On 28.02.2021 13:05, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:56 +0100, Anton Yakovlev wrote:
[snip]
--- a/sound/virtio/virtio_pcm.c +++ b/sound/virtio/virtio_pcm.c @@ -109,6 +109,7 @@ static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *vss, SNDRV_PCM_INFO_BATCH | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_RESUME | SNDRV_PCM_INFO_PAUSE;
Actually you don't need to set SNDRV_PCM_INFO_RESUME. This flag means that the driver supports the full resume procedure, which isn't often the case; with this, the driver is supposed to resume the stream exactly from the suspended position.
Most drivers don't set this but implement only the suspend-stop action. Then the application (or the sound backend) will re-setup the stream and restart accordingly.
I tried to resume driver without SNDRV_PCM_INFO_RESUME, and alsa-lib called only ops->prepare(). It makes sense for a typical hw, but we have "clean" unconfigured device on resume. And we must set hw parameters as a first step. It means, that code should be more or less the same. And maybe it's better to set SNDRV_PCM_INFO_RESUME, since it allows us to resume substream in any situation (regardless of application behavior). I can refactor code to only send requests from trigger(RESUME) path and not to call ops itself. It should make code more straitforward. What do you say?
How about calling hw_params(NULL) conditionally in the prepare?
Then the question is that condition. When ops->prepare() is called, the substream is in SUSPENDED state or not? If not then we need to track this in some additional field (and it will make logic a little bit clumsy, since that field is needed to be carefully handled in other places).
Doing the full stack work in the trigger callback is bad from the API design POV; in general the trigger callback is supposed to be as short as possible.
Yeah, but usually original subsystem design does not take into account para-virtualized devices, which usually have it's own slightly different reality. And we need to introduce some tricks.
Takashi
On Tue, 02 Mar 2021 09:09:33 +0100, Anton Yakovlev wrote:
On 02.03.2021 07:48, Takashi Iwai wrote:
On Tue, 02 Mar 2021 07:29:20 +0100, Anton Yakovlev wrote:
On 28.02.2021 13:05, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:56 +0100, Anton Yakovlev wrote:
[snip]
--- a/sound/virtio/virtio_pcm.c +++ b/sound/virtio/virtio_pcm.c @@ -109,6 +109,7 @@ static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *vss, SNDRV_PCM_INFO_BATCH | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_RESUME | SNDRV_PCM_INFO_PAUSE;
Actually you don't need to set SNDRV_PCM_INFO_RESUME. This flag means that the driver supports the full resume procedure, which isn't often the case; with this, the driver is supposed to resume the stream exactly from the suspended position.
Most drivers don't set this but implement only the suspend-stop action. Then the application (or the sound backend) will re-setup the stream and restart accordingly.
I tried to resume driver without SNDRV_PCM_INFO_RESUME, and alsa-lib called only ops->prepare(). It makes sense for a typical hw, but we have "clean" unconfigured device on resume. And we must set hw parameters as a first step. It means, that code should be more or less the same. And maybe it's better to set SNDRV_PCM_INFO_RESUME, since it allows us to resume substream in any situation (regardless of application behavior). I can refactor code to only send requests from trigger(RESUME) path and not to call ops itself. It should make code more straitforward. What do you say?
How about calling hw_params(NULL) conditionally in the prepare?
Then the question is that condition. When ops->prepare() is called, the substream is in SUSPENDED state or not? If not then we need to track this in some additional field (and it will make logic a little bit clumsy, since that field is needed to be carefully handled in other places).
Yes, you'd need to have a suspend/resume PM callback in the driver that flips the internal flag to invalidate the hw_parmas, and in the prepare callback, just call hw_params(NULL) if that flag is set.
Doing the full stack work in the trigger callback is bad from the API design POV; in general the trigger callback is supposed to be as short as possible.
Yeah, but usually original subsystem design does not take into account para-virtualized devices, which usually have it's own slightly different reality. And we need to introduce some tricks.
The hardware drivers do a lot of more things in either suspend/resume PM callbacks or prepare callback for re-setup of the hardware. We can follow the similar pattern. Heavy-lifting works in the trigger callbacks is really something to avoid.
Takashi
On 02.03.2021 10:11, Takashi Iwai wrote:
On Tue, 02 Mar 2021 09:09:33 +0100, Anton Yakovlev wrote:
On 02.03.2021 07:48, Takashi Iwai wrote:
On Tue, 02 Mar 2021 07:29:20 +0100, Anton Yakovlev wrote:
On 28.02.2021 13:05, Takashi Iwai wrote:
On Sat, 27 Feb 2021 09:59:56 +0100, Anton Yakovlev wrote:
[snip]
--- a/sound/virtio/virtio_pcm.c +++ b/sound/virtio/virtio_pcm.c @@ -109,6 +109,7 @@ static int virtsnd_pcm_build_hw(struct virtio_pcm_substream *vss, SNDRV_PCM_INFO_BATCH | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_RESUME | SNDRV_PCM_INFO_PAUSE;
Actually you don't need to set SNDRV_PCM_INFO_RESUME. This flag means that the driver supports the full resume procedure, which isn't often the case; with this, the driver is supposed to resume the stream exactly from the suspended position.
Most drivers don't set this but implement only the suspend-stop action. Then the application (or the sound backend) will re-setup the stream and restart accordingly.
I tried to resume driver without SNDRV_PCM_INFO_RESUME, and alsa-lib called only ops->prepare(). It makes sense for a typical hw, but we have "clean" unconfigured device on resume. And we must set hw parameters as a first step. It means, that code should be more or less the same. And maybe it's better to set SNDRV_PCM_INFO_RESUME, since it allows us to resume substream in any situation (regardless of application behavior). I can refactor code to only send requests from trigger(RESUME) path and not to call ops itself. It should make code more straitforward. What do you say?
How about calling hw_params(NULL) conditionally in the prepare?
Then the question is that condition. When ops->prepare() is called, the substream is in SUSPENDED state or not? If not then we need to track this in some additional field (and it will make logic a little bit clumsy, since that field is needed to be carefully handled in other places).
Yes, you'd need to have a suspend/resume PM callback in the driver that flips the internal flag to invalidate the hw_parmas, and in the prepare callback, just call hw_params(NULL) if that flag is set.
Doing the full stack work in the trigger callback is bad from the API design POV; in general the trigger callback is supposed to be as short as possible.
Yeah, but usually original subsystem design does not take into account para-virtualized devices, which usually have it's own slightly different reality. And we need to introduce some tricks.
The hardware drivers do a lot of more things in either suspend/resume PM callbacks or prepare callback for re-setup of the hardware. We can follow the similar pattern. Heavy-lifting works in the trigger callbacks is really something to avoid.
Ok, I redone this part and now the driver sets parameters for the device in ops->prepare() if the substream was suspended. And everything works fine. Thanks! I will send a new patch set soon.
Takashi
participants (2)
-
Anton Yakovlev
-
Takashi Iwai