Commit b9791ca0 authored by Takashi Iwai's avatar Takashi Iwai

Merge branch 'topic/hda-patch' into topic/hda

parents 546861f1 1e7b8c87
...@@ -768,6 +768,10 @@ Prior to version 0.9.0rc4 options had a 'snd_' prefix. This was removed. ...@@ -768,6 +768,10 @@ Prior to version 0.9.0rc4 options had a 'snd_' prefix. This was removed.
bdl_pos_adj - Specifies the DMA IRQ timing delay in samples. bdl_pos_adj - Specifies the DMA IRQ timing delay in samples.
Passing -1 will make the driver to choose the appropriate Passing -1 will make the driver to choose the appropriate
value based on the controller chip. value based on the controller chip.
patch - Specifies the early "patch" files to modify the HD-audio
setup before initializing the codecs. This option is
available only when CONFIG_SND_HDA_PATCH_LOADER=y is set.
See HD-Audio.txt for details.
[Single (global) options] [Single (global) options]
single_cmd - Use single immediate commands to communicate with single_cmd - Use single immediate commands to communicate with
......
...@@ -403,6 +403,66 @@ re-configure based on that state, run like below: ...@@ -403,6 +403,66 @@ re-configure based on that state, run like below:
------------------------------------------------------------------------ ------------------------------------------------------------------------
Early Patching
~~~~~~~~~~~~~~
When CONFIG_SND_HDA_PATCH_LOADER=y is set, you can pass a "patch" as a
firmware file for modifying the HD-audio setup before initializing the
codec. This can work basically like the reconfiguration via sysfs in
the above, but it does it before the first codec configuration.
A patch file is a plain text file which looks like below:
------------------------------------------------------------------------
[codec]
0x12345678 0xabcd1234 2
[model]
auto
[pincfg]
0x12 0x411111f0
[verb]
0x20 0x500 0x03
0x20 0x400 0xff
[hint]
hp_detect = yes
------------------------------------------------------------------------
The file needs to have a line `[codec]`. The next line should contain
three numbers indicating the codec vendor-id (0x12345678 in the
example), the codec subsystem-id (0xabcd1234) and the address (2) of
the codec. The rest patch entries are applied to this specified codec
until another codec entry is given.
The `[model]` line allows to change the model name of the each codec.
In the example above, it will be changed to model=auto.
Note that this overrides the module option.
After the `[pincfg]` line, the contents are parsed as the initial
default pin-configurations just like `user_pin_configs` sysfs above.
The values can be shown in user_pin_configs sysfs file, too.
Similarly, the lines after `[verb]` are parsed as `init_verbs`
sysfs entries, and the lines after `[hint]` are parsed as `hints`
sysfs entries, respectively.
The hd-audio driver reads the file via request_firmware(). Thus,
a patch file has to be located on the appropriate firmware path,
typically, /lib/firmware. For example, when you pass the option
`patch=hda-init.fw`, the file /lib/firmware/hda-init-fw must be
present.
The patch module option is specific to each card instance, and you
need to give one file name for each instance, separated by commas.
For example, if you have two cards, one for an on-board analog and one
for an HDMI video board, you may pass patch option like below:
------------------------------------------------------------------------
options snd-hda-intel patch=on-board-patch,hdmi-patch
------------------------------------------------------------------------
Power-Saving Power-Saving
~~~~~~~~~~~~ ~~~~~~~~~~~~
The power-saving is a kind of auto-suspend of the device. When the The power-saving is a kind of auto-suspend of the device. When the
......
...@@ -46,6 +46,20 @@ config SND_HDA_INPUT_JACK ...@@ -46,6 +46,20 @@ config SND_HDA_INPUT_JACK
Say Y here to enable the jack plugging notification via Say Y here to enable the jack plugging notification via
input layer. input layer.
config SND_HDA_PATCH_LOADER
bool "Support initialization patch loading for HD-audio"
depends on EXPERIMENTAL
select FW_LOADER
select SND_HDA_HWDEP
select SND_HDA_RECONFIG
help
Say Y here to allow the HD-audio driver to load a pseudo
firmware file ("patch") for overriding the BIOS setup at
start up. The "patch" file can be specified via patch module
option, such as patch=hda-init.
This option turns on hwdep and reconfig features automatically.
config SND_HDA_CODEC_REALTEK config SND_HDA_CODEC_REALTEK
bool "Build Realtek HD-audio codec support" bool "Build Realtek HD-audio codec support"
default y default y
......
...@@ -885,7 +885,7 @@ static void hda_set_power_state(struct hda_codec *codec, hda_nid_t fg, ...@@ -885,7 +885,7 @@ static void hda_set_power_state(struct hda_codec *codec, hda_nid_t fg,
* Returns 0 if successful, or a negative error code. * Returns 0 if successful, or a negative error code.
*/ */
int /*__devinit*/ snd_hda_codec_new(struct hda_bus *bus, unsigned int codec_addr, int /*__devinit*/ snd_hda_codec_new(struct hda_bus *bus, unsigned int codec_addr,
int do_init, struct hda_codec **codecp) struct hda_codec **codecp)
{ {
struct hda_codec *codec; struct hda_codec *codec;
char component[31]; char component[31];
...@@ -978,11 +978,6 @@ int /*__devinit*/ snd_hda_codec_new(struct hda_bus *bus, unsigned int codec_addr ...@@ -978,11 +978,6 @@ int /*__devinit*/ snd_hda_codec_new(struct hda_bus *bus, unsigned int codec_addr
codec->afg ? codec->afg : codec->mfg, codec->afg ? codec->afg : codec->mfg,
AC_PWRST_D0); AC_PWRST_D0);
if (do_init) {
err = snd_hda_codec_configure(codec);
if (err < 0)
goto error;
}
snd_hda_codec_proc_new(codec); snd_hda_codec_proc_new(codec);
snd_hda_create_hwdep(codec); snd_hda_create_hwdep(codec);
...@@ -1036,6 +1031,7 @@ int snd_hda_codec_configure(struct hda_codec *codec) ...@@ -1036,6 +1031,7 @@ int snd_hda_codec_configure(struct hda_codec *codec)
err = init_unsol_queue(codec->bus); err = init_unsol_queue(codec->bus);
return err; return err;
} }
EXPORT_SYMBOL_HDA(snd_hda_codec_configure);
/** /**
* snd_hda_codec_setup_stream - set up the codec for streaming * snd_hda_codec_setup_stream - set up the codec for streaming
......
...@@ -830,7 +830,8 @@ enum { ...@@ -830,7 +830,8 @@ enum {
int snd_hda_bus_new(struct snd_card *card, const struct hda_bus_template *temp, int snd_hda_bus_new(struct snd_card *card, const struct hda_bus_template *temp,
struct hda_bus **busp); struct hda_bus **busp);
int snd_hda_codec_new(struct hda_bus *bus, unsigned int codec_addr, int snd_hda_codec_new(struct hda_bus *bus, unsigned int codec_addr,
int do_init, struct hda_codec **codecp); struct hda_codec **codecp);
int snd_hda_codec_configure(struct hda_codec *codec);
/* /*
* low level functions * low level functions
...@@ -938,6 +939,13 @@ static inline void snd_hda_power_down(struct hda_codec *codec) {} ...@@ -938,6 +939,13 @@ static inline void snd_hda_power_down(struct hda_codec *codec) {}
#define snd_hda_codec_needs_resume(codec) 1 #define snd_hda_codec_needs_resume(codec) 1
#endif #endif
#ifdef CONFIG_SND_HDA_PATCH_LOADER
/*
* patch firmware
*/
int snd_hda_load_patch(struct hda_bus *bus, const char *patch);
#endif
/* /*
* Codec modularization * Codec modularization
*/ */
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
#include <linux/compat.h> #include <linux/compat.h>
#include <linux/mutex.h> #include <linux/mutex.h>
#include <linux/ctype.h> #include <linux/ctype.h>
#include <linux/firmware.h>
#include <sound/core.h> #include <sound/core.h>
#include "hda_codec.h" #include "hda_codec.h"
#include "hda_local.h" #include "hda_local.h"
...@@ -312,12 +313,8 @@ static ssize_t init_verbs_show(struct device *dev, ...@@ -312,12 +313,8 @@ static ssize_t init_verbs_show(struct device *dev,
return len; return len;
} }
static ssize_t init_verbs_store(struct device *dev, static int parse_init_verbs(struct hda_codec *codec, const char *buf)
struct device_attribute *attr,
const char *buf, size_t count)
{ {
struct snd_hwdep *hwdep = dev_get_drvdata(dev);
struct hda_codec *codec = hwdep->private_data;
struct hda_verb *v; struct hda_verb *v;
int nid, verb, param; int nid, verb, param;
...@@ -331,6 +328,18 @@ static ssize_t init_verbs_store(struct device *dev, ...@@ -331,6 +328,18 @@ static ssize_t init_verbs_store(struct device *dev,
v->nid = nid; v->nid = nid;
v->verb = verb; v->verb = verb;
v->param = param; v->param = param;
return 0;
}
static ssize_t init_verbs_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct snd_hwdep *hwdep = dev_get_drvdata(dev);
struct hda_codec *codec = hwdep->private_data;
int err = parse_init_verbs(codec, buf);
if (err < 0)
return err;
return count; return count;
} }
...@@ -376,19 +385,15 @@ static void remove_trail_spaces(char *str) ...@@ -376,19 +385,15 @@ static void remove_trail_spaces(char *str)
#define MAX_HINTS 1024 #define MAX_HINTS 1024
static ssize_t hints_store(struct device *dev, static int parse_hints(struct hda_codec *codec, const char *buf)
struct device_attribute *attr,
const char *buf, size_t count)
{ {
struct snd_hwdep *hwdep = dev_get_drvdata(dev);
struct hda_codec *codec = hwdep->private_data;
char *key, *val; char *key, *val;
struct hda_hint *hint; struct hda_hint *hint;
while (isspace(*buf)) while (isspace(*buf))
buf++; buf++;
if (!*buf || *buf == '#' || *buf == '\n') if (!*buf || *buf == '#' || *buf == '\n')
return count; return 0;
if (*buf == '=') if (*buf == '=')
return -EINVAL; return -EINVAL;
key = kstrndup_noeol(buf, 1024); key = kstrndup_noeol(buf, 1024);
...@@ -411,7 +416,7 @@ static ssize_t hints_store(struct device *dev, ...@@ -411,7 +416,7 @@ static ssize_t hints_store(struct device *dev,
kfree(hint->key); kfree(hint->key);
hint->key = key; hint->key = key;
hint->val = val; hint->val = val;
return count; return 0;
} }
/* allocate a new hint entry */ /* allocate a new hint entry */
if (codec->hints.used >= MAX_HINTS) if (codec->hints.used >= MAX_HINTS)
...@@ -424,6 +429,18 @@ static ssize_t hints_store(struct device *dev, ...@@ -424,6 +429,18 @@ static ssize_t hints_store(struct device *dev,
} }
hint->key = key; hint->key = key;
hint->val = val; hint->val = val;
return 0;
}
static ssize_t hints_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct snd_hwdep *hwdep = dev_get_drvdata(dev);
struct hda_codec *codec = hwdep->private_data;
int err = parse_hints(codec, buf);
if (err < 0)
return err;
return count; return count;
} }
...@@ -469,20 +486,24 @@ static ssize_t driver_pin_configs_show(struct device *dev, ...@@ -469,20 +486,24 @@ static ssize_t driver_pin_configs_show(struct device *dev,
#define MAX_PIN_CONFIGS 32 #define MAX_PIN_CONFIGS 32
static ssize_t user_pin_configs_store(struct device *dev, static int parse_user_pin_configs(struct hda_codec *codec, const char *buf)
struct device_attribute *attr,
const char *buf, size_t count)
{ {
struct snd_hwdep *hwdep = dev_get_drvdata(dev);
struct hda_codec *codec = hwdep->private_data;
int nid, cfg; int nid, cfg;
int err;
if (sscanf(buf, "%i %i", &nid, &cfg) != 2) if (sscanf(buf, "%i %i", &nid, &cfg) != 2)
return -EINVAL; return -EINVAL;
if (!nid) if (!nid)
return -EINVAL; return -EINVAL;
err = snd_hda_add_pincfg(codec, &codec->user_pins, nid, cfg); return snd_hda_add_pincfg(codec, &codec->user_pins, nid, cfg);
}
static ssize_t user_pin_configs_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct snd_hwdep *hwdep = dev_get_drvdata(dev);
struct hda_codec *codec = hwdep->private_data;
int err = parse_user_pin_configs(codec, buf);
if (err < 0) if (err < 0)
return err; return err;
return count; return count;
...@@ -553,3 +574,180 @@ int snd_hda_get_bool_hint(struct hda_codec *codec, const char *key) ...@@ -553,3 +574,180 @@ int snd_hda_get_bool_hint(struct hda_codec *codec, const char *key)
EXPORT_SYMBOL_HDA(snd_hda_get_bool_hint); EXPORT_SYMBOL_HDA(snd_hda_get_bool_hint);
#endif /* CONFIG_SND_HDA_RECONFIG */ #endif /* CONFIG_SND_HDA_RECONFIG */
#ifdef CONFIG_SND_HDA_PATCH_LOADER
/* parser mode */
enum {
LINE_MODE_NONE,
LINE_MODE_CODEC,
LINE_MODE_MODEL,
LINE_MODE_PINCFG,
LINE_MODE_VERB,
LINE_MODE_HINT,
NUM_LINE_MODES,
};
static inline int strmatch(const char *a, const char *b)
{
return strnicmp(a, b, strlen(b)) == 0;
}
/* parse the contents after the line "[codec]"
* accept only the line with three numbers, and assign the current codec
*/
static void parse_codec_mode(char *buf, struct hda_bus *bus,
struct hda_codec **codecp)
{
unsigned int vendorid, subid, caddr;
struct hda_codec *codec;
*codecp = NULL;
if (sscanf(buf, "%i %i %i", &vendorid, &subid, &caddr) == 3) {
list_for_each_entry(codec, &bus->codec_list, list) {
if (codec->addr == caddr) {
*codecp = codec;
break;
}
}
}
}
/* parse the contents after the other command tags, [pincfg], [verb],
* [hint] and [model]
* just pass to the sysfs helper (only when any codec was specified)
*/
static void parse_pincfg_mode(char *buf, struct hda_bus *bus,
struct hda_codec **codecp)
{
if (!*codecp)
return;
parse_user_pin_configs(*codecp, buf);
}
static void parse_verb_mode(char *buf, struct hda_bus *bus,
struct hda_codec **codecp)
{
if (!*codecp)
return;
parse_init_verbs(*codecp, buf);
}
static void parse_hint_mode(char *buf, struct hda_bus *bus,
struct hda_codec **codecp)
{
if (!*codecp)
return;
parse_hints(*codecp, buf);
}
static void parse_model_mode(char *buf, struct hda_bus *bus,
struct hda_codec **codecp)
{
if (!*codecp)
return;
kfree((*codecp)->modelname);
(*codecp)->modelname = kstrdup(buf, GFP_KERNEL);
}
struct hda_patch_item {
const char *tag;
void (*parser)(char *buf, struct hda_bus *bus, struct hda_codec **retc);
};
static struct hda_patch_item patch_items[NUM_LINE_MODES] = {
[LINE_MODE_CODEC] = { "[codec]", parse_codec_mode },
[LINE_MODE_MODEL] = { "[model]", parse_model_mode },
[LINE_MODE_VERB] = { "[verb]", parse_verb_mode },
[LINE_MODE_PINCFG] = { "[pincfg]", parse_pincfg_mode },
[LINE_MODE_HINT] = { "[hint]", parse_hint_mode },
};
/* check the line starting with '[' -- change the parser mode accodingly */
static int parse_line_mode(char *buf, struct hda_bus *bus)
{
int i;
for (i = 0; i < ARRAY_SIZE(patch_items); i++) {
if (!patch_items[i].tag)
continue;
if (strmatch(buf, patch_items[i].tag))
return i;
}
return LINE_MODE_NONE;
}
/* copy one line from the buffer in fw, and update the fields in fw
* return zero if it reaches to the end of the buffer, or non-zero
* if successfully copied a line
*
* the spaces at the beginning and the end of the line are stripped
*/
static int get_line_from_fw(char *buf, int size, struct firmware *fw)
{
int len;
const char *p = fw->data;
while (isspace(*p) && fw->size) {
p++;
fw->size--;
}
if (!fw->size)
return 0;
if (size < fw->size)
size = fw->size;
for (len = 0; len < fw->size; len++) {
if (!*p)
break;
if (*p == '\n') {
p++;
len++;
break;
}
if (len < size)
*buf++ = *p++;
}
*buf = 0;
fw->size -= len;
fw->data = p;
remove_trail_spaces(buf);
return 1;
}
/*
* load a "patch" firmware file and parse it
*/
int snd_hda_load_patch(struct hda_bus *bus, const char *patch)
{
int err;
const struct firmware *fw;
struct firmware tmp;
char buf[128];
struct hda_codec *codec;
int line_mode;
struct device *dev = bus->card->dev;
if (snd_BUG_ON(!dev))
return -ENODEV;
err = request_firmware(&fw, patch, dev);
if (err < 0) {
printk(KERN_ERR "hda-codec: Cannot load the patch '%s'\n",
patch);
return err;
}
tmp = *fw;
line_mode = LINE_MODE_NONE;
codec = NULL;
while (get_line_from_fw(buf, sizeof(buf) - 1, &tmp)) {
if (!*buf || *buf == '#' || *buf == '\n')
continue;
if (*buf == '[')
line_mode = parse_line_mode(buf, bus);
else if (patch_items[line_mode].parser)
patch_items[line_mode].parser(buf, bus, &codec);
}
release_firmware(fw);
return 0;
}
EXPORT_SYMBOL_HDA(snd_hda_load_patch);
#endif /* CONFIG_SND_HDA_PATCH_LOADER */
...@@ -61,6 +61,9 @@ static int probe_mask[SNDRV_CARDS] = {[0 ... (SNDRV_CARDS-1)] = -1}; ...@@ -61,6 +61,9 @@ static int probe_mask[SNDRV_CARDS] = {[0 ... (SNDRV_CARDS-1)] = -1};
static int probe_only[SNDRV_CARDS]; static int probe_only[SNDRV_CARDS];
static int single_cmd; static int single_cmd;
static int enable_msi; static int enable_msi;
#ifdef CONFIG_SND_HDA_PATCH_LOADER
static char *patch[SNDRV_CARDS];
#endif
module_param_array(index, int, NULL, 0444); module_param_array(index, int, NULL, 0444);
MODULE_PARM_DESC(index, "Index value for Intel HD audio interface."); MODULE_PARM_DESC(index, "Index value for Intel HD audio interface.");
...@@ -84,6 +87,10 @@ MODULE_PARM_DESC(single_cmd, "Use single command to communicate with codecs " ...@@ -84,6 +87,10 @@ MODULE_PARM_DESC(single_cmd, "Use single command to communicate with codecs "
"(for debugging only)."); "(for debugging only).");
module_param(enable_msi, int, 0444); module_param(enable_msi, int, 0444);
MODULE_PARM_DESC(enable_msi, "Enable Message Signaled Interrupt (MSI)"); MODULE_PARM_DESC(enable_msi, "Enable Message Signaled Interrupt (MSI)");
#ifdef CONFIG_SND_HDA_PATCH_LOADER
module_param_array(patch, charp, NULL, 0444);
MODULE_PARM_DESC(patch, "Patch file for Intel HD audio interface.");
#endif
#ifdef CONFIG_SND_HDA_POWER_SAVE #ifdef CONFIG_SND_HDA_POWER_SAVE
static int power_save = CONFIG_SND_HDA_POWER_SAVE_DEFAULT; static int power_save = CONFIG_SND_HDA_POWER_SAVE_DEFAULT;
...@@ -1286,8 +1293,7 @@ static unsigned int azx_max_codecs[AZX_NUM_DRIVERS] __devinitdata = { ...@@ -1286,8 +1293,7 @@ static unsigned int azx_max_codecs[AZX_NUM_DRIVERS] __devinitdata = {
[AZX_DRIVER_TERA] = 1, [AZX_DRIVER_TERA] = 1,
}; };
static int __devinit azx_codec_create(struct azx *chip, const char *model, static int __devinit azx_codec_create(struct azx *chip, const char *model)
int no_init)
{ {
struct hda_bus_template bus_temp; struct hda_bus_template bus_temp;
int c, codecs, err; int c, codecs, err;
...@@ -1346,7 +1352,7 @@ static int __devinit azx_codec_create(struct azx *chip, const char *model, ...@@ -1346,7 +1352,7 @@ static int __devinit azx_codec_create(struct azx *chip, const char *model,
for (c = 0; c < max_slots; c++) { for (c = 0; c < max_slots; c++) {
if ((chip->codec_mask & (1 << c)) & chip->codec_probe_mask) { if ((chip->codec_mask & (1 << c)) & chip->codec_probe_mask) {
struct hda_codec *codec; struct hda_codec *codec;
err = snd_hda_codec_new(chip->bus, c, !no_init, &codec); err = snd_hda_codec_new(chip->bus, c, &codec);
if (err < 0) if (err < 0)
continue; continue;
codecs++; codecs++;
...@@ -1356,7 +1362,16 @@ static int __devinit azx_codec_create(struct azx *chip, const char *model, ...@@ -1356,7 +1362,16 @@ static int __devinit azx_codec_create(struct azx *chip, const char *model,
snd_printk(KERN_ERR SFX "no codecs initialized\n"); snd_printk(KERN_ERR SFX "no codecs initialized\n");
return -ENXIO; return -ENXIO;
} }
return 0;
}
/* configure each codec instance */
static int __devinit azx_codec_configure(struct azx *chip)
{
struct hda_codec *codec;
list_for_each_entry(codec, &chip->bus->codec_list, list) {
snd_hda_codec_configure(codec);
}
return 0; return 0;
} }
...@@ -2460,15 +2475,32 @@ static int __devinit azx_probe(struct pci_dev *pci, ...@@ -2460,15 +2475,32 @@ static int __devinit azx_probe(struct pci_dev *pci,
return err; return err;
} }
/* set this here since it's referred in snd_hda_load_patch() */
snd_card_set_dev(card, &pci->dev);
err = azx_create(card, pci, dev, pci_id->driver_data, &chip); err = azx_create(card, pci, dev, pci_id->driver_data, &chip);
if (err < 0) if (err < 0)
goto out_free; goto out_free;
card->private_data = chip; card->private_data = chip;
/* create codec instances */ /* create codec instances */
err = azx_codec_create(chip, model[dev], probe_only[dev]); err = azx_codec_create(chip, model[dev]);
if (err < 0) if (err < 0)
goto out_free; goto out_free;
#ifdef CONFIG_SND_HDA_PATCH_LOADER
if (patch[dev]) {
snd_printk(KERN_ERR SFX "Applying patch firmware '%s'\n",
patch[dev]);
err = snd_hda_load_patch(chip->bus, patch[dev]);
if (err < 0)
goto out_free;
}
#endif
if (!probe_only[dev]) {
err = azx_codec_configure(chip);
if (err < 0)
goto out_free;
}
/* create PCM streams */ /* create PCM streams */
err = snd_hda_build_pcms(chip->bus); err = snd_hda_build_pcms(chip->bus);
...@@ -2480,8 +2512,6 @@ static int __devinit azx_probe(struct pci_dev *pci, ...@@ -2480,8 +2512,6 @@ static int __devinit azx_probe(struct pci_dev *pci,
if (err < 0) if (err < 0)
goto out_free; goto out_free;
snd_card_set_dev(card, &pci->dev);
err = snd_card_register(card); err = snd_card_register(card);
if (err < 0) if (err < 0)
goto out_free; goto out_free;
......
...@@ -99,7 +99,6 @@ struct snd_kcontrol *snd_hda_find_mixer_ctl(struct hda_codec *codec, ...@@ -99,7 +99,6 @@ struct snd_kcontrol *snd_hda_find_mixer_ctl(struct hda_codec *codec,
int snd_hda_add_vmaster(struct hda_codec *codec, char *name, int snd_hda_add_vmaster(struct hda_codec *codec, char *name,
unsigned int *tlv, const char **slaves); unsigned int *tlv, const char **slaves);
int snd_hda_codec_reset(struct hda_codec *codec); int snd_hda_codec_reset(struct hda_codec *codec);
int snd_hda_codec_configure(struct hda_codec *codec);
/* amp value bits */ /* amp value bits */
#define HDA_AMP_MUTE 0x80 #define HDA_AMP_MUTE 0x80
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment