mirror of
https://github.com/hardkernel/linux.git
synced 2026-06-11 05:17:10 +09:00
ASoC: rockchip: Adds support DLP for Multi-DAIs
This patch add support DMA-based digital loopback for
Multi-DAIs.
Ref: commit 9975bc50f3 ("ASoC: rockchip: Add support for Digital Loopback")
Signed-off-by: Sugar Zhang <sugar.zhang@rock-chips.com>
Change-Id: I2861632eb28b4c5f596cfa5af855676c6140d1e1
This commit is contained in:
@@ -47,6 +47,7 @@ config SND_SOC_ROCKCHIP_I2S_TDM_MULTI_LANES
|
||||
config SND_SOC_ROCKCHIP_MULTI_DAIS
|
||||
tristate "Rockchip Multi-DAIS Device Driver"
|
||||
depends on HAVE_CLK && SND_SOC_ROCKCHIP
|
||||
select SND_SOC_ROCKCHIP_DLP
|
||||
help
|
||||
Say Y or M if you want to add support for Multi-dais driver for
|
||||
Rockchip.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* ALSA SoC Audio Layer - Rockchip Multi-DAIS-PCM driver
|
||||
*
|
||||
@@ -16,22 +16,34 @@
|
||||
#include <sound/soc.h>
|
||||
|
||||
#include "rockchip_multi_dais.h"
|
||||
#include "rockchip_dlp.h"
|
||||
|
||||
#define MAX_FIFO_SIZE 32 /* max fifo size in frames */
|
||||
#define SND_DMAENGINE_MPCM_DRV_NAME "snd_dmaengine_mpcm"
|
||||
#define I2S_TXFIFOLR 0xc
|
||||
#define I2S_RXFIFOLR 0x2c
|
||||
|
||||
/* XFL4 is compatible for old version */
|
||||
#define I2S_FIFOLR_XFL4(v) (((v) & GENMASK(29, 24)) >> 24)
|
||||
#define I2S_FIFOLR_XFL3(v) (((v) & GENMASK(23, 18)) >> 18)
|
||||
#define I2S_FIFOLR_XFL2(v) (((v) & GENMASK(17, 12)) >> 12)
|
||||
#define I2S_FIFOLR_XFL1(v) (((v) & GENMASK(11, 6)) >> 6)
|
||||
#define I2S_FIFOLR_XFL0(v) (((v) & GENMASK(5, 0)) >> 0)
|
||||
|
||||
#define MAX_FIFO_SIZE 32 /* max fifo size in frames */
|
||||
#define SND_DMAENGINE_MPCM_DRV_NAME "snd_dmaengine_mpcm"
|
||||
|
||||
static unsigned int prealloc_buffer_size_kbytes = 512;
|
||||
module_param(prealloc_buffer_size_kbytes, uint, 0444);
|
||||
MODULE_PARM_DESC(prealloc_buffer_size_kbytes, "Preallocate DMA buffer size (KB).");
|
||||
|
||||
struct dmaengine_mpcm {
|
||||
struct dlp dlp;
|
||||
struct rk_mdais_dev *mdais;
|
||||
struct dma_chan *tx_chans[MAX_DAIS];
|
||||
struct dma_chan *rx_chans[MAX_DAIS];
|
||||
struct snd_soc_component component;
|
||||
};
|
||||
|
||||
struct dmaengine_mpcm_runtime_data {
|
||||
struct dlp_runtime_data drd;
|
||||
struct dma_chan *chans[MAX_DAIS];
|
||||
struct dma_interleaved_template *xt;
|
||||
dma_cookie_t cookies[MAX_DAIS];
|
||||
@@ -49,12 +61,17 @@ struct dmaengine_mpcm_runtime_data {
|
||||
static inline struct dmaengine_mpcm_runtime_data *substream_to_prtd(
|
||||
const struct snd_pcm_substream *substream)
|
||||
{
|
||||
return substream->runtime->private_data;
|
||||
struct dlp_runtime_data *drd = substream_to_drd(substream);
|
||||
|
||||
if (!drd)
|
||||
return NULL;
|
||||
|
||||
return container_of(drd, struct dmaengine_mpcm_runtime_data, drd);
|
||||
}
|
||||
|
||||
static struct dmaengine_mpcm *soc_component_to_mpcm(struct snd_soc_component *p)
|
||||
{
|
||||
return container_of(p, struct dmaengine_mpcm, component);
|
||||
return container_of(soc_component_to_dlp(p), struct dmaengine_mpcm, dlp);
|
||||
}
|
||||
|
||||
static struct dma_chan *to_chan(struct dmaengine_mpcm *pcm,
|
||||
@@ -104,7 +121,20 @@ static void dmaengine_mpcm_dma_complete(void *arg)
|
||||
{
|
||||
struct snd_pcm_substream *substream = arg;
|
||||
#ifdef CONFIG_SND_SOC_ROCKCHIP_VAD
|
||||
struct dmaengine_mpcm_runtime_data *prtd = substream_to_prtd(substream);
|
||||
struct dmaengine_mpcm_runtime_data *prtd;
|
||||
#endif
|
||||
struct dlp_runtime_data *drd;
|
||||
struct dlp *dlp;
|
||||
|
||||
snd_pcm_stream_lock_irq(substream);
|
||||
if (!substream->runtime) {
|
||||
snd_pcm_stream_unlock_irq(substream);
|
||||
return;
|
||||
}
|
||||
#ifdef CONFIG_SND_SOC_ROCKCHIP_VAD
|
||||
prtd = substream_to_prtd(substream);
|
||||
if (unlikely(!prtd))
|
||||
return;
|
||||
|
||||
if (snd_pcm_vad_attached(substream) &&
|
||||
substream->stream == SNDRV_PCM_STREAM_CAPTURE) {
|
||||
@@ -119,6 +149,12 @@ static void dmaengine_mpcm_dma_complete(void *arg)
|
||||
prtd->pos = 0;
|
||||
|
||||
#endif
|
||||
drd = substream_to_drd(substream);
|
||||
dlp = drd->parent;
|
||||
|
||||
dlp_dma_complete(dlp, drd);
|
||||
snd_pcm_stream_unlock_irq(substream);
|
||||
|
||||
snd_pcm_period_elapsed(substream);
|
||||
}
|
||||
|
||||
@@ -126,6 +162,9 @@ static void dmaengine_mpcm_get_master_chan(struct dmaengine_mpcm_runtime_data *p
|
||||
{
|
||||
int i;
|
||||
|
||||
if (unlikely(!prtd))
|
||||
return;
|
||||
|
||||
for (i = prtd->num_chans; i > 0; i--) {
|
||||
if (prtd->chans[i - 1]) {
|
||||
prtd->master_chan = i - 1;
|
||||
@@ -176,18 +215,23 @@ static int dmaengine_mpcm_prepare_and_submit(struct snd_pcm_substream *substream
|
||||
struct dmaengine_mpcm_runtime_data *prtd = substream_to_prtd(substream);
|
||||
struct snd_pcm_runtime *runtime = substream->runtime;
|
||||
struct dma_async_tx_descriptor *desc = NULL;
|
||||
struct dma_interleaved_template *xt = prtd->xt;
|
||||
struct dma_interleaved_template *xt;
|
||||
unsigned long flags = DMA_CTRL_ACK;
|
||||
unsigned int *maps = prtd->channel_maps;
|
||||
unsigned int *maps;
|
||||
int offset;
|
||||
int i;
|
||||
|
||||
if (unlikely(!prtd || !runtime))
|
||||
return -EINVAL;
|
||||
|
||||
if (!substream->runtime->no_period_wakeup)
|
||||
flags |= DMA_PREP_INTERRUPT;
|
||||
|
||||
prtd->pos = 0;
|
||||
offset = 0;
|
||||
|
||||
xt = prtd->xt;
|
||||
maps = prtd->channel_maps;
|
||||
for (i = 0; i < prtd->num_chans; i++) {
|
||||
if (!prtd->chans[i])
|
||||
continue;
|
||||
@@ -222,6 +266,9 @@ static void mpcm_dma_async_issue_pending(struct dmaengine_mpcm_runtime_data *prt
|
||||
{
|
||||
int i;
|
||||
|
||||
if (unlikely(!prtd))
|
||||
return;
|
||||
|
||||
for (i = 0; i < prtd->num_chans; i++) {
|
||||
if (prtd->chans[i])
|
||||
dma_async_issue_pending(prtd->chans[i]);
|
||||
@@ -232,6 +279,9 @@ static void mpcm_dmaengine_resume(struct dmaengine_mpcm_runtime_data *prtd)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (unlikely(!prtd))
|
||||
return;
|
||||
|
||||
for (i = 0; i < prtd->num_chans; i++) {
|
||||
if (prtd->chans[i])
|
||||
dmaengine_resume(prtd->chans[i]);
|
||||
@@ -242,6 +292,9 @@ static void mpcm_dmaengine_pause(struct dmaengine_mpcm_runtime_data *prtd)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (unlikely(!prtd))
|
||||
return;
|
||||
|
||||
for (i = 0; i < prtd->num_chans; i++) {
|
||||
if (prtd->chans[i])
|
||||
dmaengine_pause(prtd->chans[i]);
|
||||
@@ -252,6 +305,9 @@ static void mpcm_dmaengine_terminate_all(struct dmaengine_mpcm_runtime_data *prt
|
||||
{
|
||||
int i;
|
||||
|
||||
if (unlikely(!prtd))
|
||||
return;
|
||||
|
||||
for (i = 0; i < prtd->num_chans; i++) {
|
||||
if (prtd->chans[i])
|
||||
dmaengine_terminate_all(prtd->chans[i]);
|
||||
@@ -266,6 +322,9 @@ static void dmaengine_mpcm_single_dma_complete(void *arg)
|
||||
unsigned int pos, size;
|
||||
void *buf;
|
||||
|
||||
if (unlikely(!prtd))
|
||||
return;
|
||||
|
||||
if (snd_pcm_vad_attached(substream) &&
|
||||
substream->stream == SNDRV_PCM_STREAM_CAPTURE) {
|
||||
buf = substream->runtime->dma_area + prtd->vpos;
|
||||
@@ -289,18 +348,23 @@ static int __mpcm_prepare_single_and_submit(struct snd_pcm_substream *substream,
|
||||
int buf_offset, int size)
|
||||
{
|
||||
struct dmaengine_mpcm_runtime_data *prtd = substream_to_prtd(substream);
|
||||
struct dma_interleaved_template *xt = prtd->xt;
|
||||
struct dma_interleaved_template *xt;
|
||||
struct snd_pcm_runtime *runtime = substream->runtime;
|
||||
struct dma_async_tx_descriptor *desc;
|
||||
unsigned long flags = DMA_CTRL_ACK;
|
||||
unsigned int *maps = prtd->channel_maps;
|
||||
unsigned int *maps;
|
||||
int offset, i;
|
||||
bool callback = false;
|
||||
|
||||
if (unlikely(!prtd || !runtime))
|
||||
return -EINVAL;
|
||||
|
||||
if (!substream->runtime->no_period_wakeup)
|
||||
flags |= DMA_PREP_INTERRUPT;
|
||||
|
||||
offset = buf_offset;
|
||||
xt = prtd->xt;
|
||||
maps = prtd->channel_maps;
|
||||
for (i = 0; i < prtd->num_chans; i++) {
|
||||
if (!prtd->chans[i])
|
||||
continue;
|
||||
@@ -336,6 +400,9 @@ static int dmaengine_mpcm_prepare_single_and_submit(struct snd_pcm_substream *su
|
||||
int offset, i, count, ret;
|
||||
int buffer_bytes, period_bytes, residue_bytes;
|
||||
|
||||
if (unlikely(!prtd))
|
||||
return -EINVAL;
|
||||
|
||||
direction = snd_pcm_substream_to_dma_direction(substream);
|
||||
|
||||
if (!substream->runtime->no_period_wakeup)
|
||||
@@ -372,13 +439,51 @@ static int dmaengine_mpcm_prepare_single_and_submit(struct snd_pcm_substream *su
|
||||
}
|
||||
#endif
|
||||
|
||||
static int snd_dmaengine_mpcm_trigger(struct snd_soc_component *component,
|
||||
struct snd_pcm_substream *substream, int cmd)
|
||||
static snd_pcm_uframes_t dmaengine_mpcm_raw_pointer(struct snd_soc_component *component,
|
||||
struct snd_pcm_substream *substream)
|
||||
{
|
||||
struct dmaengine_mpcm_runtime_data *prtd = substream_to_prtd(substream);
|
||||
struct dma_tx_state state;
|
||||
unsigned int buf_size;
|
||||
unsigned int pos = 0;
|
||||
unsigned int master;
|
||||
|
||||
if (unlikely(!prtd))
|
||||
return 0;
|
||||
|
||||
master = prtd->master_chan;
|
||||
buf_size = snd_pcm_lib_buffer_bytes(substream);
|
||||
dmaengine_tx_status(prtd->chans[master], prtd->cookies[master], &state);
|
||||
if (state.residue > 0 && state.residue <= buf_size)
|
||||
pos = buf_size - state.residue;
|
||||
|
||||
return bytes_to_frames(substream->runtime, pos);
|
||||
}
|
||||
|
||||
static int dmaengine_mpcm_dlp_start(struct snd_soc_component *component,
|
||||
struct snd_pcm_substream *substream)
|
||||
{
|
||||
struct dmaengine_mpcm *pcm = soc_component_to_mpcm(component);
|
||||
|
||||
return dlp_start(component, substream, pcm->mdais->dev, dmaengine_mpcm_raw_pointer);
|
||||
}
|
||||
|
||||
static void dmaengine_mpcm_dlp_stop(struct snd_soc_component *component,
|
||||
struct snd_pcm_substream *substream)
|
||||
{
|
||||
dlp_stop(component, substream, dmaengine_mpcm_raw_pointer);
|
||||
}
|
||||
|
||||
static int dmaengine_mpcm_trigger(struct snd_soc_component *component,
|
||||
struct snd_pcm_substream *substream, int cmd)
|
||||
{
|
||||
struct dmaengine_mpcm_runtime_data *prtd = substream_to_prtd(substream);
|
||||
struct snd_pcm_runtime *runtime = substream->runtime;
|
||||
int ret;
|
||||
|
||||
if (unlikely(!prtd || !runtime))
|
||||
return -EINVAL;
|
||||
|
||||
switch (cmd) {
|
||||
case SNDRV_PCM_TRIGGER_START:
|
||||
#ifdef CONFIG_SND_SOC_ROCKCHIP_VAD
|
||||
@@ -393,16 +498,19 @@ static int snd_dmaengine_mpcm_trigger(struct snd_soc_component *component,
|
||||
if (ret)
|
||||
return ret;
|
||||
mpcm_dma_async_issue_pending(prtd);
|
||||
dmaengine_mpcm_dlp_start(component, substream);
|
||||
break;
|
||||
case SNDRV_PCM_TRIGGER_RESUME:
|
||||
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
|
||||
mpcm_dmaengine_resume(prtd);
|
||||
break;
|
||||
case SNDRV_PCM_TRIGGER_SUSPEND:
|
||||
if (runtime->info & SNDRV_PCM_INFO_PAUSE)
|
||||
if (runtime->info & SNDRV_PCM_INFO_PAUSE) {
|
||||
mpcm_dmaengine_pause(prtd);
|
||||
else
|
||||
} else {
|
||||
dmaengine_mpcm_dlp_stop(component, substream);
|
||||
mpcm_dmaengine_terminate_all(prtd);
|
||||
}
|
||||
prtd->start_flag = false;
|
||||
break;
|
||||
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
|
||||
@@ -410,6 +518,7 @@ static int snd_dmaengine_mpcm_trigger(struct snd_soc_component *component,
|
||||
prtd->start_flag = false;
|
||||
break;
|
||||
case SNDRV_PCM_TRIGGER_STOP:
|
||||
dmaengine_mpcm_dlp_stop(component, substream);
|
||||
mpcm_dmaengine_terminate_all(prtd);
|
||||
prtd->start_flag = false;
|
||||
break;
|
||||
@@ -475,6 +584,11 @@ static int dmaengine_mpcm_hw_params(struct snd_soc_component *component,
|
||||
if (ret)
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = dlp_hw_params(component, substream, params);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
return snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(params));
|
||||
}
|
||||
|
||||
@@ -500,7 +614,7 @@ static int dmaengine_mpcm_set_runtime_hwparams(struct snd_pcm_substream *substre
|
||||
|
||||
memset(&hw, 0, sizeof(hw));
|
||||
hw.info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID |
|
||||
SNDRV_PCM_INFO_INTERLEAVED;
|
||||
SNDRV_PCM_INFO_INTERLEAVED;
|
||||
hw.periods_min = 2;
|
||||
hw.periods_max = UINT_MAX;
|
||||
hw.period_bytes_min = 256;
|
||||
@@ -589,7 +703,8 @@ static int dmaengine_mpcm_open(struct snd_soc_component *component,
|
||||
|
||||
prtd->num_chans = pcm->mdais->num_dais;
|
||||
prtd->start_flag = false;
|
||||
substream->runtime->private_data = prtd;
|
||||
|
||||
dlp_open(&pcm->dlp, &prtd->drd, substream);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -629,8 +744,12 @@ static snd_pcm_uframes_t dmaengine_mpcm_pointer(struct snd_soc_component *compon
|
||||
snd_pcm_uframes_t frames;
|
||||
unsigned int buf_size;
|
||||
unsigned int pos = 0;
|
||||
unsigned int master = prtd->master_chan;
|
||||
unsigned int master;
|
||||
|
||||
if (unlikely(!prtd || !runtime))
|
||||
return 0;
|
||||
|
||||
master = prtd->master_chan;
|
||||
buf_size = snd_pcm_lib_buffer_bytes(substream);
|
||||
dmaengine_tx_status(prtd->chans[master], prtd->cookies[master], &state);
|
||||
if (state.residue > 0 && state.residue <= buf_size)
|
||||
@@ -675,25 +794,54 @@ static int dmaengine_mpcm_hw_free(struct snd_soc_component *component,
|
||||
static int dmaengine_mpcm_close(struct snd_soc_component *component,
|
||||
struct snd_pcm_substream *substream)
|
||||
{
|
||||
struct dmaengine_mpcm *pcm = soc_component_to_mpcm(component);
|
||||
struct dmaengine_mpcm_runtime_data *prtd = substream_to_prtd(substream);
|
||||
|
||||
if (unlikely(!prtd))
|
||||
return -EINVAL;
|
||||
|
||||
dlp_close(&pcm->dlp, &prtd->drd, substream);
|
||||
|
||||
kfree(prtd->xt);
|
||||
kfree(prtd);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dmaengine_mpcm_copy_user(struct snd_soc_component *component,
|
||||
struct snd_pcm_substream *substream,
|
||||
int channel, unsigned long hwoff,
|
||||
void __user *buf, unsigned long bytes)
|
||||
{
|
||||
return dlp_copy_user(component, substream, channel, hwoff, buf, bytes);
|
||||
}
|
||||
|
||||
|
||||
static int dmaengine_mpcm_prepare(struct snd_soc_component *component,
|
||||
struct snd_pcm_substream *substream)
|
||||
{
|
||||
return dlp_prepare(component, substream);
|
||||
}
|
||||
|
||||
static int dmaengine_mpcm_probe(struct snd_soc_component *component)
|
||||
{
|
||||
return dlp_probe(component);
|
||||
}
|
||||
|
||||
static const struct snd_soc_component_driver dmaengine_mpcm_platform = {
|
||||
.name = SND_DMAENGINE_MPCM_DRV_NAME,
|
||||
.probe_order = SND_SOC_COMP_ORDER_LATE,
|
||||
.probe = dmaengine_mpcm_probe,
|
||||
.pcm_construct = dmaengine_mpcm_new,
|
||||
.open = dmaengine_mpcm_open,
|
||||
.close = dmaengine_mpcm_close,
|
||||
.ioctl = dmaengine_mpcm_ioctl,
|
||||
.hw_params = dmaengine_mpcm_hw_params,
|
||||
.hw_free = dmaengine_mpcm_hw_free,
|
||||
.trigger = snd_dmaengine_mpcm_trigger,
|
||||
.prepare = dmaengine_mpcm_prepare,
|
||||
.trigger = dmaengine_mpcm_trigger,
|
||||
.pointer = dmaengine_mpcm_pointer,
|
||||
.copy_user = dmaengine_mpcm_copy_user,
|
||||
};
|
||||
|
||||
static void dmaengine_mpcm_release_chan(struct dmaengine_mpcm *pcm)
|
||||
@@ -708,6 +856,48 @@ static void dmaengine_mpcm_release_chan(struct dmaengine_mpcm *pcm)
|
||||
}
|
||||
}
|
||||
|
||||
static int dmaengine_mpcm_get_fifo_count(struct device *dev,
|
||||
struct snd_pcm_substream *substream)
|
||||
{
|
||||
struct rk_mdais_dev *mdais = dev_get_drvdata(dev);
|
||||
struct dmaengine_mpcm_runtime_data *prtd = substream_to_prtd(substream);
|
||||
struct snd_soc_component *component;
|
||||
unsigned int tx, rx;
|
||||
int val = 0;
|
||||
|
||||
if (unlikely(!prtd))
|
||||
return -EINVAL;
|
||||
|
||||
component = mdais->dais[prtd->master_chan].dai->component;
|
||||
if (unlikely(!component))
|
||||
return -EINVAL;
|
||||
|
||||
if (strstr(dev_driver_string(component->dev), "i2s")) {
|
||||
/* compatible for both I2S and I2STDM controller */
|
||||
tx = snd_soc_component_read(component, I2S_TXFIFOLR);
|
||||
rx = snd_soc_component_read(component, I2S_RXFIFOLR);
|
||||
|
||||
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
|
||||
val = I2S_FIFOLR_XFL3(tx) +
|
||||
I2S_FIFOLR_XFL2(tx) +
|
||||
I2S_FIFOLR_XFL1(tx) +
|
||||
I2S_FIFOLR_XFL0(tx);
|
||||
else
|
||||
/* XFL4 is compatible for old version */
|
||||
val = I2S_FIFOLR_XFL4(tx) +
|
||||
I2S_FIFOLR_XFL3(rx) +
|
||||
I2S_FIFOLR_XFL2(rx) +
|
||||
I2S_FIFOLR_XFL1(rx) +
|
||||
I2S_FIFOLR_XFL0(rx);
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
static const struct snd_dlp_config dconfig = {
|
||||
.get_fifo_count = dmaengine_mpcm_get_fifo_count,
|
||||
};
|
||||
|
||||
int snd_dmaengine_mpcm_register(struct rk_mdais_dev *mdais)
|
||||
{
|
||||
struct device *dev;
|
||||
@@ -725,9 +915,6 @@ int snd_dmaengine_mpcm_register(struct rk_mdais_dev *mdais)
|
||||
if (!pcm)
|
||||
return -ENOMEM;
|
||||
|
||||
#ifdef CONFIG_DEBUG_FS
|
||||
pcm->component.debugfs_prefix = "dma";
|
||||
#endif
|
||||
pcm->mdais = mdais;
|
||||
for (i = 0; i < num; i++) {
|
||||
child = mdais->dais[i].dev;
|
||||
@@ -746,12 +933,7 @@ int snd_dmaengine_mpcm_register(struct rk_mdais_dev *mdais)
|
||||
}
|
||||
}
|
||||
|
||||
ret = snd_soc_component_initialize(&pcm->component, &dmaengine_mpcm_platform,
|
||||
dev);
|
||||
if (ret)
|
||||
goto err_free_dma;
|
||||
|
||||
ret = snd_soc_add_component(&pcm->component, NULL, 0);
|
||||
ret = dlp_register(&pcm->dlp, dev, &dmaengine_mpcm_platform, &dconfig);
|
||||
if (ret)
|
||||
goto err_free_dma;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user