crazii / SBEMU

legacy sound blaster emulation for DOS
GNU General Public License v2.0
625 stars 33 forks source link

Can someone maybe write a general Linux driver "porting guide"? #111

Open volkertb opened 6 months ago

volkertb commented 6 months ago

Question for both @jiyunomegami and @crazii :

Would it be possible to write a general "porting" guide for Linux drivers?

I'd like to try my hand at porting a Linux driver over to SBEMU.

I've looked into it before, but quickly got overwhelmed by all the header files referencing other header files, with required defines and such, and it was not easy to figure out what sources needed to be included and what sources needed to be stubbed or replaced. @jiyunomegami Since you were successful with porting some drivers to SBEMU already, I was wondering if you could summarize the common steps in some kind of document or guide that other (somewhat less experienced) C programmers could use to port more drivers over. In such a document, we would probably only have to focus on PCI devices, at least for the time being.

Specifically, I'm thinking about porting over the virtio-snd driver. This is in fact a paravirtualized sound driver that does not directly support any existing physical sound device, but which allows the sound output from SBEMU to be directed to a hyvervisor that supports the VirtIO Sound device (notably QEMU), when run in a DOS session in a VM instance. I know that support for actual host hardware is the real focus of SBEMU, but I'd still like to do this for a multitude of reasons:

I'll take a look at the Linux drivers that were already ported. But in the meantime, maybe you and perhaps also others can help me documenting the porting process. I wouldn't mind taking the lead on such documentation as I go.

If I have any specific questions about Linux driver porting as I work on this, I'll ask them in this thread.

Thanks!

volkertb commented 6 months ago

Also @crazii, would it be a good idea to replace the older drivers (the ones that you ported from the Mpxplay project) with Linux drivers as well?

It would put possible license concerns to rest (since the Linux drivers are pure GPLv2), and also those drivers are better maintained and more actively developed, since a lot more people work on the Linux kernel than on Mpxplay.

And also, it might simplify the code in SBEMU if you don't have different drivers ported from different projects.

(Although the Mpxplay drivers already appeared to use Linux driver headers, if I remember correctly.)

crazii commented 6 months ago

Yes the code between Linux source and mpxplay needs reconciliation, that is planned, but not quite sure when. I've never touched/read the linux part yet so you may ask jiyunomegami, but he seems busy recent days.

jiyunomegami commented 6 months ago

@volkertb Roughly speaking, a driver does the following:

  1. Detects the card, and initializes it when not just probing using /SCL
  2. Sets up DMA buffer(s) and at least one Linux PCM stream (struct snd_pcm_substream)
  3. Puts the PCM data from SBEMU into the DMA buffer in the right place at the right time
  4. Handles interrupts
  5. Sets the volume

For no.2 and the card detection part of no. 1, you can just declare the PCI IDs and use the functions in au_linux.c.

For the card initialization part of no. 1, you just need to find the function or functions in the Linux driver that does what you want, and call them. Usually there is just one function called something like snd_acmesound_probe or snd_acmesound_create. You will probably need to modify it, to remove unwanted Linux device file creation, use of unnecessary kernel module parameters, etc.

No. 3 would normally be done for you by the SBEMU or mpxplay code, and you would just need to figure out how to get the value of the PCM playback pointer. You do need to start/stop PCM playback. Linux drivers should have everything you need in a convenient structure, struct snd_pcm_ops. Just find the right one and call the corresponding function. Some drivers have more than one, for example one for digital and another for analog. The VirtIO driver has this in virtio_pcm_ops.c:

/* 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,
};

For No. 4, the Linux driver should have an interrupt handler, so you just call that. They should be easy to find, they look like irqreturn_t snd_acmesound_interrupt(int irq, void *dev_id) in Linux 6.x.x. irqreturn_t is just int.

For No. 5, you should first find out how to get some sound out at a fixed volume, and find how to set the volume later.

The Linux drivers are called from intermediate mpxplay type drivers located in mpxplay/au_cards. Here is mpxplay/au_cards/sc_allegro.c, simplified. I added some comments that start with //***


//*** PCI ID declaration
static pci_device_s allegro_devices[] = {
  {"Allegro-1", 0x125D, 0x1988, 0}, // Both ES1988S and ES1989S share this ID

//*** Here are all the functions/snd_pcm_ops in the Linux driver that we use
extern struct snd_pcm_ops snd_m3_playback_ops;
static struct snd_pcm_ops *allegro_ops = &snd_m3_playback_ops;
extern int snd_m3_probe (struct snd_card *card, struct pci_dev *pci,
                         int probe_only,
                         int spdif,
                         int enable_amp,
                         int amp_gpio);
extern irqreturn_t snd_m3_interrupt(int irq, void *dev_id);
extern void snd_m3_ac97_init (struct snd_card *card);

//*** card detection and initialization
static int ALLEGRO_adetect (struct mpxplay_audioout_info_s *aui)
{
  struct allegro_card_s *card = (struct allegro_card_s *)pds_zalloc(sizeof(struct allegro_card_s));
  if (!card)
    return 0;
  if (au_linux_find_card(aui, &card->card, allegro_devices) < 0)
    goto err_adetect;
  int probe_only = aui->card_controlbits & AUINFOS_CARDCNTRLBIT_TESTCARD;
  int spdif = 0; // Not implemented
  err = snd_m3_probe(card->card.linux_snd_card, card->card.linux_pci_dev, probe_only, spdif, 1, -1);
  if (err) goto err_adetect;

  //*** Some cards will have hardware MPU-401 and/or hardware FM(OPL3)
  struct snd_m3 *chip = (struct snd_m3 *)card->card.linux_snd_card->private_data;
  aui->mpu401_port = chip->iobase + 0x98;
  aui->mpu401 = 1;

  if (!probe_only)
    snd_m3_ac97_init(card->card.linux_snd_card);
  return 1;
err_adetect:
  ALLEGRO_close(aui);
  return 0;
}

//*** Sets up DMA buffer(s) and at least one Linux PCM stream (`struct snd_pcm_substream`)
static void ALLEGRO_setrate (struct mpxplay_audioout_info_s *aui)
{
  struct allegro_card_s *card = aui->card_private_data;
  if (aui->freq_card < 8000) {
    aui->freq_card = 8000;
  } else if (aui->freq_card > 48000) {
    aui->freq_card = 48000;
  }
  //*** magic parameters, unique to each card, you might need to experiment if you don't have perfect documentation
  aui->card_dmasize = 512;
  aui->card_dma_buffer_size = 4096;
  aui->dma_addr_bits = 28;
  aui->buffer_size_shift = 1;
  err = au_linux_make_snd_pcm_substream(aui, &card->card, allegro_ops);
  if (err) goto err_setrate;
  allegro_ops->prepare(card->card.pcm_substream);
  return;

 err_setrate:
  allegrodbg("setrate error\n");
}

//*** start playback
static void ALLEGRO_start (struct mpxplay_audioout_info_s *aui)
{
  struct allegro_card_s *card = aui->card_private_data;
  allegro_ops->trigger(card->card.pcm_substream, SNDRV_PCM_TRIGGER_START);
}

//*** stop playback
static void ALLEGRO_stop (struct mpxplay_audioout_info_s *aui)
{
  struct allegro_card_s *card = aui->card_private_data;
  allegro_ops->trigger(card->card.pcm_substream, SNDRV_PCM_TRIGGER_STOP);
}

//*** PCM playback pointer
static long ALLEGRO_getbufpos (struct mpxplay_audioout_info_s *aui)
{
  struct allegro_card_s *card = aui->card_private_data;
#if 1 // just use the pointer directly
  unsigned long bufpos = allegro_ops->pointer(card->card.pcm_substream);
  bufpos <<= 1;
#endif
  if (bufpos < aui->card_dmasize)
    aui->card_dma_lastgoodpos = bufpos;
  return aui->card_dma_lastgoodpos;
}

//*** IRQ handler
static int ALLEGRO_IRQRoutine (struct mpxplay_audioout_info_s *aui)
{
  struct allegro_card_s *card = aui->card_private_data;
  int handled = snd_m3_interrupt(card->card.irq, card->card.linux_snd_card->private_data);
  return handled;
}
volkertb commented 6 months ago

Thank you @jiyunomegami!

So if I understand correctly:

Have I understood that correctly?

I started porting and integrating the Linux virtio sound driver in SBEMU, and so far, this all seems to make sense.

I will undoubtedly have questions as I work on this, but thanks for giving me the necessary pointers to get started on this! :slightly_smiling_face:

jiyunomegami commented 5 months ago
  • Each Linux driver we port will require a "shim" that still follows the Mpxplay driver model, at least for the time being.
  • The struct snd_pcm_ops in each Linux sound driver is basically the common integration point when interfacing them with the corresponding shims mpxplay/au_cards.

Have I understood that correctly?

Yes. There is one thing that I only hinted at, when writing about card initialization. You will probably want to disable large blocks of the Linux source code, by commenting them out or using #if 0/#endif. For example, you shouldn't need to compile anything pertaining to audio capture, or the Linux /proc file system interface. And things like request_irq or devm_request_irq, that are normally used to install interrupt handlers, should not be used. Every driver is a little bit different, so it is hard to write a guide for that. If you get the driver to compile, but get missing function errors at link time, you can try commenting out whatever is using those functions.